Overview

Vault Custom Pages enable developers to extend Vault Platform by adding custom UI to Vault. Developers can build completely custom user interfaces in Vault, using JavaScript and their preferred frameworks or libraries to write the client code. On the server side, developers can leverage the power of the Vault Java SDK to retrieve and manage Vault data.

To create a custom page, developers need to:

Getting Started

The tutorials in this section help you get started with Custom Pages and cover the basics of structuring and deploying Custom Pages to Vault. Try out Custom Pages with the quick tutorial or explore the sample code. Then, follow the hands-on Hello World tutorials to learn more detailed concepts.

Try it Out

Deploy a Custom Page to your Vault to see how it works without writing any code:

  1. Download the hello-world-react-esbuild package from the Custom Page Sample Project.
  2. In a sandbox Vault, navigate to Admin > Deployment > Inbound Packages and import and deploy the package. Learn more about importing and deploying VPK packages in Vault Help.
  3. Navigate to the Hello World (React Esbuild) tab in your Vault. You’ll see a complete Custom Page, leveraging JavaScript and the Vault Java SDK to display the text “Hello World!” and your user ID.

Sample Code

The Custom Pages sample code includes basic Hello World examples for common frameworks and build tools:

The project also provides examples of common Custom Page use cases with React and esbuild:

Hello World!

Learn how Custom Pages work by writing and deploying your first Custom Page without server code. The page will display the text “Hello World!” in Vault.

In this example, you will:

Prerequisites

Before starting this tutorial, you should have the following:

Installing Dependencies

Start from an empty directory that will be the root of your project. To install dependencies:

  1. Create a directory for your client code and this Hello World sample:

    Windows

    mkdir client\hello-world && cd client\hello-world
    

    Mac

    mkdir -p client/hello-world && cd client/hello-world
    
  2. Install the Vault npm package:

    npm install https://vault-web-sdk-releases-public.s3.us-west-2.amazonaws.com/main/veeva-vault-24.3.2-release.1.0.1.tgz
    
  3. Install React and react-dom for the runtime:

    npm install react react-dom
    
  4. Install esbuild to bundle your code:

    npm install --save-exact --save-dev esbuild
    

Developing Client Code

To display a Custom Page in Vault, you first need to define the page using JavaScript. The following client code for this sample project is written in React and produces a <div> element containing the text “Hello World!” Learn more about developing client code.

From your client/hello-world directory:

  1. Create a file for your client code in src/hello-world.jsx:

    Windows

    mkdir src && echo. > src\hello-world.jsx
    

    Mac

    mkdir src && touch src/hello-world.jsx
    
  2. Add the following client code:

    // src/hello-world.jsx
    import React from 'react';
    import { createRoot } from 'react-dom/client';
    import { definePage } from '@veeva/vault';
    
    export default definePage(({ element }) => {
        const root = createRoot(element);
        root.render(
            <div>Hello World!</div>
        );
    });
    

Creating a Manifest File

The distribution-manifest.json file describes a client code distribution for your client code. It defines the name of your Custom Page and the path to the built code. Learn more about creating a client code distribution.

From your client/hello-world directory:

  1. Create a file named distribution-manifest.json:

    Windows

    echo. > distribution-manifest.json
    

    Mac

    touch distribution-manifest.json
    
  2. Add the following to this file:

    1. Name of your distribution
    2. Name of your page
    3. Path to the built (not source) code for your page. You will bundle your source code in the next step.
    {
        "name": "hello_world__c",
        "pages": [
            {
                "name": "hello_world__c",
                "file": "dist/hello-world.js"
            }
        ]
    }
    

Building your Code

Before you can deploy your code, you will bundle it using esbuild and package it as a ZIP file.

From your client/hello-world directory:

  1. Create a file named esbuild.mjs:

    Windows

    echo. > touch esbuild.mjs
    

    Mac

    touch esbuild.mjs
    
  2. Add the following to this file to specify the source file containing your page export and define how your code is bundled:

    // esbuild.mjs
    import * as esbuild from 'esbuild';
    await esbuild.build({
        entryPoints: ['src/hello-world.jsx'],
        bundle: true,
        sourcemap: true,
        outdir: 'dist',
        format: 'esm',
    });
    
  3. Build your code using esbuild:

    node esbuild.mjs
    
  4. Zip the directory that contains your bundled code, and include the distribution manifest file:

    Windows (with 7-zip):

    7z a -tzip hello-world.zip dist distribution-manifest.json
    

    Mac

    zip -rq hello-world.zip dist distribution-manifest.json 
    

