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 type | Content type | |
|---|---|---|
| Replaces | Entire bucket UI | Individual file/folder views |
| Scope | bucket | upload, folder, or virtual-file |
| Deploy target | UPLIM_BUCKET_TYPE_SLUG | UPLIM_CONTENT_TYPE_ID |
| Example | Custom project dashboard | Markdown 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 type | Fields | Use case |
|---|---|---|
bucket | bucketId | Render an entire bucket |
folder | bucketId, folderId, path | Render a folder within a bucket |
upload | bucketId, uploadId | Render a single upload/file |
virtual-file | bucketId, 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:
| Type | Description |
|---|---|
bucket-renderer | Full-page renderer for a bucket |
folder-renderer | Full-page renderer for a folder |
preview-modal | Displayed 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.
| Permission | Description |
|---|---|
openUpload | Allows calling openUpload(uploadId) to ask the host to open a file |
createFolder | Allows 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;
}