# Quickstart (Core)

## When to use each

Use Core when your application owns release storage and deployment lifecycle.
For managed storage and invalidation, use the standard [Dynamic Apps
Quickstart](/dynamic-apps/docs/quickstart).

| | Core | Rivet-backed package |
|-|---|---|
| Package | `@rivet-dev/dynamic-apps-core` | `@rivet-dev/dynamic-apps` |
| Build artifact storage | Provide upload and download handlers | Stored automatically |
| Cache invalidation after updates | Manual notification with `watchActiveRelease` | Handled automatically |
| Rivet namespace per app | Bring your own integration | Created and connected automatically |
| Regions and scaling | Managed by your host | Managed by Rivet |
| Lifecycle | Explicit `dispose()` | Managed by the package |
| Best for | Custom infrastructure | Batteries included and scalable |

## Quickstart

### Install

Use Node.js 22 or newer:

```sh
npm add @rivet-dev/dynamic-apps-core @rivet-dev/dynamic-apps @hono/node-server hono
npm add --save-dev tsx
```

### Create the instance

Create the Dynamic Apps instance and provide storage hooks. This example uses
in-memory maps to keep the setup small:

src/server.ts:

```ts
import { serve } from "@hono/node-server";
import {
	type ActiveRelease,
	createDynamicApps,
} from "@rivet-dev/dynamic-apps-core";
import { Hono } from "hono";

// Development only: releases disappear on restart and updates cannot reach
// another process. Use durable storage and cross-process invalidation in production.
const active = new Map<string, ActiveRelease>();
const listeners = new Map<string, Set<() => void>>();

const dynamicApps = createDynamicApps({
	async publishRelease(input) {
		const release: ActiveRelease = {
			appId: input.appId,
			release: input.buildId,
			artifact: {
				...input.artifact,
				bytes: new Uint8Array(input.artifact.bytes),
			},
			maxRequestBytes: 1024 * 1024,
			maxResponseBytes: 4 * 1024 * 1024,
		};
		// The complete artifact is stored before this single active-map update.
		active.set(input.appId, release);
		for (const invalidate of listeners.get(input.appId) ?? []) invalidate();
		return { appId: input.appId, release: release.release };
	},
	async loadActiveRelease(appId) {
		const release = active.get(appId);
		return release
			? {
					...release,
					artifact: {
						...release.artifact,
						bytes: new Uint8Array(release.artifact.bytes),
					},
				}
			: undefined;
	},
	async watchActiveRelease(appId, invalidate) {
		const appListeners = listeners.get(appId) ?? new Set();
		appListeners.add(invalidate);
		listeners.set(appId, appListeners);
		return () => {
			appListeners.delete(invalidate);
			if (appListeners.size === 0) listeners.delete(appId);
		};
	},
});
```

> **NOTE:** This in-memory store is **development-only**. It loses releases on restart and cannot invalidate another process. Use durable storage and cross-process notifications in production.

### Set up the router

Mount `appsRouter` wherever generated apps should be served, then start the
HTTP server:

src/server.ts:

```ts
const app = new Hono();
app.route("/apps", dynamicApps.appsRouter);

const port = Number(process.env.PORT ?? 3000);
serve({ fetch: app.fetch, port });
console.log(`Dynamic Apps Core listening on http://localhost:${port}`);
```

### Generate and deploy

An LLM writes the app as a set of files. Pass the [skills](/dynamic-apps/docs/generate)
from `@rivet-dev/dynamic-apps` as the system prompt so the model knows the
supported project layout:

```ts
import { anthropic } from "@ai-sdk/anthropic";
import { rivetActorsSkill, webServerSkill } from "@rivet-dev/dynamic-apps";
import { generateObject } from "ai";
import { z } from "zod";

const { object } = await generateObject({
  model: anthropic("claude-sonnet-5"),
  // Skills teach the model the supported project layout.
  system: [webServerSkill, rivetActorsSkill].join("

"),
  schema: z.object({ files: z.record(z.string(), z.string()) }),
  prompt: "Build a team board.",
});

const { files } = object;
```

Pass the generated files to `deployApp()`:

```ts
await dynamicApps.deployApp({
  appId: "team-board",
  files,
});
```

### Visit the app

Run the complete example and make a request:

```sh
npm start
curl http://localhost:3000/apps/team-board/
```

Core builds the app, publishes the release through your hooks, and loads it on
the first request. Warm requests reuse the cached agentOS VM. Call
`await dynamicApps.dispose()` when your host shuts down.

Read [Core](/dynamic-apps/docs/core) before running it across multiple
processes.