Deploying your Code

Now, it’s time to deploy your client code distribution to Vault using the Vault REST API.

From your hello-world directory:

  1. Obtain a valid API Session ID.
  2. Upload the distribution ZIP using the Vault REST API. This example uses cURL:

    Windows

    curl -L https://$HOST/api/v25.1/uicode/distributions ^
            -H "Authorization: $SESSION_ID" ^
            -F "file=@hello-world.zip" 
    

    Mac

    curl -L https://$HOST/api/v25.1/uicode/distributions \
            -H "Authorization: $SESSION_ID" \
            -F "file=@hello-world.zip" 
    
  3. Create a Page component in the Vault configuration using MDL. This example uses cURL:

    Windows

    curl -L https://$HOST/api/mdl/execute ^
    -H "Content-Type: text/plain;" ^
    -H "Authorization: $SESSION_ID" ^
    -d "RECREATE Page hello_world__c (label('Hello World'),url_path_name('hello-world'),page_client_code('Pageclientcode.hello_world__c'),client_distribution('Clientdistribution.hello_world__c'));"
    

    Mac

    curl -L https://$HOST/api/mdl/execute \
            -H "Content-Type: text/plain;" \
            -H "Authorization: $SESSION_ID" \
    -d 'RECREATE Page hello_world__c ( 
    label('\''Hello World'\''), 
    url_path_name('\''hello-world'\''),
    page_client_code('\''Pageclientcode.hello_world__c'\''),
    client_distribution('\''Clientdistribution.hello_world__c'\'')
    );'
    

Viewing your Page

View your Page at: https://$HOST/ui/#custom/page/hello-world. You should see “Hello World!” printed below your Vault’s navigation bar.

Congratulations! You built your first Custom Page from scratch using standard client-side development tools!

Continue to the next tutorial to add server-side Java SDK code to your Hello World project.

Hello World!: Adding Server Code

To build upon the first Hello World example, you will now add server code to your page using the Vault Java SDK to retrieve data and pass it to the client code when the Custom Page loads.

Prerequisites

  1. Java SDK pre-requisites
  2. A working Java SDK project
  3. The Hello World client code deployed

Adding a Page Controller

To retrieve and manage Vault data, on the server, implement a PageController in your server code. The following example defines the onLoad() method. When a user loads a Custom Page in Vault, this custom method creates and returns the currently authenticated user’s ID with name userId.

From the root of your project:

  1. Add a Java class src/main/java/com/veeva/vault/custom/HelloWorld.java to the root directory of your project.
  2. Add the following server code to construct a JsonObject to return the user ID:

    package com.veeva.vault.custom;
    
    import com.veeva.vault.sdk.api.core.RequestContext;
    import com.veeva.vault.sdk.api.core.ServiceLocator;
    import com.veeva.vault.sdk.api.executeas.ExecuteAs;
    import com.veeva.vault.sdk.api.executeas.ExecuteAsUser;
    import com.veeva.vault.sdk.api.json.JsonService;
    import com.veeva.vault.sdk.api.page.PageController;
    import com.veeva.vault.sdk.api.page.PageControllerInfo;
    import com.veeva.vault.sdk.api.page.PageLoadContext;
    import com.veeva.vault.sdk.api.page.PageLoadResponse;
    
    @ExecuteAs(ExecuteAsUser.REQUEST_OWNER)
    @PageControllerInfo()
    public class HelloWorld implements PageController {
        @Override
        public PageLoadResponse onLoad(PageLoadContext context) {
            JsonService jsonService = ServiceLocator.locate(JsonService.class);
            return context.newLoadResponseBuilder()
                    .withData(jsonService.newJsonObjectBuilder()
                            .setValue("userId", RequestContext.get().getInitiatingUserId())
                            .build())
                    .build();
        }
    }
    

