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:
@veeva/vault
JavaScript package provides functions to validate and exchange data with your server code. Page
component. This links the client code and server code in Vault. Your code will run when a user loads the Custom Page in Vault.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.
Deploy a Custom Page to your Vault to see how it works without writing any 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:
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:
Before starting this tutorial, you should have the following:
Start from an empty directory that will be the root of your project. To install dependencies:
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
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
Install React and react-dom for the runtime:
npm install react react-dom
Install esbuild to bundle your code:
npm install --save-exact --save-dev esbuild
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:
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
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>
);
});
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:
Create a file named distribution-manifest.json
:
Windows
echo. > distribution-manifest.json
Mac
touch distribution-manifest.json
Add the following to this file:
{
"name": "hello_world__c",
"pages": [
{
"name": "hello_world__c",
"file": "dist/hello-world.js"
}
]
}
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:
Create a file named esbuild.mjs
:
Windows
echo. > touch esbuild.mjs
Mac
touch esbuild.mjs
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',
});
Build your code using esbuild:
node esbuild.mjs
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
Now, it’s time to deploy your client code distribution to Vault using the Vault REST API.
From your hello-world
directory:
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"
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'\'')
);'
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.
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.
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:
src/main/java/com/veeva/vault/custom/HelloWorld.java
to the root directory of your project.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();
}
}
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>
);
});
Use the Vault Java SDK Maven plugin to deploy the server code.
From your project root:
Deploy the Java server code using the Vault Java SDK Maven plugin.
mvn vaultjavasdk:clean vaultjavasdk:package vaultjavasdk:deploy
You can now deploy your client code distribution to Vault using the Vault REST API.
From your project root:
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"
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'\'')
);
'
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!
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.
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
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.
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>
);
});
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.
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.
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.
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.
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.
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);
...
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 |
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>
);
});
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>
...
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.
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.
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:
sample_distribution__c
hello_world__c
with a default export my_page__c
with named export mypage
{
"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"
]
}
To upload your client code to Vault, create a ZIP file of your client code distribution and deploy it using the Vault REST API.
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.
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.
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"
To access your Custom Page in Vault, you must first configure and secure your page using MDL.
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
.
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.
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}')
);
This section lists the limits and restrictions on Custom Pages and provides guidance on using external libraries.
The maximum number of Custom Pages that can exist for a single Vault is 200.
The maximum size of Custom Page Distributions in a single Vault is 50 MB.
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.
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
.
This section provides guidance on working with common external libraries to build Custom Pages in Vault.
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>
);
})
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.
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™.