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
| 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 through Rivet deployment options |
| Lifecycle | Explicit dispose() | Managed by the package |
| Best for | Custom infrastructure | Batteries 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.