Skip to main content
AI-Generated Apps

Host AI-Generated Apps

Deploy each app that your app builder generates as its own live app, with its own URL, backend, and data.

See the full example on GitHub. View on GitHub
Your serverApp builderyour UI or agentVisitoropens the app/deploy/:nameappsRouterLLMwrites the filesDynamic Appgenerated codeApp Actorsown SQLite eachprompt1write files2deployApp3build errors4app URL5fetch6actions7One per user projectRivet ActorDynamic AppCallReplyOptional

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/:name route 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:

  • loadSeed reads the seed from fixtures/app/. It’s a Hono app that default-exports its fetch handler, which is the layout deployApp expects. Starting from files that already build gives the model less to get wrong.
  • validateFiles keeps only the paths in editablePaths and 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.
  • maxRepairs caps 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);
}
  • generateObject with filesSchema makes the model return a map from path to file contents, so there is no code to cut out of a chat reply.
  • webServerSkill and rivetActorsSkill come 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. With rivetActorsSkill the 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");
}
  • deployApp installs dependencies and runs npm run build in a sandboxed build VM. When the build fails, it rejects with an error whose code starts with dynamic_apps_ and whose metadata holds 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. After maxRepairs failed 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.
  • appId is 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/:name checks that the prompt is a string of at most 4,000 characters, then returns what deployApp returns: 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/items reaches the team-board app’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 to appsRouter as 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.html to editablePaths and 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.
  • Generate: the skills and the file schema.
  • Deploy: bundle requirements and the repair loop.
  • Route: how appsRouter maps paths to apps.
  • Architecture: how builds and requests run in agentOS VMs.