Updating Client Code

You can now update your client code to retrieve the userId value from the PageController you implemented in the previous step. The following example retrieves the data parameter containing the currently authenticated user’s ID and displays it on the Custom Page.

From your hello-world directory, replace the contents of /src/hello-world.jsx with the following code:

// src/hello-world.jsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import { definePage } from '@veeva/vault';

export default definePage(({ data = {}, element }) => {
    const root = createRoot(element);
    root.render(
        <div>Hello user {data.userId}!</div>
    );
});

Deploying Server Code

Use the Vault Java SDK Maven plugin to deploy the server code.

From your project root:

  1. Obtain a valid API Session ID.
  2. Deploy the Java server code using the Vault Java SDK Maven plugin.

    mvn vaultjavasdk:clean vaultjavasdk:package vaultjavasdk:deploy
    

Deploying Client Code

You can now deploy your client code distribution to Vault using the Vault REST API.

From your project root:

  1. Obtain a valid API Session ID.
  2. Build and deploy your client code distribution.

    Windows

    cd client\hello-world
    rmdir /s /q dist && node esbuild.mjs
    7z a -tzip hello-world.zip dist distribution-manifest.json
    curl -L https://$HOST/api/v25.1/uicode/distributions ^
    -H "Authorization: $SESSION_ID" ^
    -F "file=@hello-world.zip"
    

    Mac

    cd client/hello-world
    rm -rf dist && node esbuild.mjs
    zip -rq hello-world.zip dist distribution-manifest.json 
    curl -L https://$HOST/api/v25.1/uicode/distributions \
            -H "Authorization: $SESSION_ID" \
            -F "file=@hello-world.zip" 
    

Updating Page Configuration

Use MDL to update the Page configuration to use the server code you uploaded. This example uses cURL:

Windows

curl -L https://$HOST/api/mdl/execute ^
-H "Content-Type: text/plain;" ^
-H "Authorization: $SESSION_ID" ^
-d "ALTER Page hello_world__c (page_controller('controller.com.veeva.vault.custom.HelloWorld'));"

Mac

curl -L https://$HOST/api/mdl/execute \
        -H "Content-Type: text/plain;" \
        -H "Authorization: $SESSION_ID" \
-d 'ALTER Page hello_world__c ( 
   page_controller('\''Pagecontroller.com.veeva.vault.custom.HelloWorld'\'')
);
'

Viewing your Page

View your Page at: https://$HOST/ui/#custom/page/hello-world.

You should see “Hello user 1234!” but with your Vault user ID.

Congratulations! You built a Custom Page using standard client-side development tools and the Vault Java SDK!

Developing Client Code

You can use JavaScript and the @veeva/vault package to create the client code for Custom Pages in Vault.

The @veeva/vault package provides functions to define a Custom Page and communicate with the server code. It also provides a JavaScript client for the Vault REST API. Learn more about retrieving and managing data with the @veeva/vault package.

Installing the @veeva/vault Package

The @veeva/vault package is required to integrate your code into Vault. If you followed the Hello World tutorial, you have already installed this package.

Use npm to install the latest version of the package:

npm install https://vault-web-sdk-releases-public.s3.us-west-2.amazonaws.com/main/veeva-vault-24.3.2-release.1.0.1.tgz

See all available versions.

Defining a Custom Page

Custom Page client code must be an export in a module that returns the value of the definePage() function exported from @veeva/vault, or a function that returns a promise that resolves to the same.

The Hello World examples use a default export to export the page definition and make it available in Vault.

You can also use a named export:

import { definePage } from '@veeva/vault';

export hello_world definePage(({ ... }) => {
    ...
});

When using a named export, the export name must be provided in the page’s export attribute in the distribution-manifest.json, and it must match exactly.

{
    "name": "hello_world__c",
    "pages": [
        {
            "name": "hello_world__c",
            "file": "dist/hello-world.js",
            "export": "hello_world" 
        }
    ]
}

When using a default export, you can omit the export attribute. Learn more about creating the manifest file.

Using the element Parameter

The element parameter is the root element for the page.

