Skip to content

Integration guide

Database to live runner

An end-to-end walkthrough for running snippets that live outside your build - wherever they come from and whoever writes them.

Architecture#

Only three things have to be true. Everything else is your application's existing shape:

Data flow
1. STORE
the snippet source lives somewhere you already control
(database row, CMS field, file in your repo)
│ 2. SERVE
│ your app hands it to the client
│ as a plain string
<LiveProvider code={source}>
│ 3. RUN
│ compiled in the browser
a live React component
Nothing compiles on the server. The snippet travels as a string and becomes a component in the browser.
  1. 1

    Decide your SDK surface first

    Pick stable namespaces before you write any code. These become a public contract with your authors, renaming @app/store later breaks every stored snippet that imported it.

    The contract
    @app/store application state (hooks + actions)
    @app/ui buttons, cards, layout primitives
    @app/format dates, currency, numbers
    @app/data fetch helpers

    Keep the surface small and boring. Every export is something you have to keep working.

  2. 2

    Build the registry from small files

    One file per domain, composed in an index.ts. This beats a literal inlined on LiveProvider: the groups are testable, and createRegistry warns in development when two of them claim the same key.

    lib/live-sdk/index.ts
    import { createRegistry } from 'next-live';
    import { formatModules } from './format-modules';
    import { storeModules } from './store-modules';
    import { uiModules } from './ui-modules';
    import { vendorModules } from './vendor';
    export const liveModules = createRegistry(
    vendorModules,
    uiModules,
    formatModules,
    storeModules,
    );

    Every entry is defineLoader, so a snippet that never imports @app/ui does not pull it into the page.

  3. 3

    Store snippets as strings

    A snippet is TSX source text, not a file path. You can author .tsx files in your repo and read them server-side, but LiveProvider always receives a string.

    Whatever your storage looks like, the snippet is text
    {
    id: 'dashboard-widget',
    name: 'Revenue widget',
    source: "import { Card } from '@app/ui';\n\nexport default function App() { … }",
    }
    A database row, a CMS entry, or a .tsx file you read at build time, next-live only sees the string.
  4. 4

    Serve it behind whatever auth you already have

    If the source is already in your bundle or read at build time, skip this step entirely; there is nothing to fetch. If you serve it over HTTP, the route is an ordinary authenticated endpoint:

    app/api/apps/[id]/route.ts
    export async function GET(request: Request, { params }) {
    const session = await auth(request);
    if (!session) {
    return Response.json({ error: 'unauthorized' }, { status: 401 });
    }
    const app = await db.apps.find(params.id);
    if (!app) return Response.json({ error: 'not found' }, { status: 404 });
    return Response.json({ id: app.id, source: app.source });
    }
  5. 5

    Render it, and persist edits

    onCodeChange fires only for edits made inside the runner, never when the code prop changes from outside. That is what makes it safe to save from: it cannot echo your own writes back at you in a loop.

    app/apps/[id]/AppRunner.tsx
    'use client';
    import { LiveProvider, LivePreview, LiveError } from 'next-live';
    import { LiveEditor } from 'next-live/editor';
    import { liveModules } from '@/lib/live-sdk';
    export function AppRunner({ id, source, user, canEdit }) {
    return (
    <LiveProvider
    code={source}
    modules={liveModules}
    props={{ user }}
    filePath={`app-${id}.tsx`}
    onCodeChange={
    canEdit ? (next) => saveDraft(id, next) : undefined
    }
    fallback={<Skeleton className="h-80 w-full" />}
    >
    {canEdit && <LiveEditor />}
    <LivePreview />
    <LiveError />
    </LiveProvider>
    );
    }
    filePath gives each app a stable identity, so stack traces and DevTools name the right thing.
  6. 6

    Add CSP and CI validation

    Scope unsafe-eval to the runner routes only, and run validateSnippets over every stored app in CI so an SDK rename fails your build instead of your users' pages.

    A CI gate
    import { validateSnippets } from 'next-live/server';
    const failures = validateSnippets(await db.apps.all(), {
    modules: LIVE_MODULE_KEYS,
    forbidRemoteImports: true,
    });
    if (failures.length > 0) {
    console.error(failures);
    process.exit(1);
    }

    Full detail in Security and Validating in CI.

See it running#

The Apps shell demo in this repo implements exactly this pattern: sidebar tabs, API-fetched snippets, shadcn components through @app/ui, and a store shared with the host page through @app/store.