# Host AI-Generated Apps

Your app builder posts a prompt to the /deploy/:name route on your server. The route asks an LLM to write the app's files, then publishes them as that project's Dynamic App with deployApp. If the build fails, deployApp returns the build errors, and the route sends them back to the LLM to fix the files before deploying again. Later a visitor opens the app's URL, and appsRouter on your server passes the request to the fetch handler of that project's Dynamic App. If the generated app defines Rivet Actors, it calls actions on them, and each one keeps its data in its own SQLite database. The numbers show the order of the calls. There is one Dynamic App per user project.

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.

> **NOTE:** Dynamic Apps is in preview and its API is subject to change.

### Install

Use Node.js 22:

```sh
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`:

```sh
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](/dynamic-apps/docs/connect).

### Limit what the model can write

The model starts from a small seed app and may only change a fixed list of files:

server.ts:

```ts
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;
}
```

fixtures/app/src/index.ts:

```ts
import { Hono } from "hono";

const app = new Hono();

app.get("/", (context) =>
	context.json({
		message: "Replace this seed with the generated application.",
	}),
);

export default app;
```

- `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.

> **WARNING:** 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:

server.ts:

```ts
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](/dynamic-apps/docs/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:

server.ts:

```ts
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](/dynamic-apps/docs/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:

server.ts:

```ts
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](/dynamic-apps/docs/routing).
- `/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.

> **NOTE:** 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:

```sh
npx tsx src/server.ts
```

Ask for an app:

```sh
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](/dynamic-apps/docs/sqlite): connect to a generated app's Actors from your own code.
- [Multiplayer](/dynamic-apps/docs/multiplayer): build apps where many users share live state.
- [Workflows](/dynamic-apps/docs/workflows): run long, multi-step jobs inside a generated app.
- [Frontends & Static Sites](/dynamic-apps/docs/static-websites): add `public/index.html` to `editablePaths` and the seed so the model can write a frontend.
- [Authentication](/dynamic-apps/docs/authentication): add middleware that checks who can open each app.
- [Collecting Logs](/dynamic-apps/docs/logging): see build and runtime logs from generated apps.

## Related docs

- [Generate](/dynamic-apps/docs/generate): the skills and the file schema.
- [Deploy](/dynamic-apps/docs/deploy): bundle requirements and the repair loop.
- [Route](/dynamic-apps/docs/routing): how `appsRouter` maps paths to apps.
- [Architecture](/dynamic-apps/docs/architecture): how builds and requests run in agentOS VMs.
