Custom content lets you create fully interactive applications that run inside entity pages. Your app runs in a sandboxed iframe and can read/write entity data via the REST API.
How it works Seal injects window.SEAL_CONTEXT into your HTML with credentials to access the API.
Context
Your app receives this context automatically:
interface SealContext {
// Entity data (snapshot at load time)
entityId : string ;
version : string | null ;
entity : EntityData ; // Full entity - render instantly, no fetch needed
// User context
workspaceId : string ; // Current entity's system
userId : string ;
actorRoleId : string | null ;
displayName : string ;
email : string ;
orgSlug : string ;
// Systems the user has access to
systems : Array <{
id : string ;
slug : string ;
name : string ;
permissionLevel : 'VIEWER' | 'OPERATOR' | 'BUILDER' | 'WORKSPACE_ADMIN' | null ;
access : 'direct' | 'transitive' ; // transitive = inherited via hierarchy
}>;
// API access
apiToken : string ;
apiUrl : string ;
}
API examples
Instant render
Edit and refresh
// Entity is included in context - no fetch needed for initial render
const { entity , userId , displayName } = window . SEAL_CONTEXT ;
document . getElementById ( 'title' ). textContent = entity . title ;
document . getElementById ( 'user' ). textContent = `Viewed by ${ displayName } ` ;
Development
Start local dev server
Run your app on port 5347 :
Enter development mode
Open a custom content entity in Seal and click Start Development Mode .
Handle context
In dev mode, context is sent via postMessage: let ctx = window . SEAL_CONTEXT ;
window . addEventListener ( 'message' , ( e ) => {
if ( e . data ?. type === 'SEAL_CONTEXT' ) ctx = e . data . payload ;
});
if ( ! ctx ) window . parent . postMessage ({ type: 'REQUEST_CONTEXT' }, '*' );
Your code works in both dev mode (postMessage) and production (injected context) with the pattern above.
Deploy
Create zip
The zip must contain index.html at the root.
Deploy
curl -X POST " $SEAL_URL /api/custom-content/ $ENTITY_ID /deploy" \
-H "Authorization: Bearer $API_KEY " \
-H "Content-Type: application/zip" \
--data-binary @build.zip
Complete example
An editable notes field that saves on blur:
<! DOCTYPE html >
< html lang = "en" >
< head >
< meta charset = "UTF-8" />
< title > Custom Content </ title >
< style >
body { font-family : system-ui , sans-serif ; padding : 1 rem ; }
textarea { width : 100 % ; height : 200 px ; font-size : 14 px ; }
.meta { color : #666 ; font-size : 12 px ; margin-bottom : 0.5 rem ; }
.saving { opacity : 0.6 ; }
</ style >
</ head >
< body >
< div class = "meta" > Loading... </ div >
< textarea disabled > Loading... </ textarea >
< script >
let ctx = window . SEAL_CONTEXT ;
let currentEntity = null ;
// Dev mode: receive context via postMessage
window . addEventListener ( 'message' , ( e ) => {
if ( e . data ?. type === 'SEAL_CONTEXT' ) {
ctx = e . data . payload ;
init ();
}
});
if ( ctx ) {
init ();
} else {
window . parent . postMessage ({ type: 'REQUEST_CONTEXT' }, '*' );
}
function init () {
currentEntity = ctx . entity ; // Instant - no fetch needed
render ();
}
function render () {
const notes = currentEntity . fields . notes ?. value ?? '' ;
document . querySelector ( '.meta' ). textContent =
`Editing as ${ ctx . displayName } ( ${ ctx . email } )` ;
const textarea = document . querySelector ( 'textarea' );
textarea . value = notes ;
textarea . disabled = false ;
textarea . onblur = () => saveNotes ( textarea . value );
}
async function saveNotes ( value ) {
const textarea = document . querySelector ( 'textarea' );
textarea . classList . add ( 'saving' );
await fetch ( ` ${ ctx . apiUrl } /entities/ ${ ctx . entityId } /fields/notes` , {
method: 'PATCH' ,
headers: {
Authorization: `Bearer ${ ctx . apiToken } ` ,
'Content-Type' : 'application/json'
},
body: JSON . stringify ({ value })
});
// Refresh to get server state
currentEntity = await fetch (
` ${ ctx . apiUrl } /entities/ ${ ctx . entityId } ` ,
{ headers: { Authorization: `Bearer ${ ctx . apiToken } ` } }
). then ( r => r . json ());
textarea . classList . remove ( 'saving' );
render ();
}
</ script >
</ body >
</ html >
API reference Full REST API documentation
The SDK uses postMessage instead of REST API. Both work, but REST API is recommended for new apps.