Using vanilla JavaScript, you can inject your client code into the page by appending your custom elements to the element parameter as children. The following example appends an <h2> element with the text “Hello World!”:

import { definePage } from '@veeva/vault';
export default definePage(({ element }) => {
    const helloWorld = document.createElement('h2');
    helloWorld.textContent = 'Hello World!';
    element.appendChild(helloWorld);
});

When using React, create a root from the element parameter and define your code in root.render():

import React from 'react';
import { createRoot } from 'react-dom/client';
import { definePage } from '@veeva/vault';

export default definePage(({ element }) => {
    const root = createRoot(element);
    root.render(
        <h2>Hello World!</h2>
    );
});

Developing Server Code

To develop Custom Page server code with the Vault Java SDK, implement the PageController interface. This server code can send and receive data from the Custom Page client code in Vault. Learn more in the Javadocs.

All implementations of PageController must be decorated with @PageControllerInfo and @ExecuteAs annotations.

To deploy your Java server code, use the Vault Java SDK Maven plugin. Learn more about deploying Vault Java SDK code in the SDK documentation.

@ExecuteAs Annotation

A PageController must use @ExecuteAs(ExecuteAsUser.REQUEST_OWNER) to execute as the user at the keyboard. This means all user permissions are respected when reading data to display to the user or when performing operations.

onLoad()

The onLoad() method executes when a user accesses the URL for the Custom Page in the browser.

The code you provide in this method can perform validation or return data to the client. This method can return data as JSON or a User-Defined Model.

In the client code, retrieve this data using the data parameter from the definePage() function.

onEvent()

The onEvent() method executes when the client code sends an event to the server code using the sendEvent() function.

From the PageEventContext, you can retrieve the name of the event and, optionally, any data passed from the client.

Write code here to process the event and, optionally, return data to the client. Data can be returned as JSON or a User-Defined Model.

Returning Errors

You can construct and return errors to the client when handling the onLoad() and onEvent() methods. Vault displays Page Load errors to the user. The client code should handle Page Event errors.

Using User-Defined Models

Instead of using a JsonObject to work with data on the server, PageController can work directly with a UserDefinedModel (UDM) to send and receive data to the client.

Both onLoad() and onEvent() can return data as a UDM.

onLoad():

return context.newLoadResponseBuilder()
            .withData(loadResponseData, HelloWorldLoadResponse.class)
            .build();

onEvent():

return context.newEventResponseBuilder()
            .withData(loadResponseData, HelloWorldEventResponse.class)
            .build();

The onEvent() method can receive data from the client as a UDM:

