Xeonr Developer Docs
Custom Renderers

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-dom

2. Create config.json

This manifest declares your renderer's requirements:

{
  "version": 1,
  "permissions": [],
  "sandbox": {
    "allowPopups": false,
    "allowForms": false
  }
}
FieldDescription
versionAlways 1
permissionsArray of "createFolder" and/or "openUpload"
sandbox.allowPopupsAllow window.open() / target="_blank" links
sandbox.allowFormsAllow 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 component

Tips

  • Start simple. Get a basic renderer working with the dev harness before adding complexity.
  • Handle loading states. The connected flag is false until the host sends uplim:init. Always show a loading state until connected.
  • Use the API adapter. Don't manage tokens manually — apiAdapter handles 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: transparent on <body> so the host's background shows through during loading.

On this page