Skip to main content
General

Core Quickstart

Build and serve a Dynamic App with a development-only in-memory release store.

View the complete Core Quick Start example on GitHub.

Use Core when your application owns release storage and deployment lifecycle. Use the standard Dynamic Apps Quick Start when you want Rivet to provide those pieces.

Choosing between Core and the Rivet-backed package

CoreRivet-backed package
Package@rivet-dev/dynamic-apps-core@rivet-dev/dynamic-apps
Build artifact storageProvide upload and download handlersStored automatically
Cache invalidation after updatesManual notification with watchActiveReleaseHandled automatically
Rivet namespace per appBring your own integrationCreated and connected automatically
Regions and scalingManaged by your hostManaged through Rivet deployment options
LifecycleExplicit dispose()Managed by the package
Best forCustom infrastructureBatteries included and scalable

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.

Install

Use Node.js 22 or newer:

npm add @rivet-dev/dynamic-apps-core @hono/node-server hono
npm add --save-dev tsx
npm pkg set type=module

Create the host and release store

The example keeps a Map of active releases and a Map of update listeners. Its hooks atomically publish a complete copied artifact, load a copied active release, and register an update watcher that returns an unsubscribe function. It then mounts the router, deploys a complete generated two-file app, and disposes the instance during shutdown.

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),
			},
			regions: input.regions ?? ["local"],
			scaling: {
				minReplicas: input.scaling?.minReplicas ?? 0,
				maxReplicas: input.scaling?.maxReplicas ?? 1,
				targetConcurrency: input.scaling?.targetConcurrency ?? 8,
			},
			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,
					regions: [...release.regions],
					scaling: { ...release.scaling },
					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);
		};
	},
});

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

let server: ReturnType<typeof serve> | undefined;
let shuttingDown = false;
const shutdown = async () => {
	if (shuttingDown) return;
	shuttingDown = true;
	await dynamicApps.dispose();
	server?.close();
};
process.once("SIGINT", () => void shutdown());
process.once("SIGTERM", () => void shutdown());

await dynamicApps.deployApp({
	appId: "hello",
	files: {
		"package.json": JSON.stringify({
			private: true,
			type: "module",
			main: "index.js",
		}),
		"index.js": `
			export default {
				fetch() {
					return new Response("Hello from Dynamic Apps Core!");
				},
			};
		`,
	},
});

const hostIndex = process.argv.indexOf("--host");
const hostname = hostIndex >= 0 ? process.argv[hostIndex + 1] : "127.0.0.1";
if (!hostname) throw new Error("--host requires a value");
const port = Number(process.env.PORT ?? 3000);
server = serve({ fetch: app.fetch, hostname, port });
console.log(`Dynamic Apps Core listening on http://${hostname}:${port}`);

Run the server

Pass the listening host on the command line:

npx tsx src/server.ts --host 0.0.0.0

Call the generated app

curl http://localhost:3000/apps/hello/
# Hello from Dynamic Apps Core!

The request lifecycle is compact:

agentOS build -> publishRelease
first request -> watchActiveRelease + loadActiveRelease
warm request  -> cached agentOS VM (zero hooks)

Continue with Custom Storage for durable, multi-process hooks. Use the ordinary Quick Start for the batteries-included Rivet actor-backed package.