public PageEventResponse onEvent(PageEventContext context) {
    HelloWorldEvent eventData = context.getData(HelloWorldEvent.class);
...

Sending and Receiving Data

The @veeva/vault JavaScript package allows you to send and receive data from the server-side PageController.

From your client code, you can:

For reference, the definePage() function provides the following parameters:

Parameter Description
data Data returned from PageController’s onLoad() response
element Root HTML element to append to when writing client code
pageContext Page context providing the reload() function
sendEvent Function to send an event to the PageController. Requires an event name and optional data

Retrieving Data on Page Load

Loading your Custom Page in the browser calls the PageController’s onLoad() method. In the client code, the data parameter provides the data returned from the onLoad() method.

For example, if the PageController’s onLoad() method returns the userId of the currently authenticated user, you can get the user ID from the data parameter and display it on the page:

import React from 'react';
import { createRoot } from 'react-dom/client';
import { definePage } from '@veeva/vault';

export default definePage(({ data = {}, element }) => {
    const { userId = '<userId>' } = data;
    const root = createRoot(element);
    root.render(
        <div>Hello user {userId}!</div>
    );
});

Reloading the Page

You can trigger a page reload from the client using the pageContext parameter’s reload() function. This function calls the PageController’s onLoad() method.

export default definePage(({ element, pageContext }) => {
...
        <button onClick={() => pageContext.reload()} >Refresh</button>
...

Sending Events

From the client code, use sendEvent() to send an event with data to the PageController. This calls the PageController’s onEvent() method, which processes the data and returns it as a JSON response.

export default definePage(({ element, sendEvent }) => {
...
async function fetchData() {
    const serverResponse = await sendEvent("myEvent", requestData);
...

You can queue a maximum of 50 events in the client code for processing.

Using the Vault REST API

You can use the Vault REST API directly from your client code by importing the vaultApiClient. You can retrieve Vault data using the vaultApiClient.fetch() async function:

import { vaultApiClient } from "@veeva/vault";
...
  async function fetchDocuments() {
      vaultApiClient.fetch('/v25.1/objects/documents', {
        headers: {
          "X-VaultAPI-ClientID": "hello-world",
          "Content-Type": "application/json",
        },
      })
        .then(...);
...

Vault automatically uses the host name of the currently authenticated Vault and appends the /api suffix.

We recommend setting a unique X-VaultAPI-ClientID header value to track in the API Usage Logs.

API calls made from Custom Pages count towards API rate limits and are filtered with Client ID Filtering.

Users do not need API permission to view a Custom Page that calls the Vault REST API from code.

Client Code Distributions

You can deploy your client code to Vault by uploading a ZIP file containing multiple files. These files must be deployed together in order to function. We call this collection of code a client code distribution.

Vault requires a name for your client code distribution along with other metadata. Set this metadata in the distribution-manifest.json file at the root of your ZIP file.

The distribution-manifest.json file also contains the following distribution metadata:

Name Description
name String
required
Name of the client code distribution. Must be unique and end with __c.
pages[] Array of Custom Page definitions
required
An array of Custom Page definitions consisting of the page name, file path, and optional export name.
pages[].name String
required
Name of the page client code. Must be unique and end with __c.
pages[].file String
required
Path to the file containing the export
pages[].export String
optional
Name of the export. If omitted, uses “default”.
stylesheets[] Array of strings
optional
File paths to stylesheets to attach to the main HTML document

The following code:

{
    "name": "sample_distribution__c", 
    "pages": [
        {
            "name": "hello_world__c",
            "file": "dist/hello-world.js"
        },
        {
            "name": "my_page__c",
            "file": "dist/my-page.js",
            "export": "mypage"
        }
    ],
    "stylesheets": [
        "styles/normalize.css",
        "styles/styles.css"
    ]
}

Deploying your Client Code

To upload your client code to Vault, create a ZIP file of your client code distribution and deploy it using the Vault REST API.

Bundling your Code

Most modern JavaScript code is bundled before it is deployed. Vault Custom Pages supports bundled code but it is not required. We recommend including source maps when bundling. This makes it easier to debug your code.

Creating a ZIP File

To deploy a single client code distribution, create a ZIP file that contains your code and the distribution-manifest.json file. When creating a ZIP file, use a tool that follows the ZIP file specification.

Deploying Using the Vault REST API

Deploy a single client code distribution as a ZIP file using the Vault REST API.

Windows

curl -L https://$HOST/api/v25.1/uicode/distributions ^
        -H "Authorization: $SESSION_ID" ^
        -F "file=@hello-world.zip"

Mac

curl -L https://$HOST/api/v25.1/uicode/distributions \
        -H "Authorization: $SESSION_ID" \
        -F "file=@hello-world.zip" 

Configuring your Custom Page

To access your Custom Page in Vault, you must first configure and secure your page using MDL.

Creating a Page

The Page component type links the client code and the server code for your Custom Page. Use the following attributes to create a Page component:

Attribute Description Format
client_distribution The name of your Custom Page Distribution in the distribution-manifest.json file Clientdistribution.{distribution_file_name}
page_client_code The name of your page in the distribution-manifest.json file Pageclientcode.{page_name}
page_controller The fully qualified name of your PageController Pagecontroller.{path.to.PageController}
url_path_name A unique URL-safe path to use to access your Page {url_path}

For example, the following MDL creates a Page component named hello_world__c:

RECREATE Page hello_world__c (
   label('Hello World!'),
   active(true),
   client_distribution('Clientdistribution.hello_world__c'),
   page_client_code('Pageclientcode.hello_world__c'),
   page_controller('Pagecontroller.com.veeva.vault.custom.pages.HelloWorld'),
   url_path_name('hello-world')
);

You can access the example Custom Page from the following URL: https://$HOST/ui/#custom/page/hello-world.

Managing Page Permissions

A user’s security profile must grant them permission to view a Page component.

To secure your Page, navigate to Admin > Users & Groups > Permission Sets in your Vault. Create a new permission set or select an existing custom one, then navigate to the Pages tab and click Edit. Select the View permission for All Pages or a specific page, then click Save.

Creating a Tab

You can create a Tab component to link to your Custom Page from your Vault’s navigation bar.

Users must have Tab and Page permissions to access the Page in a Tab.

The url must begin with https://${Vault.domain}/ui/#custom/page/${Page.url_path_name}. This includes URL tokens to retrieve the Vault domain and the path to the Custom Page. URL parameters can be provided to your Page component by appending to the url value.

The following example uses MDL to create a Tab that references the Page configuration:

RECREATE Tab hello_world__c (
   active(true),
   label('Hello World!'),
   order(100),
   page('Page.hello_world__c'),
   url('https://${Vault.domain}/ui/#custom/page/${Page.url_path_name}')
);

Limits & Restrictions

This section lists the limits and restrictions on Custom Pages and provides guidance on using external libraries.

Page Limits

The maximum number of Custom Pages that can exist for a single Vault is 200.

Size Limits

The maximum size of Custom Page Distributions in a single Vault is 50 MB.

Runtime Limits

Runtime limits are:

All Java SDK limits apply to the Custom Page PageController.

REST API calls made for Custom Pages are subject to API rate limits.

Runtime Considerations

Vault executes custom JavaScript independently from the main HTML document and writes Custom Page HTML to the shadow DOM. This protects your custom code from conflicting with the Vault application and vice versa.

This may result in unexpected behavior if your custom code or a library you import assumes it is executing in the top HTML document. For example, you should register stylesheets in the distribution-manifest.json file instead of in your code.

The shadow DOM does not support @font-face.

Working with External Libraries

This section provides guidance on working with common external libraries to build Custom Pages in Vault.

Styled Components

The following code uses styled-components to create a style root and use StyleSheetManager:

import { definePage } from '@veeva/vault';
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import { StyleSheetManager } from 'styled-components'


// https://styled-components.com/docs/basics#installation
export default definePage(({ element }) => {

    const stylesRoot = document.createElement('div');
    const reactRoot = document.createElement('div');
    element.appendChild(stylesRoot);
    element.appendChild(reactRoot);

    const root = createRoot(reactRoot);
    root.render(
        <StyleSheetManager target={stylesRoot}>
            <App />
        </StyleSheetManager>
    );
})

Emotion-Based Libraries

Some libraries (e.g., Emotion-based libraries such as Material UI) require a specific setup when defining your Custom Page in order for their CSS to work in Custom Pages. The following example defines a Custom Page using Material UI:

import { definePage } from '@veeva/vault';
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import createCache from '@emotion/cache';
import { CacheProvider } from '@emotion/react';

// https://mui.com/material-ui/getting-started/usage/
export default definePage(({ element }) => {
    const myCache = createCache({
        key: 'material-ui-emotion-cache',
        container: element
    });

    const root = createRoot(element);
    root.render(
        <CacheProvider value={myCache}>
            <App />
        </CacheProvider>
    );
})

Some libraries that use Emotion for styling rely on attributes set on the <html> tag to render styles correctly. However, when UI code is placed within the shadow DOM, it isn’t wrapped in an <html> tag. This can lead to visual and CSS issues due to the absence of necessary attributes.

To address these issues, we recommend wrapping your UI code in a <div> or a similar container and applying the necessary attributes to ensure proper styling. For example, Chakra UI requires the following attributes for correct CSS rendering:

data-theme="light" style={{ 'color-scheme': 'light' }}

You should add these attributes to a container element that encapsulates the entire UI. Different attributes might be necessary when using other UI libraries. We advise inspecting the <iframe> in which the UI code is initially served to identify any specific attributes that must be applied.

Resources

Sample Code

The sample code includes simple Hello World examples using the most common frameworks and bundling tools. We also provide samples of common use cases.

Sample code is available at the Veeva GitHub™.

Developer Support

Join Vault for Developers on Veeva Connect.