Custom Renderers
Build sandboxed applications that control how files and folders are displayed inside upl.im.
Custom renderers let you control how files and folders are displayed inside upl.im. A renderer is a sandboxed React application that runs inside an iframe and communicates with the host via a standardised message protocol. You build it with Vite, package it as a .zip archive, and deploy it to your integration.
Architecture
┌──────────────────────────────────────────────┐
│ upl.im host (dashboard or portal) │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ iframe (sandboxed) │ │
│ │ │ │
│ │ Your renderer application │ │
│ │ @xeonr/renderer-sdk │ │
│ │ │ │
│ └──────────────────────────────────────┘ │
│ │
│ Communication: window.postMessage │
│ Protocol prefix: uplim:* │
└──────────────────────────────────────────────┘The host embeds your renderer in a sandboxed iframe served via the renderer proxy ({archiveUploadId}.renderer.upl.im). All communication happens through postMessage using the uplim: protocol — the SDK handles this for you.
Quick start
1. Create the project
mkdir my-renderer && cd my-renderer
pnpm init
pnpm add react react-dom @xeonr/renderer-sdk
pnpm add -D vite @vitejs/plugin-react @xeonr/renderer-vite-plugin typescript @types/react @types/react-dom2. Create config.json
This manifest declares your renderer's requirements:
{
"version": 1,
"permissions": [],
"sandbox": {
"allowPopups": false,
"allowForms": false
}
}| Field | Description |
|---|---|
version | Always 1 |
permissions | Array of "createFolder" and/or "openUpload" |
sandbox.allowPopups | Allow window.open() / target="_blank" links |
sandbox.allowForms | Allow HTML form submission |
3. Create HTML entrypoints
Every renderer requires two HTML entrypoints. See Entrypoints for details on when each is used.
dashboard.html and portal.html both follow the same structure:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Renderer</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./src/dashboard.tsx"></script>
<style>body { background: transparent; }</style>
</body>
</html>4. Create the entrypoint scripts
src/dashboard.tsx:
import { createRoot } from 'react-dom/client';
import { MyRenderer } from './MyRenderer';
createRoot(document.getElementById('root')!).render(<MyRenderer />);src/portal.tsx: Same structure, potentially with a different root component for read-only display.
5. Create your renderer component
import { useRendererClient } from '@xeonr/renderer-sdk/react';
export function MyRenderer() {
const {
connected,
scope,
theme,
renderingType,
entrypoint,
apiAdapter,
openUpload,
close,
} = useRendererClient();
if (!connected) {
return <div>Loading...</div>;
}
// Render based on scope type
switch (scope?.type) {
case 'upload':
return <UploadView uploadId={scope.uploadId} bucketId={scope.bucketId} />;
case 'folder':
return <FolderView folderId={scope.folderId} path={scope.path} />;
case 'bucket':
return <BucketView bucketId={scope.bucketId} />;
case 'virtual-file':
return <VirtualFileView path={scope.path} />;
default:
return <div>Unsupported scope</div>;
}
}6. Configure Vite
vite.config.ts:
import { defineConfig } from 'vite';
import { resolve } from 'path';
import react from '@vitejs/plugin-react';
import { rendererArchive } from '@xeonr/renderer-vite-plugin';
export default defineConfig({
root: resolve(__dirname),
plugins: [
react(),
rendererArchive(),
],
build: {
rollupOptions: {
input: {
dashboard: resolve(__dirname, 'dashboard.html'),
portal: resolve(__dirname, 'portal.html'),
},
},
},
});7. Add scripts to package.json
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
}Project structure
A minimal renderer project looks like this:
my-renderer/
├── config.json # Renderer manifest
├── dashboard.html # Dashboard entrypoint
├── portal.html # Portal entrypoint
├── package.json
├── vite.config.ts
├── tsconfig.json
└── src/
├── dashboard.tsx # Dashboard bootstrap
├── portal.tsx # Portal bootstrap
└── MyRenderer.tsx # Your renderer componentTips
- Start simple. Get a basic renderer working with the dev harness before adding complexity.
- Handle loading states. The
connectedflag isfalseuntil the host sendsuplim:init. Always show a loading state until connected. - Use the API adapter. Don't manage tokens manually —
apiAdapterhandles authentication and refresh automatically. - Keep archives small. The archive is downloaded by every user viewing your renderer. Tree-shake aggressively and consider lazy-loading heavy dependencies.
- Test both entrypoints. Dashboard and portal may be rendered in different contexts — make sure both work.
- Respect the theme. Users expect consistent theming. Always sync your UI with the provided theme.
- Set
background: transparenton<body>so the host's background shows through during loading.