Host AI-Generated Apps
Deploy each app that your app builder generates as its own live app, with its own URL, backend, and data.
Your app builder turns each user’s prompt into a running app, and your own server builds, deploys, and serves it.
What we’ll build:
- A
/deploy/:nameroute that turns a user’s prompt into an app’s files and deploys them. - A build and repair loop that sends build errors back to the model before anything goes live.
- One Dynamic App per user project, served from your own server at its own path.
- App Actors that give each project its own backend state and SQLite database.
Dynamic Apps is in preview and its API is subject to change.
Install
Use Node.js 22:
npm add @rivet-dev/dynamic-apps @ai-sdk/anthropic ai zod hono @hono/node-server
npm add --save-dev tsx
npm pkg set type=module
Configure
Set your model key. AI_MODEL is optional and defaults to claude-sonnet-4-5:
ANTHROPIC_API_KEY=sk-ant-...
AI_MODEL=claude-sonnet-4-5
In local development, Dynamic Apps connects to the local Rivet Engine, so there is nothing else to set. To deploy to Rivet Cloud, also set RIVET_CLOUD_TOKEN. See Connect to Rivet.
Limit what the model can write
The model starts from a small seed app and may only change a fixed list of files:
const editablePaths = [
"package.json",
"tsconfig.json",
"src/index.ts",
] as const;
const maxRepairs = 3;
const maxFileBytes = 64 * 1024;
async function loadSeed(): Promise<Record<string, string>> {
const files: Record<string, string> = {};
for (const path of editablePaths) {
files[path] = await readFile(
new URL(`../fixtures/app/${path}`, import.meta.url),
"utf8",
);
}
return files;
}
const filesSchema = z.object({ files: z.record(z.string(), z.string()) });
function validateFiles(value: Record<string, string>): Record<string, string> {
const files: Record<string, string> = {};
for (const path of editablePaths) {
const content = value[path];
if (typeof content !== "string") {
throw new TypeError(`model response is missing ${path}`);
}
if (Buffer.byteLength(content) > maxFileBytes) {
throw new RangeError(`${path} exceeds ${maxFileBytes} bytes`);
}
files[path] = content;
}
return files;
}
loadSeedreads the seed fromfixtures/app/. It’s a Hono app that default-exports its fetch handler, which is the layoutdeployAppexpects. Starting from files that already build gives the model less to get wrong.validateFileskeeps only the paths ineditablePathsand rejects any file over 64 KiB. Anything else the model returns is dropped, so it can’t add a lockfile, a config file, or a file outside the project.maxRepairscaps how many times a failed build goes back to the model. It’s used in the deploy step.
Generated code is untrusted. Each app builds and runs in its own agentOS VM, but your server decides what the model may write and who may call the route that deploys it. Put authentication and rate limits in front of /deploy/:name before you expose it.
Generate the files
revise asks the model for the complete file tree. It writes the first version and fixes each failed build:
async function revise(
prompt: string,
files: Record<string, string>,
diagnostics?: string,
): Promise<Record<string, string>> {
const result = await generateObject({
model: anthropic(process.env.AI_MODEL ?? "claude-sonnet-4-5"),
maxOutputTokens: 8_000,
schema: filesSchema,
prompt: [
// These skills tell the model how to structure, build, and serve the generated code.
webServerSkill,
rivetActorsSkill,
`You may edit only: ${editablePaths.join(", ")}.`,
`User request: ${prompt}`,
diagnostics ? `Previous build diagnostics:\n${diagnostics}` : "",
`Current files:\n${JSON.stringify(files)}`,
]
.filter(Boolean)
.join("\n\n"),
});
return validateFiles(result.object.files);
}
generateObjectwithfilesSchemamakes the model return a map from path to file contents, so there is no code to cut out of a chat reply.webServerSkillandrivetActorsSkillcome from@rivet-dev/dynamic-apps. They give the model a starter project that builds and the rules for serving it, such as exporting a fetch handler instead of opening a port. WithrivetActorsSkillthe model can add Rivet Actors for state that lasts. See Generate.- The prompt always includes the current files. On a repair it also includes the diagnostics from the failed build, so the model edits what it wrote instead of starting over.
Deploy and repair build errors
generateApp deploys the files and loops until a build succeeds:
async function generateApp(appId: string, prompt: string) {
let files = await revise(prompt, await loadSeed());
for (let attempt = 0; attempt <= maxRepairs; attempt += 1) {
try {
return await deployApp({
appId,
files,
});
} catch (error) {
const appsError =
typeof error === "object" &&
error !== null &&
"code" in error &&
typeof error.code === "string" &&
error.code.startsWith("dynamic_apps_");
if (!appsError || attempt === maxRepairs) {
throw error;
}
const details = error as {
code: string;
message?: string;
metadata?: unknown;
};
const diagnostics = JSON.stringify({
code: details.code,
message: details.message ?? String(error),
metadata: details.metadata,
}).slice(0, 16 * 1024);
files = await revise(prompt, files, diagnostics);
}
}
throw new Error("unreachable");
}
deployAppinstalls dependencies and runsnpm run buildin a sandboxed build VM. When the build fails, it rejects with an error whosecodestarts withdynamic_apps_and whosemetadataholds the compiler output.- Only errors with a
dynamic_apps_code go back to the model, such as a failed build or a missing entrypoint. Any other error is thrown right away. AftermaxRepairsfailed attempts the last error is thrown too. - The diagnostics are capped at 16 KiB so a long compiler log doesn’t fill the prompt.
- A failed build never replaces the active release. Visitors keep the last working version until a new one builds. See Deploy.
appIdis the project name from the URL. Deploying the same name again replaces that project’s app, and each name gets its own Rivet namespace for its Actors and data.
Route requests
The server has a route that deploys apps and a router that serves them:
const server = new Hono();
server.all("/api/rivet/*", (c) => appsRouter.fetch(c.req.raw));
// An agent or any other part of the system can call this route. A generic
// deployment endpoint could accept multipart files; this example generates the
// files from a prompt instead.
server.post("/deploy/:name", async (context) => {
const body = await context.req.json<{ prompt?: unknown }>();
if (typeof body.prompt !== "string" || body.prompt.length > 4_000) {
return context.json(
{ error: "prompt must be at most 4,000 characters" },
400,
);
}
return context.json(
await generateApp(context.req.param("name"), body.prompt),
);
});
server.route("/apps", appsRouter);
POST /deploy/:namechecks that the prompt is a string of at most 4,000 characters, then returns whatdeployAppreturns: the app’s release, endpoint, namespace, pool, and a token when one is needed.server.route("/apps", appsRouter)serves every app under its own path. A request to/apps/team-board/api/itemsreaches theteam-boardapp’s fetch handler as/api/items. See Route./api/rivet/*is a private callback that lets Rivet reach your server to manage deployments. Forward it toappsRouteras shown.- Requests run in a cached agentOS VM inside the same process, so there is no extra network hop between your server and the app.
This example serves apps by path, such as /apps/:name/. This guide does not cover custom domains or a subdomain per app.
Try it
Start the server:
npx tsx src/server.ts
Ask for an app:
curl -X POST http://localhost:3000/deploy/team-board \
-H "content-type: application/json" \
-d '{"prompt":"Build a team board where people post ideas and vote on them."}'
The request returns after the app is built and live. Then open http://localhost:3000/apps/team-board/. Send another prompt to the same name to replace it with a new version.
Next steps
- SQLite: connect to a generated app’s Actors from your own code.
- Multiplayer: build apps where many users share live state.
- Workflows: run long, multi-step jobs inside a generated app.
- Frontends & Static Sites: add
public/index.htmltoeditablePathsand the seed so the model can write a frontend. - Authentication: add middleware that checks who can open each app.
- Collecting Logs: see build and runtime logs from generated apps.
Related docs
- Generate: the skills and the file schema.
- Deploy: bundle requirements and the repair loop.
- Route: how
appsRoutermaps paths to apps. - Architecture: how builds and requests run in agentOS VMs.