Xeonr Developer Docs
Custom Renderers

Renderer Types & Scopes

Understand the two renderer types, entrypoints, scopes, and virtual files.

Renderer types

There are two types of renderer, each operating at a different level:

Bucket type renderers

A bucket type renderer replaces the entire bucket UI. When a bucket is created with your integration's bucket type, your renderer takes over the full view — the default file list, folder navigation, and upload UI are all replaced.

Use this when your integration provides a fundamentally different experience for the bucket, such as a custom dashboard, a specialised file manager, or an application-style interface.

The renderer receives a bucket scope:

{ type: 'bucket', bucketId: 'bkt_abc123' }

During deployment, bucket type renderers are registered against a bucket type slug on your integration (via UPLIM_BUCKET_TYPE_SLUG).

Content type renderers

A content type renderer handles individual resources within a bucket — specific files, folders, or virtual files. The default bucket UI remains intact, and your renderer is invoked when a user opens a matching resource.

Content types are matched based on criteria configured on your integration:

  • Target kinds — which resource types to match: uploads (files), folders, integration folders, or virtual files
  • Filename pattern — an optional regex to match specific file extensions or names (e.g. \.md$ for Markdown files)

When multiple content types could match a resource, they are evaluated in priority order.

The renderer receives a scope matching the resource type:

// For uploads
{ type: 'upload', bucketId: 'bkt_abc', uploadId: 'upl_xyz' }

// For folders
{ type: 'folder', bucketId: 'bkt_abc', folderId: 'fld_xyz', path: '/my-folder' }

// For virtual files
{ type: 'virtual-file', bucketId: 'bkt_abc', path: '/notes/my-note', folderId: 'fld_xyz' }

During deployment, content type renderers are registered against a content type ID on your integration (via UPLIM_CONTENT_TYPE_ID).

Choosing the right type

Bucket typeContent type
ReplacesEntire bucket UIIndividual file/folder views
Scopebucketupload, folder, or virtual-file
Deploy targetUPLIM_BUCKET_TYPE_SLUGUPLIM_CONTENT_TYPE_ID
ExampleCustom project dashboardMarkdown editor, code viewer

Entrypoints

Every renderer must provide two HTML entrypoints, each serving a different context:

Dashboard entrypoint (dashboard.html)

Loaded when the renderer is displayed inside the authenticated upl.im dashboard. This is where the bucket owner (and collaborators) interact with the renderer. The dashboard entrypoint typically provides the full-featured experience — editing, management, configuration, etc.

Portal entrypoint (portal.html)

Loaded when the renderer is displayed on the public portal. Portal views are for sharing — they may be accessed by unauthenticated users viewing a shared link. The portal entrypoint should typically be a read-only or limited view.

Both entrypoints can render the same component if the experience is identical in both contexts (e.g. a read-only code viewer). Or they can render entirely different UIs — a full editor in dashboard and a clean read-only view in portal.

The SDK tells you which entrypoint is active via the entrypoint field:

const { entrypoint } = useRendererClient();
// entrypoint === 'dashboard' | 'portal'

Scopes

The scope tells your renderer what resource it should display. Always check scope.type to determine what you're rendering.

Scope typeFieldsUse case
bucketbucketIdRender an entire bucket
folderbucketId, folderId, pathRender a folder within a bucket
uploadbucketId, uploadIdRender a single upload/file
virtual-filebucketId, path, folderId?Render a virtual file (not backed by a real upload)

Your renderer doesn't need to support all scope types. If you receive a scope you don't handle, display an appropriate message:

if (scope?.type !== 'upload') {
  return <div>This renderer only supports individual file previews.</div>;
}

Rendering types

The renderingType field indicates how the host is displaying your renderer:

TypeDescription
bucket-rendererFull-page renderer for a bucket
folder-rendererFull-page renderer for a folder
preview-modalDisplayed in a modal overlay

Use this to adjust your layout — for example, showing less chrome in preview-modal mode.

Virtual folders and files

Virtual files are an abstraction that lets integrations expose content that isn't backed by a traditional file upload. Instead of uploading a file to a bucket, the integration manages its own data and presents it as a virtual file within the bucket's folder structure.

A folder can be marked as a virtual file — meaning it appears as a single file in the UI (with a display name), but is actually a folder containing integration-managed content underneath. This enables integrations to maintain rich internal structures (e.g. a note with metadata, attachments, and revision history) while presenting a clean file-like interface to the user.

When your renderer handles a virtual-file scope, you receive a path that you can resolve against your integration's own data model:

const { scope } = useRendererClient();

if (scope?.type === 'virtual-file') {
  // Resolve scope.path against your integration's data
  const note = await resolveNotePath(scope.path);
  return <Editor noteId={note.id} />;
}

Virtual files are particularly useful for integrations that:

  • Manage structured content (notes, documents, spreadsheets)
  • Need internal folder hierarchies invisible to the end user
  • Want to present application-like content as files within a bucket

Permissions

Permissions are declared in config.json and control what actions your renderer can request from the host.

PermissionDescription
openUploadAllows calling openUpload(uploadId) to ask the host to open a file
createFolderAllows the renderer to create folders

Only request permissions your renderer actually needs — the host may present these to the user.

Theme support

Your renderer receives the current theme ('light' or 'dark') on init and whenever the user toggles themes. The SDK hook automatically sets document.documentElement.dataset.theme, which you can target in CSS:

[data-theme="dark"] {
  --bg: #1a1a1a;
  --text: #e0e0e0;
}

[data-theme="light"] {
  --bg: #ffffff;
  --text: #1a1a1a;
}

On this page