diff --git a/benchmarks/dynamic-apps/package.json b/benchmarks/dynamic-apps/package.json
index fabb6c168..a62a7499c 100644
--- a/benchmarks/dynamic-apps/package.json
+++ b/benchmarks/dynamic-apps/package.json
@@ -15,12 +15,12 @@
"test": "npx tsx --test src/load.test.ts"
},
"dependencies": {
- "@hono/node-server": "^2.0.11",
+ "@hono/node-server": "^2.1.1",
"@rivet-dev/agentos-core": "0.2.18",
"@rivet-dev/agentos-toolchain": "0.2.18",
"@rivet-dev/dynamic-apps": "workspace:*",
"@rivet-dev/dynamic-apps-core": "workspace:*",
- "hono": "^4.12.9",
+ "hono": "^4.13.5",
"rivetkit": "2.3.11"
},
"devDependencies": {
diff --git a/benchmarks/dynamic-apps/src/fixture.ts b/benchmarks/dynamic-apps/src/fixture.ts
index 5a0c42b7e..3b2d61dc0 100644
--- a/benchmarks/dynamic-apps/src/fixture.ts
+++ b/benchmarks/dynamic-apps/src/fixture.ts
@@ -10,7 +10,6 @@ export type BenchmarkDeploymentClient = NonNullable<
export async function deployBenchmarkFixture(
client?: BenchmarkDeploymentClient,
) {
- const region = process.env.BENCH_REGION;
return deployApp(
{
appId: BENCHMARK_APP_ID,
@@ -42,12 +41,6 @@ export default {
};
`,
},
- scaling: {
- minReplicas: 1,
- maxReplicas: 1,
- targetConcurrency: 128,
- },
- ...(region ? { regions: [region] } : {}),
},
client ? { client } : undefined,
);
@@ -56,7 +49,6 @@ export default {
export async function deployActorBenchmarkFixture(
client?: BenchmarkDeploymentClient,
) {
- const region = process.env.BENCH_REGION;
return deployApp(
{
appId: ACTOR_BENCHMARK_APP_ID,
@@ -68,7 +60,7 @@ export async function deployActorBenchmarkFixture(
type: "module",
main: "index.js",
dependencies: {
- hono: "4.13.3",
+ hono: "4.13.5",
rivetkit: "2.3.11",
},
}),
@@ -115,12 +107,6 @@ app.all("*", () => Response.json({ ok: true, workload: "actor-and-direct-http" }
export default app;
`,
},
- scaling: {
- minReplicas: 0,
- maxReplicas: 16,
- targetConcurrency: 8,
- },
- ...(region ? { regions: [region] } : {}),
},
client ? { client } : undefined,
);
diff --git a/benchmarks/dynamic-apps/src/runtime-stress.ts b/benchmarks/dynamic-apps/src/runtime-stress.ts
index 05c691d3d..cf4602d0c 100644
--- a/benchmarks/dynamic-apps/src/runtime-stress.ts
+++ b/benchmarks/dynamic-apps/src/runtime-stress.ts
@@ -99,8 +99,6 @@ class FakeStatePlane {
byteLength: state.artifact.bytes.byteLength,
usesRivetKit: false,
},
- regions: ["local"],
- scaling: { minReplicas: 0, maxReplicas: 1, targetConcurrency: 32 },
maxRequestBytes: 1024 * 1024,
maxResponseBytes: 4 * 1024 * 1024,
};
diff --git a/docs/content/docs/_architecture-diagram.astro b/docs/content/docs/_architecture-diagram.astro
new file mode 100644
index 000000000..9eb0b2295
--- /dev/null
+++ b/docs/content/docs/_architecture-diagram.astro
@@ -0,0 +1,29 @@
+
diff --git a/docs/content/docs/architecture.mdx b/docs/content/docs/architecture.mdx
new file mode 100644
index 000000000..648f0ce1e
--- /dev/null
+++ b/docs/content/docs/architecture.mdx
@@ -0,0 +1,46 @@
+---
+title: "Architecture"
+description: "How Dynamic Apps serves, builds, and stores user-generated applications."
+---
+
+import ArchitectureDiagram from "./_architecture-diagram.astro";
+
+
+**Dynamic Apps is a library, not a hosted app deployment platform.** You own
+the routing server, its authentication, and the URL on which applications are
+mounted.
+
+
+
+The request lifecycle looks like:
+
+1. Your routing server receives a request.
+2. Your middleware handles it: authentication, rate limits, whatever else you run.
+3. The request is passed to `appsRouter`.
+4. `appsRouter` executes it in the agentOS VM for that app, starting the VM if it isn't already running.
+
+
+
+Requests reach your routing server, where `appsRouter` executes them in a
+cached agentOS VM embedded in the same process. There is no network hop to
+another service, and warm requests never touch storage. Apps export fetch
+handlers; Dynamic Apps owns any listener inside the VM.
+
+## Deployment is separate from serving
+
+`deployApp()` builds the files in a sandboxed agentOS build VM and publishes an
+immutable release. The default `@rivet-dev/dynamic-apps` package stores
+releases in Rivet. `@rivet-dev/dynamic-apps-core` lets you
+supply another store.
+
+## App-defined actors
+
+An app may also export a RivetKit registry. Those app-defined actors use normal
+Rivet routing for durable state, actions, events, and connections. Dynamic Apps
+imports the app once into a cached server process, then sends both ordinary HTTP
+and Rivet callbacks through that process.
+
+
+agentOS provides the filesystem, process, environment, and network permission
+boundary for direct requests and app-defined actor workers.
+
diff --git a/docs/content/docs/backends.mdx b/docs/content/docs/backends.mdx
new file mode 100644
index 000000000..069077945
--- /dev/null
+++ b/docs/content/docs/backends.mdx
@@ -0,0 +1,30 @@
+---
+title: "Backends & REST APIs"
+description: "Serve HTTP backends and REST APIs from a Dynamic App."
+---
+
+import ExampleLinkBar from "@/components/docs/ExampleLinkBar.astro";
+
+
+
+An app is a directory with a `package.json` and an entrypoint that
+default-exports a `fetch` handler. Any framework that speaks `fetch` works.
+The app does not bind a port; Dynamic Apps owns its listener. This app serves a
+frontend and a JSON API from the same handler.
+
+## Example generated code
+
+
+
+
+
+
+## Deploy and route
+
+Deploy the directory and every route the app defines is served under
+`/apps/:appId` on your routing server:
+
+
+
+See [Routing](/dynamic-apps/docs/routing) for how requests reach the app and
+[Deploy](/dynamic-apps/docs/deploy) for builds and releases.
diff --git a/docs/content/docs/connect.mdx b/docs/content/docs/connect.mdx
new file mode 100644
index 000000000..5f388902d
--- /dev/null
+++ b/docs/content/docs/connect.mdx
@@ -0,0 +1,42 @@
+---
+title: "Connect to Rivet"
+description: "Every deployed app gets its own Rivet namespace for actors, workflows, and data."
+skill: true
+---
+
+Every deployment gets its own Rivet namespace. That gives each user's app:
+
+- Complete per-tenant isolation
+- Per-user billing
+- Infrastructure that costs nothing when idle
+
+## What namespaces are
+
+A namespace holds an app's Rivet Actors, which power its workflows, SQLite,
+realtime state, queues, and crons. `deployApp()` creates one namespace per app
+and returns the endpoint, namespace, pool, and publishable token your client
+needs to connect.
+
+## Connecting to Rivet
+
+### Local development
+
+No action needed. Dynamic Apps connects to the local Rivet Engine by default.
+
+### Rivet Cloud
+
+Set the `RIVET_CLOUD_TOKEN` environment variable. Issue a token from
+**Settings > Advanced > Cloud Token** in the
+[dashboard](https://dashboard.rivet.dev). `deployApp()` uses it to create each
+app's namespace through the Rivet Cloud API. Keep this token server-side.
+
+### Self-hosting
+
+Use your admin token. Nothing else is needed. The admin token is the same
+token your backend already uses to connect to Rivet, so namespace creation
+works out of the box.
+
+## Disabling namespace creation
+
+Set `createNamespace: false` on `deployApp()` for HTTP-only apps. Apps that
+use `rivetkit` require a namespace.
diff --git a/docs/content/docs/custom-storage.mdx b/docs/content/docs/core.mdx
similarity index 78%
rename from docs/content/docs/custom-storage.mdx
rename to docs/content/docs/core.mdx
index 347526e13..dd6ce3247 100644
--- a/docs/content/docs/custom-storage.mdx
+++ b/docs/content/docs/core.mdx
@@ -1,6 +1,6 @@
---
-title: "Custom Storage"
-description: "Implement durable release publication, loading, and invalidation for Dynamic Apps Core."
+title: "Core"
+description: "Use Dynamic Apps Core to own release storage, loading, and invalidation."
skill: true
---
@@ -44,3 +44,10 @@ change for the entire lifetime of every serving process.
Call `await dynamicApps.dispose()` during shutdown to release subscriptions,
build resources, cached runtimes, and agentOS contexts.
+
+Deployed app entrypoints only export a Fetch handler; they never bind a port.
+If Core loads an actor-enabled release, provide its `server.environment` and a
+shared `serverRuntime`. The standard `@rivet-dev/dynamic-apps` package supplies
+that actor runtime automatically.
+
+Get started with the [Quickstart (Core)](/dynamic-apps/docs/quickstart-core).
diff --git a/docs/content/docs/customize-vm.mdx b/docs/content/docs/customize-vm.mdx
new file mode 100644
index 000000000..2a627a490
--- /dev/null
+++ b/docs/content/docs/customize-vm.mdx
@@ -0,0 +1,29 @@
+---
+title: "Customize the VM"
+description: "Configure the agentOS VM that builds and serves each app."
+skill: true
+---
+
+With [Core](/dynamic-apps/docs/core), use the `vm` option to configure the
+agentOS VM that serves each app:
+
+```ts
+const dynamicApps = createDynamicApps({
+ // Release hooks omitted.
+ vm: {
+ software: [firstPackage, secondPackage],
+ onAgentStderr: (event) => logger.error(event),
+ onLimitWarning: (warning) => logger.warn(warning),
+ },
+});
+```
+
+Dynamic Apps itself controls where the app's code is placed in the VM and the
+V8 heap limit. `vm` options cannot override them.
+Use the separate `logger` option for application output and build events.
+
+The VM itself is configured through agentOS:
+
+- [Software](/agentos/docs/software): packages available inside the VM
+- [Resource Limits](/agentos/docs/resource-limits): CPU, memory, and disk caps
+- [Permissions](/agentos/docs/permissions): filesystem, process, and network boundaries
diff --git a/docs/content/docs/deploy.mdx b/docs/content/docs/deploy.mdx
index 041ea6ee8..f9988422f 100644
--- a/docs/content/docs/deploy.mdx
+++ b/docs/content/docs/deploy.mdx
@@ -1,9 +1,13 @@
---
-title: "Deploying Apps"
+title: "Deploy"
description: "Deploy a directory or generated files with deployApp(), preserve rollback, and configure app actors."
skill: true
---
+`deployApp()` publishes an app's files as a new release.
+
+## Getting started
+
Deploy a local application directory:
```ts
@@ -35,14 +39,28 @@ await deployApp({
});
```
-The direct entrypoint must default-export a function or an object with
-`fetch(request)`. Code runs inside agentOS with filesystem, process,
-environment, and network permissions, and supported Node builtins are
-available. Directories that contain only static files are rejected; see
-[Static Websites](/dynamic-apps/docs/static-websites). Native addons are not
-supported.
+## Bundle requirements
+
+An app needs two things:
+
+- A `package.json`.
+- An entrypoint that default-exports a `fetch` handler.
+
+Dynamic Apps owns the HTTP listener. Application code must not call `serve()`,
+`listen()`, or `registry.start()`.
+
+With Hono, export the app directly:
+
+```ts
+import { Hono } from "hono";
+
+const app = new Hono();
+app.get("/", (c) => c.json({ ok: true }));
+
+export default app;
+```
-## Build repair and rollback
+## Build and repair loop
`deployApp()` rejects with bounded build diagnostics when generated source does
not compile. Feed those diagnostics back to the generator and try again:
@@ -59,53 +77,16 @@ for (let attempt = 0; attempt < 3; attempt++) {
}
```
-Include `webServerSkill` and `rivetActorsSkill` from `@rivet-dev/dynamic-apps`
-in the model prompt. They describe the supported TypeScript server layout and
-Rivet actor integration. [View the complete AI App Builder example](https://github.com/rivet-dev/dynamic-apps/tree/main/examples/apps-ai-builder).
-
-A failed build or incomplete artifact write never replaces the active release.
-A successful call returns only after the immutable artifact is persisted and
-activated; it does not mean a request-serving replica was warmed.
-
-With core, this is the `publishRelease` guarantee: make the complete artifact
-durable first, atomically replace the active release second, and resolve only
-when `loadActiveRelease` can read it. The default adapter provides those
-semantics through its per-app Rivet actor.
-
-`appId` must contain 1–63 lowercase letters, numbers, or hyphens. Pass exactly
-one of `source` or `files`.
+A failed build never replaces the active release. A successful call resolves
+only after the new release is persisted and activated. Activation is
+all-or-nothing and rolls out instantly: warm VMs load the new release on the
+next request.
## Configuration
-```ts
-await deployApp({
- appId: "my-app",
- source,
- regions: ["atl", "fra"],
- scaling: {
- minReplicas: 0,
- maxReplicas: 128,
- targetConcurrency: 8,
- },
-});
-```
-
| Option | Default | Meaning |
| --- | --- | --- |
-| `regions` | State actor's current region | Stored compatibility metadata; it does not move direct HTTP execution |
-| `createNamespace` | none | Deprecated compatibility option; every app always receives its own stable namespace |
-| `scaling.minReplicas` | `0` | App-defined actor runner setting; compatibility metadata for direct HTTP |
-| `scaling.maxReplicas` | `128` | App-defined actor runner setting; compatibility metadata for direct HTTP |
-| `scaling.targetConcurrency` | `8` | App-defined actor runner setting; compatibility metadata for direct HTTP |
-
-Every app receives its own Rivet namespace. Locally, configure an Engine secret
-token with permission to create namespaces. On Rivet Cloud, set
-`RIVET_CLOUD_TOKEN` to a `cloud_api_*` token for the project; `deployApp()` uses
-it to create the namespace and namespace-scoped actor credentials. Keep this
-management token server-side. The deployment result includes the app's Engine
-endpoint, namespace, pool, and publishable token for connecting to app-defined
-actors.
-
-Rivet Compute automatically uses the deployment's `.rivet.run/api/rivet`
-callback. For another public host, set `DYNAMIC_APPS_CALLBACK_URL` to its
-origin.
+| `createNamespace` | `true` | Set `false` to disable namespace provisioning entirely. Unset keeps the default. Apps that use `rivetkit` require a namespace |
+
+See [Connect to Rivet](/dynamic-apps/docs/connect) for credentials and
+namespace setup.
diff --git a/docs/content/docs/generate.mdx b/docs/content/docs/generate.mdx
new file mode 100644
index 000000000..4764f28a7
--- /dev/null
+++ b/docs/content/docs/generate.mdx
@@ -0,0 +1,51 @@
+---
+title: "Generate"
+description: "Generate an app's files with a model and the Dynamic Apps skills."
+skill: true
+---
+
+import ExampleLinkBar from "@/components/docs/ExampleLinkBar.astro";
+
+
+
+An LLM writes the app as a set of files.
+
+## Getting started
+
+Use `generateObject` with a schema for the file tree, and pass the skills 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("\n\n"),
+ schema: z.object({ files: z.record(z.string(), z.string()) }),
+ prompt: "Build a team board.",
+});
+
+const { files } = object;
+```
+
+Pass the generated `files` to [`deployApp()`](/dynamic-apps/docs/deploy). If the
+build fails, feed the diagnostics back to the model and deploy again. See
+[Deploy](/dynamic-apps/docs/deploy) for the repair loop.
+
+## Skills
+
+Skills are prompt fragments exported by `@rivet-dev/dynamic-apps`. Each one
+carries the instructions and a complete starter project for one kind of app, so
+generated code compiles and serves on the first try more often.
+
+| Skill | What the model learns |
+| --- | --- |
+| `webServerSkill` | The supported TypeScript Fetch-handler layout: `package.json`, `tsconfig.json`, and a Hono entrypoint with no application-owned listener. |
+| `rivetActorsSkill` | The same layout plus Rivet Actors: durable state, actions, and HTTP routes backed by actors. |
+
+More skills will be added over time.
diff --git a/docs/content/docs/index.mdx b/docs/content/docs/index.mdx
index 872b4bbfc..94a0b071d 100644
--- a/docs/content/docs/index.mdx
+++ b/docs/content/docs/index.mdx
@@ -1,92 +1,28 @@
---
title: "Dynamic Apps"
-description: "Deploy user-generated applications in isolated agentOS VMs with SQLite, workflows, multiplayer, and static sites out of the box."
+description: "Deploy an AI-generated app and backend for each of your users."
skill: true
---
-Dynamic Apps runs user-generated HTTP applications inside your own Node.js
-server. Each app runs in an isolated agentOS VM and can add durable SQLite
-state, workflows, realtime multiplayer, and static sites.
-
Dynamic Apps is in preview and its API is subject to change.
+Dynamic Apps runs user-generated HTTP applications inside your own Node.js
+server. Each app runs in an isolated agentOS VM and can add durable SQLite
+state, workflows, realtime multiplayer, and static sites.
+
- Start the host server, deploy a generated app, and visit it.
-
-
- Deploy files or a directory, repair build errors, and configure apps.
+ Generate a backend, deploy it, and serve it from your router.
-
- Serve an HTML, CSS, and JavaScript site.
+
+ The same flow with your own release storage.
-
- Store durable relational data in an actor-owned database.
-
-
- Share realtime state between every connected client.
+
+ Deploy files or a directory, repair build errors, and configure apps.
-
- Run durable multi-step jobs that sleep and resume.
+
+ How requests reach each deployed app.
-
-## Architecture
-
-**Dynamic Apps is a library, not a hosted app deployment platform.** You own
-the Hono server, its authentication, and the URL on which applications are
-mounted.
-
-Requests reach your Hono server, where `appsRouter` executes them in a cached
-agentOS VM holding the app's active release. Warm requests never touch storage
-or a Rivet actor.
-
-
-
-Deployment is separate from serving. `deployApp()` builds the files in a
-sandboxed agentOS build VM and publishes an immutable release. The default
-`@rivet-dev/dynamic-apps` package stores releases in a per-app Rivet actor;
-`@rivet-dev/dynamic-apps-core` lets you supply another store.
-
-An app may also export a RivetKit registry. Those app-defined actors use normal
-Rivet routing for durable state, actions, events, and connections, while the
-same app's ordinary HTTP handler still runs through the agentOS VM.
-
-
-agentOS provides the filesystem, process, environment, and network permission
-boundary for direct requests. App-defined actor workers share the host process,
-so run one trust domain per container for mutually untrusted tenants.
-
diff --git a/docs/content/docs/logging.mdx b/docs/content/docs/logging.mdx
index e7185785a..4324723a2 100644
--- a/docs/content/docs/logging.mdx
+++ b/docs/content/docs/logging.mdx
@@ -1,5 +1,5 @@
---
-title: "Collecting logs"
+title: "Collecting Logs"
description: "Forward Dynamic Apps output and runtime events to your logging provider."
---
@@ -32,6 +32,6 @@ plus bounded metadata. Messages are limited to 64 KiB and carry
`metadata.truncated: true` when shortened.
Delivery is best effort and happens synchronously. Do not make blocking network
-requests in the callback; enqueue into your logging SDK instead. Request and
-response bodies, authorization headers, environment variables, callback
-secrets, endpoint credentials, and build source are excluded.
+requests in the callback. Enqueue into your logging SDK instead. Request and
+response bodies, authorization headers, environment variables, credentials,
+and build source are excluded.
diff --git a/docs/content/docs/multiplayer.mdx b/docs/content/docs/multiplayer.mdx
index 65a134353..9168fd715 100644
--- a/docs/content/docs/multiplayer.mdx
+++ b/docs/content/docs/multiplayer.mdx
@@ -3,16 +3,22 @@ title: "Multiplayer"
description: "Share realtime state between every client connected to an app."
---
+import ExampleLinkBar from "@/components/docs/ExampleLinkBar.astro";
+
+
+
An actor holds the shared state for one room and broadcasts events to every
-connected client. [View the complete example](https://github.com/rivet-dev/dynamic-apps/tree/main/examples/apps-multiplayer).
+connected client.
+
+## Example generated code
+
+
+
+## Deploy and connect
-The server is the generated app. The client shows how your own system connects
-to the actors inside it.
+Deploy the app, then connect to its actors from your own system:
-
-
-
Dynamic Apps does not wrap RivetKit's action, event, or connection APIs. See
[Events](https://rivet.dev/actors/docs/events/) and
diff --git a/docs/content/docs/quickstart-core.mdx b/docs/content/docs/quickstart-core.mdx
index ab14bdfa0..7829c2bf9 100644
--- a/docs/content/docs/quickstart-core.mdx
+++ b/docs/content/docs/quickstart-core.mdx
@@ -1,16 +1,22 @@
---
-title: "Core Quickstart"
+title: "Quickstart (Core)"
description: "Build and serve a Dynamic App with a development-only in-memory release store."
skill: true
---
-[View the complete Core Quick Start example on GitHub](https://github.com/rivet-dev/dynamic-apps/tree/main/examples/apps-core-quickstart).
+import ExampleLinkBar from "@/components/docs/ExampleLinkBar.astro";
-Use Core when your application owns release storage and deployment lifecycle.
-Use the standard [Dynamic Apps Quick Start](/dynamic-apps/docs/quickstart) when
-you want Rivet to provide those pieces.
+import ArchitectureDiagram from "./_architecture-diagram.astro";
+
+
+
+
-## Choosing between Core and the Rivet-backed package
+## 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 |
|-|---|---|
@@ -22,11 +28,7 @@ you want Rivet to provide those pieces.
| 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.
-
+## Quickstart
@@ -37,52 +39,86 @@ Use Node.js 22 or newer:
```sh
npm add @rivet-dev/dynamic-apps-core @hono/node-server hono
npm add --save-dev tsx
-npm pkg set type=module
```
-
+
+
+Create the Dynamic Apps instance and provide storage hooks. This example uses
+in-memory maps to keep the setup small:
-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.
+
-
+
+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.
+
-
+
-Pass the listening host on the command line:
+Mount `appsRouter` wherever generated apps should be served, then start the
+HTTP server:
-```sh
-npx tsx src/server.ts --host 0.0.0.0
-```
+
-
+
-```sh
-curl http://localhost:3000/apps/hello/
-# Hello from Dynamic Apps Core!
+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,
+});
```
-
+
-The request lifecycle is compact:
+Run the complete example and make a request:
-```text
-agentOS build -> publishRelease
-first request -> watchActiveRelease + loadActiveRelease
-warm request -> cached agentOS VM (zero hooks)
+```sh
+npm start
+curl http://localhost:3000/apps/team-board/
```
-Continue with [Custom Storage](/dynamic-apps/docs/custom-storage) for durable,
-multi-process hooks. Use the ordinary [Quick Start](/dynamic-apps/docs/quickstart)
-for the batteries-included Rivet actor-backed package.
+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.
+
+
+
+
+
+Continue with [Core](/dynamic-apps/docs/core) before using
+Core across multiple processes.
diff --git a/docs/content/docs/quickstart.mdx b/docs/content/docs/quickstart.mdx
index 9b3e92714..b07f5cd4c 100644
--- a/docs/content/docs/quickstart.mdx
+++ b/docs/content/docs/quickstart.mdx
@@ -1,12 +1,19 @@
---
title: "Quickstart"
-description: "Start the host server, deploy a generated app, and serve it through agentOS."
+description: "Generate a backend, deploy it, and serve it through agentOS."
skill: true
---
+import ExampleLinkBar from "@/components/docs/ExampleLinkBar.astro";
+
import { Hosting } from "@/components/docs/Hosting";
+import ArchitectureDiagram from "./_architecture-diagram.astro";
+
+
-[View the complete Quickstart example on GitHub](https://github.com/rivet-dev/dynamic-apps/tree/main/examples/apps-hello-world).
+
+
+## Quickstart
@@ -22,11 +29,12 @@ npm pkg set type=module
-
+
Mount the private Rivet callback separately from application traffic. The
-callback keeps the deployment state actor available; ordinary app requests are
-served by `appsRouter` through a cached agentOS VM.
+callback lets Rivet reach your server to manage deployments. Ordinary app
+requests are served by `appsRouter` through agentOS. Generated app code only
+exports a Fetch handler; it does not create its own listener.
@@ -38,12 +46,46 @@ npx tsx src/server.ts
-
+
+
+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.",
+});
-Pass a complete application tree to `deployApp()`. This can be called by an
-agent, an upload endpoint, or another trusted control-plane process.
+const { files } = object;
+```
+
+[View the complete AI App Builder example](https://github.com/rivet-dev/dynamic-apps/tree/main/examples/apps-ai-builder).
+
+Pass the generated files to `deployApp()`. This can be called by an agent, an
+upload endpoint, or another trusted control-plane process.
+
+```ts
+import { deployApp } from "@rivet-dev/dynamic-apps";
+
+await deployApp({
+ appId: "team-board",
+ files,
+});
+```
-
+Run the deploy script:
```sh
npx tsx src/deploy.ts
@@ -53,8 +95,8 @@ npx tsx src/deploy.ts
-Open `http://localhost:3000/apps/hello-world/`. The bare
-`/apps/hello-world` path redirects to its trailing-slash form.
+Open `http://localhost:3000/apps/team-board/`. The bare
+`/apps/team-board` path redirects to its trailing-slash form.
diff --git a/docs/content/docs/routing.mdx b/docs/content/docs/routing.mdx
index 6bebd4cf2..fec83f82e 100644
--- a/docs/content/docs/routing.mdx
+++ b/docs/content/docs/routing.mdx
@@ -1,11 +1,14 @@
---
-title: "Route requests"
-description: "Mount direct app traffic and the private Rivet callback on a Hono server."
+title: "Route"
+description: "Serve every deployed app from your own server."
skill: true
---
-The package root exports one router. Mount ordinary application traffic at any
-prefix and forward the private `/api/rivet/*` callback to the same router:
+`appsRouter` serves every deployed app at `/:appId/*`.
+
+## Getting started
+
+Mount it at any prefix and forward the private `/api/rivet/*` callback to it:
```ts
import { appsRouter } from "@rivet-dev/dynamic-apps";
@@ -17,8 +20,5 @@ server.all("/api/rivet/*", (c) => appsRouter.fetch(c.req.raw));
server.route("/apps", appsRouter);
```
-This serves `/:appId/*` relative to the mount. For example,
-`/apps/example/api/items?q=1` reaches the deployed handler as
-`/api/items?q=1`. A bare `/apps/example` request redirects to
-`/apps/example/`.
-
+A request to `/apps/example/api/items` reaches that app's handler as
+`/api/items`.
diff --git a/docs/content/docs/sqlite.mdx b/docs/content/docs/sqlite.mdx
index 00b201659..3300bb448 100644
--- a/docs/content/docs/sqlite.mdx
+++ b/docs/content/docs/sqlite.mdx
@@ -3,17 +3,23 @@ title: "SQLite"
description: "Store durable relational data in an actor-owned SQLite database."
---
+import ExampleLinkBar from "@/components/docs/ExampleLinkBar.astro";
+
+
+
Apps that depend on `rivetkit` can define actors. Each actor owns its own
SQLite database, so a generated app gets durable relational data without any
-extra infrastructure. [View the complete example](https://github.com/rivet-dev/dynamic-apps/tree/main/examples/apps-sqlite).
+extra infrastructure.
+
+## Example generated code
+
+
+
+## Deploy and connect
-The server is the generated app. The client shows how your own system connects
-to the actors inside it.
+Deploy the app, then connect to its actors from your own system:
-
-
-
`deployApp()` returns the endpoint, namespace, pool, and token the ordinary
RivetKit client needs. See [SQLite in Rivet Actors](https://rivet.dev/actors/docs/sqlite/)
diff --git a/docs/content/docs/static-websites.mdx b/docs/content/docs/static-websites.mdx
index f265d6136..51d388660 100644
--- a/docs/content/docs/static-websites.mdx
+++ b/docs/content/docs/static-websites.mdx
@@ -1,11 +1,17 @@
---
-title: "Static Websites"
+title: "Frontends & Static Sites"
description: "Serve an HTML, CSS, and JavaScript site from a Dynamic App."
---
-An app is a directory with a `package.json` and a server entrypoint. A static
+import ExampleLinkBar from "@/components/docs/ExampleLinkBar.astro";
+
+
+
+An app is a directory with a `package.json` and a fetch entrypoint. A static
site is the same directory plus a `public/` folder and a handler that serves
-it. [View the complete example](https://github.com/rivet-dev/dynamic-apps/tree/main/examples/apps-static-website).
+it.
+
+## Example generated code
@@ -13,6 +19,8 @@ it. [View the complete example](https://github.com/rivet-dev/dynamic-apps/tree/m
+## Deploy and route
+
Deploy the directory and open `/apps/static-website/`:
diff --git a/docs/content/docs/workflows.mdx b/docs/content/docs/workflows.mdx
index 9f595ac71..e98654535 100644
--- a/docs/content/docs/workflows.mdx
+++ b/docs/content/docs/workflows.mdx
@@ -3,17 +3,23 @@ title: "Workflows"
description: "Run durable multi-step jobs that sleep, scale to zero, and resume."
---
+import ExampleLinkBar from "@/components/docs/ExampleLinkBar.astro";
+
+
+
An actor's `run` workflow executes when the actor is created. Each step is
durable, so the job survives restarts and the app scales to zero while it
-sleeps. [View the complete example](https://github.com/rivet-dev/dynamic-apps/tree/main/examples/apps-workflows).
+sleeps.
+
+## Example generated code
+
+
+
+## Deploy and connect
-The server is the generated app. The client shows how your own system connects
-to the actors inside it.
+Deploy the app, then connect to its actors from your own system:
-
-
-
See [Workflows in Rivet Actors](https://rivet.dev/actors/docs/workflows/) for
steps, loops, queues, and error handling.
diff --git a/docs/sidebar.json b/docs/sidebar.json
index 2616afda2..d199582ff 100644
--- a/docs/sidebar.json
+++ b/docs/sidebar.json
@@ -1,75 +1,105 @@
{
- "docs": [
- {
- "title": "General",
- "pages": [
- {
- "title": "Introduction",
- "href": "/dynamic-apps/docs",
- "icon": "faSquareInfo"
- },
- {
- "title": "Quickstart",
- "href": "/dynamic-apps/docs/quickstart",
- "icon": "faForwardFast"
- },
- {
- "title": "Core Quickstart",
- "href": "/dynamic-apps/docs/quickstart-core",
- "icon": "faForwardFast"
- }
- ]
- },
- {
- "title": "Concepts",
- "pages": [
- {
- "title": "Deploying Apps",
- "href": "/dynamic-apps/docs/deploy"
- },
- {
- "title": "Routing",
- "href": "/dynamic-apps/docs/routing"
- },
- {
- "title": "Custom Storage",
- "href": "/dynamic-apps/docs/custom-storage"
- }
- ]
- },
- {
- "title": "Capabilities",
- "pages": [
- {
- "title": "Static Websites",
- "href": "/dynamic-apps/docs/static-websites"
- },
- {
- "title": "SQLite",
- "href": "/dynamic-apps/docs/sqlite"
- },
- {
- "title": "Multiplayer",
- "href": "/dynamic-apps/docs/multiplayer"
- },
- {
- "title": "Workflows",
- "href": "/dynamic-apps/docs/workflows"
- }
- ]
- },
- {
- "title": "Reference",
- "pages": [
- {
- "title": "Authentication",
- "href": "/dynamic-apps/docs/authentication"
- },
- {
- "title": "Collecting logs",
- "href": "/dynamic-apps/docs/logging"
- }
- ]
- }
- ]
+ "docs": [
+ {
+ "title": "General",
+ "pages": [
+ {
+ "title": "Introduction",
+ "href": "/dynamic-apps/docs",
+ "icon": "faSquareInfo"
+ },
+ {
+ "title": "Quickstart",
+ "href": "/dynamic-apps/docs/quickstart",
+ "icon": "faForwardFast"
+ },
+ {
+ "title": "Quickstart (Core)",
+ "href": "/dynamic-apps/docs/quickstart-core",
+ "icon": "faForwardFast"
+ }
+ ]
+ },
+ {
+ "title": "Concepts",
+ "pages": [
+ {
+ "title": "Generate",
+ "href": "/dynamic-apps/docs/generate",
+ "icon": "faSparkles"
+ },
+ {
+ "title": "Deploy",
+ "href": "/dynamic-apps/docs/deploy",
+ "icon": "faRocket"
+ },
+ {
+ "title": "Route",
+ "href": "/dynamic-apps/docs/routing",
+ "icon": "faRoute"
+ },
+ {
+ "title": "Connect to Rivet",
+ "href": "/dynamic-apps/docs/connect",
+ "icon": "faPlug"
+ }
+ ]
+ },
+ {
+ "title": "Workloads",
+ "pages": [
+ {
+ "title": "Backends & REST APIs",
+ "href": "/dynamic-apps/docs/backends"
+ },
+ {
+ "title": "Frontends & Static Sites",
+ "href": "/dynamic-apps/docs/static-websites"
+ },
+ {
+ "title": "SQLite",
+ "href": "/dynamic-apps/docs/sqlite"
+ },
+ {
+ "title": "Multiplayer",
+ "href": "/dynamic-apps/docs/multiplayer"
+ },
+ {
+ "title": "Workflows",
+ "href": "/dynamic-apps/docs/workflows"
+ }
+ ]
+ },
+ {
+ "title": "Reference",
+ "pages": [
+ {
+ "title": "Architecture",
+ "href": "/dynamic-apps/docs/architecture"
+ },
+ {
+ "title": "Authentication",
+ "href": "/dynamic-apps/docs/authentication"
+ },
+ {
+ "title": "Collecting Logs",
+ "href": "/dynamic-apps/docs/logging"
+ },
+ {
+ "title": "Advanced",
+ "collapsible": true,
+ "pages": [
+ {
+ "title": "Core",
+ "href": "/dynamic-apps/docs/core"
+ },
+ {
+ "title": "Customize the VM",
+ "href": "/dynamic-apps/docs/customize-vm"
+ }
+ ]
+ }
+ ]
+ }
+ ]
}
diff --git a/examples/apps-ai-builder/fixtures/app/package.json b/examples/apps-ai-builder/fixtures/app/package.json
index 7174e192b..dc4c4ae1f 100644
--- a/examples/apps-ai-builder/fixtures/app/package.json
+++ b/examples/apps-ai-builder/fixtures/app/package.json
@@ -9,11 +9,10 @@
"check-types": "tsc --noEmit"
},
"dependencies": {
- "@hono/node-server": "2.0.11",
- "hono": "4.12.9"
+ "hono": "4.13.5"
},
"devDependencies": {
- "@types/node": "22.19.15",
- "typescript": "5.7.3"
+ "@types/node": "22.20.1",
+ "typescript": "5.9.3"
}
}
diff --git a/examples/apps-ai-builder/fixtures/app/src/index.ts b/examples/apps-ai-builder/fixtures/app/src/index.ts
index 88dc11adf..234b31809 100644
--- a/examples/apps-ai-builder/fixtures/app/src/index.ts
+++ b/examples/apps-ai-builder/fixtures/app/src/index.ts
@@ -1,4 +1,3 @@
-import { serve } from "@hono/node-server";
import { Hono } from "hono";
const app = new Hono();
@@ -9,7 +8,4 @@ app.get("/", (context) =>
}),
);
-serve({
- fetch: app.fetch,
- port: Number(process.env.PORT ?? 3000),
-});
+export default app;
diff --git a/examples/apps-ai-builder/package.json b/examples/apps-ai-builder/package.json
index cab81a21e..1f71d7eb8 100644
--- a/examples/apps-ai-builder/package.json
+++ b/examples/apps-ai-builder/package.json
@@ -8,15 +8,16 @@
"check-types": "tsc --noEmit"
},
"dependencies": {
- "@ai-sdk/anthropic": "^4.0.19",
- "@hono/node-server": "^2.0.11",
+ "@ai-sdk/anthropic": "^4.0.46",
+ "@hono/node-server": "^2.1.1",
"@rivet-dev/dynamic-apps": "workspace:*",
- "ai": "^7.0.37",
- "hono": "^4.12.9"
+ "ai": "^7.0.86",
+ "hono": "^4.13.5",
+ "zod": "^4.5.4"
},
"devDependencies": {
- "@types/node": "^22.19.15",
- "tsx": "^4.20.6",
- "typescript": "^5.7.3"
+ "@types/node": "^22.20.1",
+ "tsx": "^4.23.13",
+ "typescript": "^5.9.3"
}
}
diff --git a/examples/apps-ai-builder/src/server.ts b/examples/apps-ai-builder/src/server.ts
index fd08a9800..0959d72ed 100644
--- a/examples/apps-ai-builder/src/server.ts
+++ b/examples/apps-ai-builder/src/server.ts
@@ -7,8 +7,9 @@ import {
rivetActorsSkill,
webServerSkill,
} from "@rivet-dev/dynamic-apps";
-import { generateText } from "ai";
+import { generateObject } from "ai";
import { Hono } from "hono";
+import { z } from "zod";
const editablePaths = [
"package.json",
@@ -29,15 +30,12 @@ async function loadSeed(): Promise> {
return files;
}
-function parseFiles(text: string): Record {
- const json = text.match(/```json\s*([\s\S]*?)```/)?.[1] ?? text;
- const value = JSON.parse(json) as { files?: Record };
- if (!value.files || typeof value.files !== "object") {
- throw new TypeError("model response must contain a files object");
- }
+const filesSchema = z.object({ files: z.record(z.string(), z.string()) });
+
+function validateFiles(value: Record): Record {
const files: Record = {};
for (const path of editablePaths) {
- const content = value.files[path];
+ const content = value[path];
if (typeof content !== "string") {
throw new TypeError(`model response is missing ${path}`);
}
@@ -49,19 +47,20 @@ function parseFiles(text: string): Record {
return files;
}
+// docs:start generate
async function revise(
prompt: string,
files: Record,
diagnostics?: string,
): Promise> {
- const result = await generateText({
+ 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,
- 'Return JSON only as {"files":{"path":"content"}}.',
`You may edit only: ${editablePaths.join(", ")}.`,
`User request: ${prompt}`,
diagnostics ? `Previous build diagnostics:\n${diagnostics}` : "",
@@ -70,8 +69,9 @@ async function revise(
.filter(Boolean)
.join("\n\n"),
});
- return parseFiles(result.text);
+ return validateFiles(result.object.files);
}
+// docs:end generate
async function generateApp(appId: string, prompt: string) {
let files = await revise(prompt, await loadSeed());
diff --git a/examples/apps-core-quickstart/package.json b/examples/apps-core-quickstart/package.json
index e0ae2bb2d..5077bd6a5 100644
--- a/examples/apps-core-quickstart/package.json
+++ b/examples/apps-core-quickstart/package.json
@@ -8,13 +8,13 @@
"check-types": "tsc --noEmit"
},
"dependencies": {
- "@hono/node-server": "^2.0.11",
+ "@hono/node-server": "^2.1.1",
"@rivet-dev/dynamic-apps-core": "workspace:*",
- "hono": "^4.12.9"
+ "hono": "^4.13.5"
},
"devDependencies": {
- "@types/node": "^22.19.15",
- "tsx": "^4.20.6",
- "typescript": "^5.7.3"
+ "@types/node": "^22.20.1",
+ "tsx": "^4.23.13",
+ "typescript": "^5.9.3"
}
}
diff --git a/examples/apps-core-quickstart/src/server.ts b/examples/apps-core-quickstart/src/server.ts
index a801a78fa..999f9b884 100644
--- a/examples/apps-core-quickstart/src/server.ts
+++ b/examples/apps-core-quickstart/src/server.ts
@@ -1,3 +1,4 @@
+// docs:start setup
import { serve } from "@hono/node-server";
import {
type ActiveRelease,
@@ -19,12 +20,6 @@ const dynamicApps = createDynamicApps({
...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,
};
@@ -38,8 +33,6 @@ const dynamicApps = createDynamicApps({
return release
? {
...release,
- regions: [...release.regions],
- scaling: { ...release.scaling },
artifact: {
...release.artifact,
bytes: new Uint8Array(release.artifact.bytes),
@@ -57,21 +50,9 @@ const dynamicApps = createDynamicApps({
};
},
});
+// docs:end setup
-const app = new Hono();
-app.route("/apps", dynamicApps.appsRouter);
-
-let server: ReturnType | 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());
-
+// docs:start deploy
await dynamicApps.deployApp({
appId: "hello",
files: {
@@ -89,10 +70,13 @@ await dynamicApps.deployApp({
`,
},
});
+// docs:end deploy
+
+// docs:start routing
+const app = new Hono();
+app.route("/apps", dynamicApps.appsRouter);
-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}`);
+serve({ fetch: app.fetch, port });
+console.log(`Dynamic Apps Core listening on http://localhost:${port}`);
+// docs:end routing
diff --git a/examples/apps-hello-world/fixtures/app/package.json b/examples/apps-hello-world/fixtures/app/package.json
index 29af844f2..18a47cf11 100644
--- a/examples/apps-hello-world/fixtures/app/package.json
+++ b/examples/apps-hello-world/fixtures/app/package.json
@@ -3,10 +3,7 @@
"version": "0.0.0",
"private": true,
"main": "src/index.ts",
- "scripts": {
- "check-types": "node --check src/index.mjs"
- },
"dependencies": {
- "hono": "^4.12.9"
+ "hono": "4.13.5"
}
}
diff --git a/examples/apps-hello-world/package.json b/examples/apps-hello-world/package.json
index 993787fd1..bc1dd3803 100644
--- a/examples/apps-hello-world/package.json
+++ b/examples/apps-hello-world/package.json
@@ -9,13 +9,13 @@
"check-types": "tsc --noEmit"
},
"dependencies": {
- "@hono/node-server": "^2.0.11",
+ "@hono/node-server": "^2.1.1",
"@rivet-dev/dynamic-apps": "workspace:*",
- "hono": "^4.12.9"
+ "hono": "^4.13.5"
},
"devDependencies": {
- "@types/node": "^22.19.15",
- "tsx": "^4.20.6",
- "typescript": "^5.7.3"
+ "@types/node": "^22.20.1",
+ "tsx": "^4.23.13",
+ "typescript": "^5.9.3"
}
}
diff --git a/examples/apps-hello-world/src/deploy.ts b/examples/apps-hello-world/src/deploy.ts
index 0798c4d69..0bb836c12 100644
--- a/examples/apps-hello-world/src/deploy.ts
+++ b/examples/apps-hello-world/src/deploy.ts
@@ -12,7 +12,7 @@ await deployApp({
type: "module",
main: "src/index.ts",
dependencies: {
- hono: "^4.12.9",
+ hono: "4.13.5",
},
}),
"src/index.ts": `
diff --git a/examples/apps-hello-world/src/server.ts b/examples/apps-hello-world/src/server.ts
index b13cf5934..b42544187 100644
--- a/examples/apps-hello-world/src/server.ts
+++ b/examples/apps-hello-world/src/server.ts
@@ -4,6 +4,7 @@ import { Hono } from "hono";
const server = new Hono();
+// This is how the Rivet control plane communicates with your backend.
server.all("/api/rivet/*", (c) => appsRouter.fetch(c.req.raw));
// Mount every deployed application at /apps/:appId.
diff --git a/examples/apps-multiplayer/README.md b/examples/apps-multiplayer/README.md
new file mode 100644
index 000000000..73b3a97f3
--- /dev/null
+++ b/examples/apps-multiplayer/README.md
@@ -0,0 +1,15 @@
+# Dynamic Apps: Multiplayer
+
+The deployed app defines one keyed room actor. Clients share player positions
+and receive realtime events whenever a player joins or moves.
+
+Run the example with Node.js 22 or newer:
+
+```sh
+pnpm --dir examples/apps-multiplayer start
+# In another terminal:
+pnpm --dir examples/apps-multiplayer client
+```
+
+The app's ordinary HTTP handler is available at
+`http://localhost:3000/apps/multiplayer-room/`.
diff --git a/examples/apps-multiplayer/fixtures/app/package.json b/examples/apps-multiplayer/fixtures/app/package.json
index 7da6914f1..7466f95c0 100644
--- a/examples/apps-multiplayer/fixtures/app/package.json
+++ b/examples/apps-multiplayer/fixtures/app/package.json
@@ -5,8 +5,7 @@
"type": "module",
"main": "src/index.ts",
"dependencies": {
- "@hono/node-server": "2.1.1",
- "hono": "4.13.3",
+ "hono": "4.13.5",
"rivetkit": "2.3.11"
}
}
diff --git a/examples/apps-multiplayer/fixtures/app/src/index.ts b/examples/apps-multiplayer/fixtures/app/src/index.ts
index 9be651b42..6debf3686 100644
--- a/examples/apps-multiplayer/fixtures/app/src/index.ts
+++ b/examples/apps-multiplayer/fixtures/app/src/index.ts
@@ -1,4 +1,3 @@
-import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { actor, event, setup } from "rivetkit";
@@ -30,15 +29,4 @@ app.get("/", (c) =>
c.json({ message: "Use the RivetKit client to join a room." }),
);
-// Dynamic Apps runs the actors in serverless mode and waits for this listener.
-if (process.env.RIVETKIT_RUNTIME_MODE === "serverless") {
- await new Promise((resolve, reject) => {
- const server = serve(
- { fetch: app.fetch, port: Number(process.env.PORT), hostname: "0.0.0.0" },
- () => resolve(),
- );
- server.once("error", reject);
- });
-}
-
export default app;
diff --git a/examples/apps-multiplayer/package.json b/examples/apps-multiplayer/package.json
index dad4ae8f8..121dd3f7d 100644
--- a/examples/apps-multiplayer/package.json
+++ b/examples/apps-multiplayer/package.json
@@ -9,14 +9,14 @@
"check-types": "tsc --noEmit"
},
"dependencies": {
- "@hono/node-server": "^2.0.11",
+ "@hono/node-server": "^2.1.1",
"@rivet-dev/dynamic-apps": "workspace:*",
- "hono": "^4.12.9",
+ "hono": "^4.13.5",
"rivetkit": "2.3.11"
},
"devDependencies": {
- "@types/node": "^22.19.15",
- "tsx": "^4.20.6",
- "typescript": "^5.7.3"
+ "@types/node": "^22.20.1",
+ "tsx": "^4.23.13",
+ "typescript": "^5.9.3"
}
}
diff --git a/examples/apps-sqlite/README.md b/examples/apps-sqlite/README.md
new file mode 100644
index 000000000..50da64acd
--- /dev/null
+++ b/examples/apps-sqlite/README.md
@@ -0,0 +1,15 @@
+# Dynamic Apps: SQLite
+
+The deployed app defines a RivetKit actor backed by SQLite. The client deploys
+the app, connects to its namespace, and writes and reads real database rows.
+
+Run the example with Node.js 22 or newer:
+
+```sh
+pnpm --dir examples/apps-sqlite start
+# In another terminal:
+pnpm --dir examples/apps-sqlite client
+```
+
+The app's ordinary HTTP handler is available at
+`http://localhost:3000/apps/sqlite-notes/`.
diff --git a/examples/apps-sqlite/fixtures/app/package.json b/examples/apps-sqlite/fixtures/app/package.json
index 52522ca5a..99541d252 100644
--- a/examples/apps-sqlite/fixtures/app/package.json
+++ b/examples/apps-sqlite/fixtures/app/package.json
@@ -5,8 +5,7 @@
"type": "module",
"main": "src/index.ts",
"dependencies": {
- "@hono/node-server": "2.1.1",
- "hono": "4.13.3",
+ "hono": "4.13.5",
"rivetkit": "2.3.11"
}
}
diff --git a/examples/apps-sqlite/fixtures/app/src/index.ts b/examples/apps-sqlite/fixtures/app/src/index.ts
index 553a9efe7..bb9069195 100644
--- a/examples/apps-sqlite/fixtures/app/src/index.ts
+++ b/examples/apps-sqlite/fixtures/app/src/index.ts
@@ -1,4 +1,3 @@
-import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { actor, setup } from "rivetkit";
import { db } from "rivetkit/db";
@@ -33,15 +32,4 @@ app.get("/", (c) =>
c.json({ message: "Use the RivetKit client to add notes." }),
);
-// Dynamic Apps runs the actors in serverless mode and waits for this listener.
-if (process.env.RIVETKIT_RUNTIME_MODE === "serverless") {
- await new Promise((resolve, reject) => {
- const server = serve(
- { fetch: app.fetch, port: Number(process.env.PORT), hostname: "0.0.0.0" },
- () => resolve(),
- );
- server.once("error", reject);
- });
-}
-
export default app;
diff --git a/examples/apps-sqlite/package.json b/examples/apps-sqlite/package.json
index 84bdb1dbe..f4165d5ab 100644
--- a/examples/apps-sqlite/package.json
+++ b/examples/apps-sqlite/package.json
@@ -9,14 +9,14 @@
"check-types": "tsc --noEmit"
},
"dependencies": {
- "@hono/node-server": "^2.0.11",
+ "@hono/node-server": "^2.1.1",
"@rivet-dev/dynamic-apps": "workspace:*",
- "hono": "^4.12.9",
+ "hono": "^4.13.5",
"rivetkit": "2.3.11"
},
"devDependencies": {
- "@types/node": "^22.19.15",
- "tsx": "^4.20.6",
- "typescript": "^5.7.3"
+ "@types/node": "^22.20.1",
+ "tsx": "^4.23.13",
+ "typescript": "^5.9.3"
}
}
diff --git a/examples/apps-static-website/README.md b/examples/apps-static-website/README.md
new file mode 100644
index 000000000..68ecae10b
--- /dev/null
+++ b/examples/apps-static-website/README.md
@@ -0,0 +1,14 @@
+# Dynamic Apps: Static Website
+
+A directory with `index.html` and no `package.json` is served directly. CSS,
+JavaScript, SVG, and other assets are included in the immutable release. A
+package with a build script is treated as a built static site when it emits
+`dist/index.html`.
+
+Run the example with Node.js 22 or newer:
+
+```sh
+pnpm --dir examples/apps-static-website start
+```
+
+Open `http://localhost:3000/apps/static-website/`.
diff --git a/examples/apps-static-website/fixtures/app/package.json b/examples/apps-static-website/fixtures/app/package.json
index 2119d84bf..a04f703bd 100644
--- a/examples/apps-static-website/fixtures/app/package.json
+++ b/examples/apps-static-website/fixtures/app/package.json
@@ -5,6 +5,6 @@
"type": "module",
"main": "src/index.ts",
"dependencies": {
- "hono": "4.13.3"
+ "hono": "4.13.5"
}
}
diff --git a/examples/apps-static-website/package.json b/examples/apps-static-website/package.json
index 3e6e165b2..e67ed73b9 100644
--- a/examples/apps-static-website/package.json
+++ b/examples/apps-static-website/package.json
@@ -8,13 +8,13 @@
"check-types": "tsc --noEmit"
},
"dependencies": {
- "@hono/node-server": "^2.0.11",
+ "@hono/node-server": "^2.1.1",
"@rivet-dev/dynamic-apps": "workspace:*",
- "hono": "^4.12.9"
+ "hono": "^4.13.5"
},
"devDependencies": {
- "@types/node": "^22.19.15",
- "tsx": "^4.20.6",
- "typescript": "^5.7.3"
+ "@types/node": "^22.20.1",
+ "tsx": "^4.23.13",
+ "typescript": "^5.9.3"
}
}
diff --git a/examples/apps-workflows/README.md b/examples/apps-workflows/README.md
new file mode 100644
index 000000000..dd73f046a
--- /dev/null
+++ b/examples/apps-workflows/README.md
@@ -0,0 +1,15 @@
+# Dynamic Apps: Workflows
+
+The deployed app defines a keyed job actor whose durable workflow starts when
+the actor is created, sleeps, and resumes before marking the job complete.
+
+Run the example with Node.js 22 or newer:
+
+```sh
+pnpm --dir examples/apps-workflows start
+# In another terminal:
+pnpm --dir examples/apps-workflows client
+```
+
+The app's ordinary HTTP handler is available at
+`http://localhost:3000/apps/durable-workflow/`.
diff --git a/examples/apps-workflows/fixtures/app/package.json b/examples/apps-workflows/fixtures/app/package.json
index fd1f12ca2..f2abde44a 100644
--- a/examples/apps-workflows/fixtures/app/package.json
+++ b/examples/apps-workflows/fixtures/app/package.json
@@ -5,8 +5,7 @@
"type": "module",
"main": "src/index.ts",
"dependencies": {
- "@hono/node-server": "2.1.1",
- "hono": "4.13.3",
+ "hono": "4.13.5",
"rivetkit": "2.3.11"
}
}
diff --git a/examples/apps-workflows/fixtures/app/src/index.ts b/examples/apps-workflows/fixtures/app/src/index.ts
index 3b5439e93..b1b24032a 100644
--- a/examples/apps-workflows/fixtures/app/src/index.ts
+++ b/examples/apps-workflows/fixtures/app/src/index.ts
@@ -1,4 +1,3 @@
-import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { actor, setup } from "rivetkit";
import { workflow } from "rivetkit/workflow";
@@ -34,15 +33,4 @@ app.get("/", (c) =>
c.json({ message: "Use the RivetKit client to read orders." }),
);
-// Dynamic Apps runs the actors in serverless mode and waits for this listener.
-if (process.env.RIVETKIT_RUNTIME_MODE === "serverless") {
- await new Promise((resolve, reject) => {
- const server = serve(
- { fetch: app.fetch, port: Number(process.env.PORT), hostname: "0.0.0.0" },
- () => resolve(),
- );
- server.once("error", reject);
- });
-}
-
export default app;
diff --git a/examples/apps-workflows/package.json b/examples/apps-workflows/package.json
index aa1d61911..3aad4d328 100644
--- a/examples/apps-workflows/package.json
+++ b/examples/apps-workflows/package.json
@@ -9,14 +9,14 @@
"check-types": "tsc --noEmit"
},
"dependencies": {
- "@hono/node-server": "^2.0.11",
+ "@hono/node-server": "^2.1.1",
"@rivet-dev/dynamic-apps": "workspace:*",
- "hono": "^4.12.9",
+ "hono": "^4.13.5",
"rivetkit": "2.3.11"
},
"devDependencies": {
- "@types/node": "^22.19.15",
- "tsx": "^4.20.6",
- "typescript": "^5.7.3"
+ "@types/node": "^22.20.1",
+ "tsx": "^4.23.13",
+ "typescript": "^5.9.3"
}
}
diff --git a/packages/dynamic-apps-builder/cli/apps-builder.mjs b/packages/dynamic-apps-builder/cli/apps-builder.mjs
index 406fc782e..1d28d4213 100755
--- a/packages/dynamic-apps-builder/cli/apps-builder.mjs
+++ b/packages/dynamic-apps-builder/cli/apps-builder.mjs
@@ -303,6 +303,14 @@ function nodeFileSystemPlugin() {
const resolver = createRequire(pathToFileURL(importer));
return { path: resolver.resolve(args.path) };
} catch (error) {
+ if (
+ args.path === "@hono/node-server" ||
+ args.path === "hono/ws"
+ ) {
+ try {
+ return { path: builderRequire.resolve(args.path) };
+ } catch {}
+ }
if (
config.usesRivetKit &&
(args.path === "rivetkit" ||
diff --git a/packages/dynamic-apps-builder/package.json b/packages/dynamic-apps-builder/package.json
index 689545834..d8c09e983 100644
--- a/packages/dynamic-apps-builder/package.json
+++ b/packages/dynamic-apps-builder/package.json
@@ -34,8 +34,10 @@
"test": "vitest run test/ --passWithNoTests"
},
"dependencies": {
+ "@hono/node-server": "2.1.1",
"@rivetkit/rivetkit-wasm": "2.3.11",
"esbuild-wasm": "0.27.4",
+ "hono": "4.13.5",
"rivetkit": "2.3.11"
},
"devDependencies": {
diff --git a/packages/dynamic-apps-builder/test/builder.test.ts b/packages/dynamic-apps-builder/test/builder.test.ts
index 1efc76504..9a1da8b46 100644
--- a/packages/dynamic-apps-builder/test/builder.test.ts
+++ b/packages/dynamic-apps-builder/test/builder.test.ts
@@ -295,15 +295,20 @@ describe("apps-builder", () => {
await writeFile(
join(workspace, "src", "index.mjs"),
[
- 'import http from "node:http";',
'import { actor, setup } from "rivetkit";',
"const counter = actor({ state: { count: 0 } });",
"const registry = setup({ use: { counter } });",
- "http.createServer(async (incoming, outgoing) => {",
- " const chunks = []; for await (const chunk of incoming) chunks.push(Buffer.from(chunk));",
- " const response = await registry.handler(new Request(new URL(incoming.url ?? '/', 'http://actor.test'), { method: incoming.method, headers: incoming.headers, body: incoming.method === 'GET' || incoming.method === 'HEAD' ? undefined : Buffer.concat(chunks) }));",
- " outgoing.statusCode = response.status; response.headers.forEach((value, name) => outgoing.setHeader(name, value)); outgoing.end(Buffer.from(await response.arrayBuffer()));",
- "}).listen(Number(process.env.PORT), '0.0.0.0');",
+ "export default { fetch(request) {",
+ " if (new URL(request.url).pathname === '/ordinary') return new Response(new ReadableStream({",
+ " async start(controller) {",
+ " controller.enqueue(new TextEncoder().encode('started-'));",
+ " await new Promise((resolve) => setTimeout(resolve, 100));",
+ " controller.enqueue(new TextEncoder().encode('finished'));",
+ " controller.close();",
+ " },",
+ " }));",
+ " return registry.handler(request);",
+ "} };",
].join("\n"),
);
const configPath = join(root, "config.json");
@@ -349,36 +354,48 @@ describe("apps-builder", () => {
env: {
...process.env,
PORT: String(port),
+ DYNAMIC_APPS_READY_NONCE: "builder-test",
RIVETKIT_RUNTIME: "wasm",
RIVETKIT_RUNTIME_MODE: "serverless",
},
});
let stderr = "";
- child.stdout.resume();
+ let stdout = "";
+ child.stdout.on("data", (chunk) => {
+ stdout += String(chunk);
+ });
child.stderr.on("data", (chunk) => {
stderr += chunk;
});
- await new Promise((resolveReady, rejectReady) => {
- const timeout = setTimeout(() => {
- child.kill("SIGKILL");
- rejectReady(new Error(`actor bundle did not start: ${stderr}`));
- }, 5_000);
- const poll = setInterval(() => {
- void fetch(`http://127.0.0.1:${port}/.agentos/ready`)
- .then(() => {
- clearInterval(poll);
- clearTimeout(timeout);
- resolveReady();
- })
- .catch(() => {});
- }, 20);
- child.once("exit", (code) => {
- clearInterval(poll);
- clearTimeout(timeout);
- rejectReady(new Error(`actor bundle exited ${code}: ${stderr}`));
+ try {
+ await new Promise((resolveReady, rejectReady) => {
+ const timeout = setTimeout(() => {
+ child.kill("SIGKILL");
+ rejectReady(new Error(`actor bundle did not start: ${stderr}`));
+ }, 5_000);
+ const poll = setInterval(() => {
+ if (!stdout.includes("DYNAMIC_APPS_SERVER_READY:builder-test"))
+ return;
+ clearInterval(poll);
+ clearTimeout(timeout);
+ resolveReady();
+ }, 20);
+ child.once("exit", (code) => {
+ clearInterval(poll);
+ clearTimeout(timeout);
+ rejectReady(new Error(`actor bundle exited ${code}: ${stderr}`));
+ });
});
- });
- child.kill("SIGKILL");
+ const response = await fetch(`http://127.0.0.1:${port}/ordinary`);
+ const reader = response.body?.getReader();
+ if (!reader) throw new Error("streaming response has no body");
+ const decoder = new TextDecoder();
+ expect(decoder.decode((await reader.read()).value)).toBe("started-");
+ expect(decoder.decode((await reader.read()).value)).toBe("finished");
+ expect((await reader.read()).done).toBe(true);
+ } finally {
+ child.kill("SIGKILL");
+ }
}, 15_000);
});
diff --git a/packages/dynamic-apps-core/README.md b/packages/dynamic-apps-core/README.md
index 2c80edd90..d5dd1a68c 100644
--- a/packages/dynamic-apps-core/README.md
+++ b/packages/dynamic-apps-core/README.md
@@ -22,12 +22,6 @@ const dynamicApps = createDynamicApps({
...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,
};
@@ -64,10 +58,39 @@ Each factory instance owns its builder configuration, router, release
subscriptions, runtime cache, agentOS context pool, and cleanup timer. Await
`dynamicApps.dispose()` during shutdown.
+Application entrypoints always default-export a Fetch-compatible handler. They
+must not call `serve()`, `listen()`, or `registry.start()`; the serving runtime
+owns the listener.
+
+For a release whose artifact has `usesRivetKit: true`, `loadActiveRelease` must
+also return its `server.environment`, and the factory must receive an
+`ApplicationServerRuntime` through `serverRuntime`. That shared runtime handles
+ordinary HTTP and Rivet callbacks in one cached process. The standard
+`@rivet-dev/dynamic-apps` package wires this automatically.
+
The default `pooled` mode leases a bounded agentOS context and resets it after
each request. `ephemeral` mode creates a fresh context per request while reusing
the immutable release VM. Use container isolation between trust domains.
+Pass additional VM configuration through `vm`. This supports any
+number of agentOS software packages as well as runtime observability callbacks:
+
+```ts
+const dynamicApps = createDynamicApps({
+ // Release hooks omitted.
+ vm: {
+ software: [firstPackage, secondPackage],
+ onAgentStderr: (event) => logger.error(event),
+ onLimitWarning: (warning) => logger.warn(warning),
+ },
+});
+```
+
+Dynamic Apps retains ownership of the `/app` artifact mount and the V8 heap
+limit used for executor memory accounting. Other agentOS options pass through
+to each serving VM. The Dynamic Apps `logger` continues to receive application
+stdout/stderr and build events.
+
The in-memory example is development-only: it loses releases on restart and
cannot invalidate another process. Use durable object storage plus a reliable
cross-process invalidation channel in production.
diff --git a/packages/dynamic-apps-core/package.json b/packages/dynamic-apps-core/package.json
index 4d805d3bf..5ee5e6294 100644
--- a/packages/dynamic-apps-core/package.json
+++ b/packages/dynamic-apps-core/package.json
@@ -10,7 +10,10 @@
},
"type": "module",
"sideEffects": false,
- "files": ["dist", "package.json"],
+ "files": [
+ "dist",
+ "package.json"
+ ],
"exports": {
".": {
"import": {
@@ -25,7 +28,9 @@
}
}
},
- "engines": { "node": ">=22.0.0" },
+ "engines": {
+ "node": ">=22.0.0"
+ },
"scripts": {
"build": "tsup src/index.ts src/internal.ts --format esm --dts --sourcemap --clean --external @rivet-dev/agentos-core --external @rivet-dev/agentos-toolchain --external @rivet-dev/dynamic-apps-builder --external @agentos-software/sh --external @agentos-software/tar",
"check-types": "tsc --noEmit",
@@ -37,7 +42,7 @@
"@rivet-dev/agentos-core": "0.2.18",
"@rivet-dev/agentos-toolchain": "0.2.18",
"@rivet-dev/dynamic-apps-builder": "workspace:0.3.0",
- "hono": "^4.7.0"
+ "hono": "^4.13.5"
},
"devDependencies": {
"@types/node": "^22.19.15",
diff --git a/packages/dynamic-apps-core/src/build.ts b/packages/dynamic-apps-core/src/build.ts
index 9ec74a6ce..7b30d0510 100644
--- a/packages/dynamic-apps-core/src/build.ts
+++ b/packages/dynamic-apps-core/src/build.ts
@@ -210,7 +210,7 @@ export function validateDeployment(
if (!packageJsonSource) {
fail(
"dynamic_apps_entrypoint_not_found",
- "direct applications must contain package.json and a server entrypoint",
+ "direct applications must contain package.json and a fetch entrypoint",
);
}
let packageJson: {
@@ -281,7 +281,7 @@ export function validateDeployment(
}
fail(
"dynamic_apps_entrypoint_not_found",
- "could not infer a direct server entrypoint",
+ "could not infer a direct fetch entrypoint",
);
}
diff --git a/packages/dynamic-apps-core/src/executor.ts b/packages/dynamic-apps-core/src/executor.ts
index af92f20ea..a0df986ed 100644
--- a/packages/dynamic-apps-core/src/executor.ts
+++ b/packages/dynamic-apps-core/src/executor.ts
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
import { availableParallelism, tmpdir } from "node:os";
import { join } from "node:path";
-import { AgentOs } from "@rivet-dev/agentos-core";
+import { AgentOs, type AgentOsOptions } from "@rivet-dev/agentos-core";
import { DynamicAppsError } from "./errors.js";
import { DynamicAppsLogLineDecoder, emitDynamicAppsLog } from "./logging.js";
import { capConcurrencyForMemory, readCgroupMemory } from "./memory.js";
@@ -10,6 +10,7 @@ import { DIRECT_BUNDLE_PATH, DIRECT_RUNTIME_FORMAT } from "./runtime.js";
import { validateAppId } from "./source.js";
import type {
ActiveRelease,
+ ApplicationServerRuntime,
ReleaseInvalidation,
ReleaseLoadContext,
Unsubscribe,
@@ -130,7 +131,7 @@ interface PreparedRuntime {
interface AppMapping {
resolution: ActiveRelease;
- runtime: PreparedRuntime;
+ runtime?: PreparedRuntime;
}
interface AppCacheEntry {
@@ -320,6 +321,55 @@ export function resolveExecutorConfig(
return config;
}
+export function resolveRuntimeAgentOsOptions(
+ base: AgentOsOptions | undefined,
+ artifactPath: string,
+ heapLimitMb: number,
+): AgentOsOptions {
+ if (base?.mounts?.some((mount) => mount.path === "/app")) {
+ throw new DynamicAppsError(
+ "dynamic_apps_invalid_config",
+ 'vm.mounts cannot replace the reserved "/app" mount',
+ );
+ }
+ return {
+ ...base,
+ sidecar: base?.sidecar ?? { kind: "shared", pool: "dynamic-apps-direct" },
+ defaultSoftware: base?.defaultSoftware ?? false,
+ mounts: [
+ ...(base?.mounts ?? []),
+ {
+ path: "/app",
+ readOnly: true,
+ plugin: {
+ id: "agentos_packages",
+ config: {
+ kind: "tar",
+ tarPath: artifactPath,
+ root: "/",
+ readOnly: true,
+ },
+ },
+ },
+ ],
+ permissions: {
+ fs: "allow",
+ childProcess: "allow",
+ process: "allow",
+ env: "allow",
+ network: "allow",
+ ...base?.permissions,
+ },
+ limits: {
+ ...base?.limits,
+ jsRuntime: {
+ ...base?.limits?.jsRuntime,
+ v8HeapLimitMb: heapLimitMb,
+ },
+ },
+ };
+}
+
function invalidExecutorConfig(name: string): DynamicAppsError {
return new DynamicAppsError(
"dynamic_apps_invalid_config",
@@ -330,6 +380,8 @@ function invalidExecutorConfig(name: string): DynamicAppsError {
export class DynamicAppsExecutor {
readonly config: ExecutorConfig;
readonly #source: ExecutorReleaseSource;
+ readonly #serverRuntime?: ApplicationServerRuntime;
+ readonly #vmOptions?: AgentOsOptions;
readonly #semaphore: Semaphore;
readonly #apps = new Map();
readonly #runtimes = new Map();
@@ -340,9 +392,16 @@ export class DynamicAppsExecutor {
#disposed = false;
#disposePromise?: Promise;
- constructor(source: ExecutorReleaseSource, config: ExecutorConfig) {
+ constructor(
+ source: ExecutorReleaseSource,
+ config: ExecutorConfig,
+ vmOptions?: AgentOsOptions,
+ serverRuntime?: ApplicationServerRuntime,
+ ) {
this.config = config;
this.#source = source;
+ this.#vmOptions = vmOptions;
+ this.#serverRuntime = serverRuntime;
this.#semaphore = new Semaphore(
config.executionConcurrency,
config.executionQueueSize,
@@ -381,8 +440,6 @@ export class DynamicAppsExecutor {
const envelope = await measure(trace, "request-buffer", () =>
serializeRequest(request),
);
- const requestedRegion =
- request.headers.get("x-agentos-app-region") ?? undefined;
const { entry, hit } = this.#appEntry(appId);
if (!hit) trace.cacheOutcome = "app-miss";
entry.refs += 1;
@@ -391,22 +448,23 @@ export class DynamicAppsExecutor {
const mapping = entry.mapping
? entry.mapping
: await this.#resolveAndPrepare(entry, trace);
- if (
- requestedRegion &&
- !mapping.resolution.regions.includes(requestedRegion)
- ) {
- throw new DynamicAppsError(
- "dynamic_apps_region_not_deployed",
- `app is not deployed in requested region ${requestedRegion}`,
- { requestedRegion, regions: mapping.resolution.regions },
+ trace.release = mapping.resolution.release;
+ const runtime = mapping.runtime;
+ if (!runtime) {
+ const response = await this.#executeServer(
+ mapping.resolution,
+ envelope,
+ trace,
+ request.signal,
);
+ this.#finishTrace(response.headers, trace);
+ return response;
}
- trace.release = mapping.resolution.release;
- mapping.runtime.refs += 1;
- mapping.runtime.lastUsedAt = Date.now();
+ runtime.refs += 1;
+ runtime.lastUsedAt = Date.now();
try {
const response = await this.#execute(
- mapping.runtime,
+ runtime,
envelope,
trace,
request.signal,
@@ -414,8 +472,8 @@ export class DynamicAppsExecutor {
this.#finishTrace(response.headers, trace);
return response;
} finally {
- mapping.runtime.refs -= 1;
- void this.#maybeDisposeRuntime(mapping.runtime);
+ runtime.refs -= 1;
+ void this.#maybeDisposeRuntime(runtime);
}
} finally {
entry.refs -= 1;
@@ -473,6 +531,7 @@ export class DynamicAppsExecutor {
.filter((item) => item.lastContextResetError !== undefined)
.at(-1)?.lastContextResetError,
evaluations: runtimes.reduce((sum, item) => sum + item.evaluations, 0),
+ serverRuntime: this.#serverRuntime?.diagnostics?.(),
};
}
@@ -589,7 +648,18 @@ export class DynamicAppsExecutor {
"artifact-verify",
async () => verifyActiveRelease(resolution),
);
- const runtime = await this.#prepareRuntime(verifiedResolution, trace);
+ const runtime = verifiedResolution.artifact.usesRivetKit
+ ? undefined
+ : await this.#prepareRuntime(verifiedResolution, trace);
+ if (
+ verifiedResolution.artifact.usesRivetKit &&
+ (!this.#serverRuntime || !verifiedResolution.server)
+ ) {
+ throw new DynamicAppsError(
+ "dynamic_apps_server_runtime_missing",
+ "RivetKit applications require a configured HTTP server runtime",
+ );
+ }
if (entry.epoch !== epoch) continue;
if (this.#disposed || this.#apps.get(entry.appId) !== entry) {
throw disposedError();
@@ -607,6 +677,34 @@ export class DynamicAppsExecutor {
}
}
+ async #executeServer(
+ resolution: ActiveRelease,
+ envelope: RequestEnvelope,
+ trace: RequestTrace,
+ requestSignal: AbortSignal,
+ ): Promise {
+ const serverRuntime = this.#serverRuntime;
+ const server = resolution.server;
+ if (!serverRuntime || !server) {
+ throw new DynamicAppsError(
+ "dynamic_apps_server_runtime_missing",
+ "RivetKit applications require a configured HTTP server runtime",
+ );
+ }
+ return await measure(trace, "server-request", () =>
+ serverRuntime.request({
+ key: `${resolution.release}:${resolution.artifact.hash}`,
+ appId: resolution.appId,
+ release: resolution.release,
+ loadArtifact: async () => new Uint8Array(resolution.artifact.bytes),
+ environment: { ...server.environment },
+ request: requestFromEnvelope(envelope, requestSignal),
+ maxRequestBytes: resolution.maxRequestBytes,
+ maxResponseBytes: resolution.maxResponseBytes,
+ }),
+ );
+ }
+
async #prepareRuntime(
resolution: ActiveRelease,
trace?: RequestTrace,
@@ -656,35 +754,13 @@ export class DynamicAppsExecutor {
await chmod(directory, 0o700);
await writeFile(artifactPath, artifact, { mode: 0o600 });
vm = await measureOptional(trace, "vm-prepare", () =>
- AgentOs.create({
- sidecar: { kind: "shared", pool: "dynamic-apps-direct" },
- defaultSoftware: false,
- mounts: [
- {
- path: "/app",
- readOnly: true,
- plugin: {
- id: "agentos_packages",
- config: {
- kind: "tar",
- tarPath: artifactPath,
- root: "/",
- readOnly: true,
- },
- },
- },
- ],
- permissions: {
- fs: "allow",
- childProcess: "allow",
- process: "allow",
- env: "allow",
- network: "allow",
- },
- limits: {
- jsRuntime: { v8HeapLimitMb: this.config.contextHeapLimitMb },
- },
- }),
+ AgentOs.create(
+ resolveRuntimeAgentOsOptions(
+ this.#vmOptions,
+ artifactPath,
+ this.config.contextHeapLimitMb,
+ ),
+ ),
);
const runtime: PreparedRuntime = {
key,
@@ -1299,14 +1375,6 @@ function verifyActiveRelease(input: ActiveRelease): ActiveRelease {
Buffer.byteLength(input.release) < 1 ||
Buffer.byteLength(input.release) > 256 ||
/[\0-\x1f\x7f]/.test(input.release) ||
- !Array.isArray(input.regions) ||
- input.regions.length === 0 ||
- input.regions.length > 128 ||
- input.regions.some(
- (region) =>
- typeof region !== "string" || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(region),
- ) ||
- !validScaling(input.scaling) ||
!Number.isSafeInteger(input.maxRequestBytes) ||
input.maxRequestBytes < 1 ||
!Number.isSafeInteger(input.maxResponseBytes) ||
@@ -1327,7 +1395,9 @@ function verifyActiveRelease(input: ActiveRelease): ActiveRelease {
!Number.isSafeInteger(artifact.byteLength) ||
artifact.byteLength < 1 ||
artifact.byteLength !== artifact.bytes.byteLength ||
- typeof artifact.usesRivetKit !== "boolean"
+ typeof artifact.usesRivetKit !== "boolean" ||
+ artifact.usesRivetKit !== Boolean(input.server) ||
+ (input.server !== undefined && !validServerConfig(input.server))
) {
throw new DynamicAppsError(
"dynamic_apps_artifact_manifest_mismatch",
@@ -1343,25 +1413,35 @@ function verifyActiveRelease(input: ActiveRelease): ActiveRelease {
}
return {
...input,
- regions: [...input.regions],
- scaling: { ...input.scaling },
+ ...(input.server
+ ? { server: { environment: { ...input.server.environment } } }
+ : {}),
artifact: { ...artifact, bytes },
};
}
-function validScaling(value: ActiveRelease["scaling"]): boolean {
+function validServerConfig(
+ value: NonNullable,
+): boolean {
+ if (
+ !(
+ value !== null &&
+ typeof value === "object" &&
+ value.environment !== null &&
+ typeof value.environment === "object" &&
+ !Array.isArray(value.environment)
+ )
+ )
+ return false;
+ const entries = Object.entries(value.environment);
return (
- value !== null &&
- typeof value === "object" &&
- Number.isInteger(value.minReplicas) &&
- value.minReplicas >= 0 &&
- Number.isInteger(value.maxReplicas) &&
- value.maxReplicas >= 1 &&
- value.maxReplicas <= 128 &&
- value.minReplicas <= value.maxReplicas &&
- Number.isInteger(value.targetConcurrency) &&
- value.targetConcurrency >= 1 &&
- value.targetConcurrency <= 1_024
+ entries.length <= 128 &&
+ entries.every(
+ ([name, content]) =>
+ /^[A-Z_][A-Z0-9_]*$/.test(name) &&
+ typeof content === "string" &&
+ Buffer.byteLength(content) <= 64 * 1024,
+ )
);
}
@@ -1409,6 +1489,24 @@ async function serializeRequest(request: Request): Promise {
};
}
+function requestFromEnvelope(
+ envelope: RequestEnvelope,
+ signal: AbortSignal,
+): Request {
+ const body = envelope.bodyBase64
+ ? Buffer.from(envelope.bodyBase64, "base64")
+ : undefined;
+ return new Request(envelope.url, {
+ method: envelope.method,
+ headers: envelope.headers,
+ body:
+ envelope.method === "GET" || envelope.method === "HEAD"
+ ? undefined
+ : body,
+ signal,
+ });
+}
+
function responseFromEnvelope(
envelope: ResponseEnvelope,
method: string,
diff --git a/packages/dynamic-apps-core/src/factory.ts b/packages/dynamic-apps-core/src/factory.ts
index 1ce15e5c4..3d99e8318 100644
--- a/packages/dynamic-apps-core/src/factory.ts
+++ b/packages/dynamic-apps-core/src/factory.ts
@@ -38,6 +38,8 @@ export function createDynamicApps(
watchActiveRelease: options.watchActiveRelease,
},
executorConfig,
+ options.vm,
+ options.serverRuntime,
);
const appsRouter = createAppsRouter(executor);
const inFlight = new Set>();
@@ -67,8 +69,6 @@ export function createDynamicApps(
...built.artifact,
bytes: new Uint8Array(built.artifact.bytes),
},
- regions: input.regions ? [...input.regions] : undefined,
- scaling: input.scaling ? { ...input.scaling } : undefined,
createdAt: Date.now(),
};
const result = await options.publishRelease(publishInput, deployOptions);
diff --git a/packages/dynamic-apps-core/src/index.ts b/packages/dynamic-apps-core/src/index.ts
index f77ecac4b..0d8a67499 100644
--- a/packages/dynamic-apps-core/src/index.ts
+++ b/packages/dynamic-apps-core/src/index.ts
@@ -1,7 +1,8 @@
export { createDynamicApps } from "./factory.js";
export type {
ActiveRelease,
- AppScaling,
+ ApplicationServerRuntime,
+ ApplicationServerRuntimeRequest,
BuildArtifactCache,
BuildConfig,
BuiltAppRelease,
diff --git a/packages/dynamic-apps-core/src/runtime.ts b/packages/dynamic-apps-core/src/runtime.ts
index 84fc495e8..12093a773 100644
--- a/packages/dynamic-apps-core/src/runtime.ts
+++ b/packages/dynamic-apps-core/src/runtime.ts
@@ -71,15 +71,7 @@ export function directRunnerSource(input: {
}): string {
const entrypoint = `./${normalizeAppPath(input.entrypoint)}`;
return `const dynamicAppsModuleImportStartedAt = performance.now();
-const dynamicAppsPreviousRuntimeMode = process.env.RIVETKIT_RUNTIME_MODE;
-delete process.env.RIVETKIT_RUNTIME_MODE;
-let application;
-try {
- application = await import(${JSON.stringify(entrypoint)});
-} finally {
- if (dynamicAppsPreviousRuntimeMode === undefined) delete process.env.RIVETKIT_RUNTIME_MODE;
- else process.env.RIVETKIT_RUNTIME_MODE = dynamicAppsPreviousRuntimeMode;
-}
+const application = await import(${JSON.stringify(entrypoint)});
const dynamicAppsModuleImportMs = performance.now() - dynamicAppsModuleImportStartedAt;
const exported = application.default;
const appFetch = typeof exported === "function"
@@ -147,15 +139,33 @@ export async function dispatch(input) {
`;
}
-/** Initializes RivetKit WASM, then starts the application's own HTTP server. */
+/** Initializes RivetKit WASM and hosts the application's exported fetch handler. */
export function actorRunnerSource(entrypointInput: string): string {
const entrypoint = `./${normalizeAppPath(entrypointInput)}`;
return `import { readFile } from "node:fs/promises";
+import { serve } from "@hono/node-server";
import initializeRivetKit from "@rivetkit/rivetkit-wasm";
const wasmUrl = new URL(__AGENTOS_RIVETKIT_WASM_PATH__, import.meta.url);
await initializeRivetKit({ module_or_path: await readFile(wasmUrl) });
-await import(${JSON.stringify(entrypoint)});
+const applicationModule = await import(${JSON.stringify(entrypoint)});
+const application = applicationModule.default;
+const fetch = typeof application === "function"
+ ? application
+ : typeof application?.fetch === "function"
+ ? application.fetch.bind(application)
+ : undefined;
+if (!fetch) {
+ throw new TypeError("Dynamic App entrypoint default export must be a fetch handler");
+}
+await new Promise((resolve, reject) => {
+ const server = serve({
+ fetch,
+ port: Number(process.env.PORT),
+ hostname: "0.0.0.0",
+ }, resolve);
+ server.once("error", reject);
+});
console.log("DYNAMIC_APPS_SERVER_READY:" + (process.env.DYNAMIC_APPS_READY_NONCE ?? ""));
`;
}
diff --git a/packages/dynamic-apps-core/src/types.ts b/packages/dynamic-apps-core/src/types.ts
index e03b92e8f..b8c65ba1d 100644
--- a/packages/dynamic-apps-core/src/types.ts
+++ b/packages/dynamic-apps-core/src/types.ts
@@ -1,18 +1,16 @@
+import type { AgentOsOptions } from "@rivet-dev/agentos-core";
import type { Hono } from "hono";
import type { BlankEnv, BlankSchema } from "hono/types";
-export interface AppScaling {
- minReplicas?: number;
- maxReplicas?: number;
- targetConcurrency?: number;
-}
-
interface DeployAppBase {
appId: string;
- /** @deprecated Retained by the Rivet adapter for source compatibility. */
+ /**
+ * Whether the storage adapter provisions an isolated namespace for this
+ * app. Defaults to true, or to the app's stored setting from an earlier
+ * explicit deploy. Set false to skip provisioning; apps deployed without a
+ * namespace cannot use app-defined actors.
+ */
createNamespace?: boolean;
- regions?: string[];
- scaling?: AppScaling;
}
export type DeployAppInput =
@@ -35,8 +33,6 @@ export interface PublishReleaseInput {
appId: string;
buildId: string;
artifact: ReleaseArtifact;
- regions?: string[];
- scaling?: AppScaling;
createdAt: number;
}
@@ -44,10 +40,28 @@ export interface ActiveRelease {
appId: string;
release: string;
artifact: ReleaseArtifact;
- regions: string[];
- scaling: Required;
maxRequestBytes: number;
maxResponseBytes: number;
+ /** Required runtime environment for releases that use RivetKit. */
+ server?: {
+ environment: Record;
+ };
+}
+
+export interface ApplicationServerRuntimeRequest {
+ key: string;
+ appId: string;
+ release: string;
+ loadArtifact(): Promise;
+ environment: Record;
+ request: Request;
+ maxRequestBytes?: number;
+ maxResponseBytes?: number;
+}
+
+export interface ApplicationServerRuntime {
+ request(input: ApplicationServerRuntimeRequest): Promise;
+ diagnostics?(): Record;
}
export type ReleaseInvalidation = () => void;
@@ -120,6 +134,10 @@ export interface DynamicAppsOptions {
invalidate: ReleaseInvalidation,
): Promise;
executor?: Partial;
+ /** Additional options passed to every application-serving VM. */
+ vm?: AgentOsOptions;
+ /** Shared HTTP runtime used by releases that contain RivetKit actors. */
+ serverRuntime?: ApplicationServerRuntime;
build?: Partial;
artifactCache?: BuildArtifactCache;
logger?: DynamicAppsLogger;
diff --git a/packages/dynamic-apps-core/tests/agentos-options.test.ts b/packages/dynamic-apps-core/tests/agentos-options.test.ts
new file mode 100644
index 000000000..b838bb43b
--- /dev/null
+++ b/packages/dynamic-apps-core/tests/agentos-options.test.ts
@@ -0,0 +1,67 @@
+import type { AgentOsOptions } from "@rivet-dev/agentos-core";
+import { describe, expect, test, vi } from "vitest";
+import { resolveRuntimeAgentOsOptions } from "../src/executor.js";
+
+describe("agentOS runtime options", () => {
+ test("preserves multiple software packages and observability callbacks", () => {
+ const software = [
+ { packagePath: "/packages/one" },
+ { packagePath: "/packages/two" },
+ ];
+ const onAgentStderr = vi.fn();
+ const onLimitWarning = vi.fn();
+ const options = resolveRuntimeAgentOsOptions(
+ { software, onAgentStderr, onLimitWarning },
+ "/tmp/release.aospkg",
+ 96,
+ );
+
+ expect(options.software).toEqual(software);
+ expect(options.onAgentStderr).toBe(onAgentStderr);
+ expect(options.onLimitWarning).toBe(onLimitWarning);
+ expect(options.defaultSoftware).toBe(false);
+ expect(options.limits?.jsRuntime?.v8HeapLimitMb).toBe(96);
+ expect(options.mounts?.at(-1)?.path).toBe("/app");
+ });
+
+ test("preserves caller options while protecting executor-owned settings", () => {
+ const options = resolveRuntimeAgentOsOptions(
+ {
+ defaultSoftware: true,
+ allowedNodeBuiltins: ["node:path"],
+ limits: {
+ resources: { maxSockets: 512 },
+ jsRuntime: { capturedOutputLimitBytes: 1024, v8HeapLimitMb: 512 },
+ },
+ } satisfies AgentOsOptions,
+ "/tmp/release.aospkg",
+ 64,
+ );
+
+ expect(options.defaultSoftware).toBe(true);
+ expect(options.allowedNodeBuiltins).toEqual(["node:path"]);
+ expect(options.limits?.resources?.maxSockets).toBe(512);
+ expect(options.limits?.jsRuntime).toEqual({
+ capturedOutputLimitBytes: 1024,
+ v8HeapLimitMb: 64,
+ });
+ });
+
+ test("rejects a caller-provided /app mount", () => {
+ expect(() =>
+ resolveRuntimeAgentOsOptions(
+ {
+ mounts: [
+ {
+ path: "/app",
+ readOnly: true,
+ plugin: { id: "test", config: {} },
+ },
+ ],
+ },
+ "/tmp/release.aospkg",
+ 64,
+ ),
+ ).toThrow('vm.mounts cannot replace the reserved "/app" mount');
+ });
+});
diff --git a/packages/dynamic-apps-core/tests/core.test.ts b/packages/dynamic-apps-core/tests/core.test.ts
index 8a746bf12..9aa49ff60 100644
--- a/packages/dynamic-apps-core/tests/core.test.ts
+++ b/packages/dynamic-apps-core/tests/core.test.ts
@@ -51,12 +51,6 @@ describe("createDynamicApps", () => {
...input.artifact,
bytes: new Uint8Array(input.artifact.bytes),
},
- regions: input.regions ?? ["local"],
- scaling: {
- minReplicas: 0,
- maxReplicas: 128,
- targetConcurrency: 8,
- },
maxRequestBytes: 1024 * 1024,
maxResponseBytes: 4 * 1024 * 1024,
};
@@ -160,7 +154,6 @@ describe("createDynamicApps", () => {
const first = await dynamicApps.appsRouter.request("/demo/");
expect(first.headers.get("x-agentos-app-release")).toBe("release-before");
loaded.release = "release-after";
- loaded.regions[0] = "mutated";
loaded.artifact.bytes[0] ^= 1;
const second = await dynamicApps.appsRouter.request("/demo/");
expect(second.headers.get("x-agentos-app-release")).toBe(
@@ -171,6 +164,64 @@ describe("createDynamicApps", () => {
await dynamicApps.dispose();
}
});
+
+ test("routes RivetKit releases through the shared server runtime", async () => {
+ const bytes = await makeArtifact("unused-direct-entrypoint");
+ const active = release("demo", "actors", bytes);
+ active.artifact.usesRivetKit = true;
+ active.server = {
+ environment: {
+ RIVET_ENDPOINT: "https://api.rivet.dev",
+ RIVET_NAMESPACE: "demo",
+ },
+ };
+ const requests: Array<{ key: string; path: string; environment: string }> =
+ [];
+ const dynamicApps = createDynamicApps({
+ async publishRelease() {},
+ async watchActiveRelease() {
+ return () => {};
+ },
+ async loadActiveRelease() {
+ return active;
+ },
+ serverRuntime: {
+ async request(input) {
+ requests.push({
+ key: input.key,
+ path: new URL(input.request.url).pathname,
+ environment: input.environment.RIVET_NAMESPACE ?? "",
+ });
+ expect(await input.loadArtifact()).toEqual(bytes);
+ return new Response(`server:${requests.length}`);
+ },
+ },
+ executor: { executionMode: "ephemeral" },
+ });
+ try {
+ expect(
+ await (await dynamicApps.appsRouter.request("/demo/one")).text(),
+ ).toBe("server:1");
+ expect(
+ await (await dynamicApps.appsRouter.request("/demo/two")).text(),
+ ).toBe("server:2");
+ expect(requests).toEqual([
+ {
+ key: `release-actors:${active.artifact.hash}`,
+ path: "/one",
+ environment: "demo",
+ },
+ {
+ key: `release-actors:${active.artifact.hash}`,
+ path: "/two",
+ environment: "demo",
+ },
+ ]);
+ expect(dynamicApps.diagnostics()).toMatchObject({ runtimes: 0 });
+ } finally {
+ await dynamicApps.dispose();
+ }
+ });
});
function release(
@@ -189,8 +240,6 @@ function release(
byteLength: bytes.byteLength,
usesRivetKit: false,
},
- regions: ["local"],
- scaling: { minReplicas: 0, maxReplicas: 128, targetConcurrency: 8 },
maxRequestBytes: 1024 * 1024,
maxResponseBytes: 4 * 1024 * 1024,
};
diff --git a/packages/dynamic-apps/API_CONTRACT.md b/packages/dynamic-apps/API_CONTRACT.md
index c3f5b3651..f3a4fa052 100644
--- a/packages/dynamic-apps/API_CONTRACT.md
+++ b/packages/dynamic-apps/API_CONTRACT.md
@@ -47,17 +47,9 @@ The expected generated declaration is structurally equivalent to:
import type { Hono } from "hono";
import type { BlankEnv, BlankSchema } from "hono/types";
-interface AppScaling {
- minReplicas?: number;
- maxReplicas?: number;
- targetConcurrency?: number;
-}
-
interface DeployAppBase {
appId: string;
createNamespace?: boolean;
- regions?: string[];
- scaling?: AppScaling;
}
type DeployAppInput =
@@ -77,14 +69,12 @@ interface Deployment {
namespace: string;
pool: string;
token?: string;
- regions: string[];
}
interface PreparedDeployAppInput {
appId: string;
files: Record;
- regions?: string[];
- scaling?: AppScaling;
+ createNamespace?: boolean;
}
interface DeployAppOptions {
@@ -228,7 +218,7 @@ export.
release is active. It no longer means a replica was warmed.
- A failed build or incomplete artifact write does not replace the previous
active release.
-- Identical built artifact bytes plus normalized regions/scaling,
+- Identical built artifact bytes plus normalized
namespace/runtime metadata, and packaging identity produce the same opaque
release ID within one packaging version. Unlocked dependency resolution or a
nondeterministic build may produce a different artifact and therefore a new
@@ -237,15 +227,16 @@ export.
### Compatibility fields
-- `regions` remains accepted, deduplicated in input order, validated, stored,
- and returned. There must be 1–8 values matching
- `[a-z0-9][a-z0-9-]{0,62}`. The default is the state actor's current region,
- falling back to `default`. It does not place local execution in a remote
- region.
-- `scaling` remains accepted and validated. Defaults are `minReplicas: 0`,
- `maxReplicas: 128`, and `targetConcurrency: 8`; bounds are 0–128, 1–128, and
- 1–1,024 respectively, with `minReplicas <= maxReplicas`. It is compatibility
- metadata for direct HTTP and has no deleted scaler/replica effect.
+- `createNamespace` defaults to true as of 0.12. An explicit value is
+ persisted on the app actor's state and a later explicit value overrides it.
+ With `false`, publish and activation skip namespace provisioning and token
+ minting, and the deployment reports the host connection's namespace and the
+ deterministic pool. A `false` publish of a release that declares `rivetkit`
+ is rejected with `dynamic_apps_namespace_required`.
+- `regions` and `scaling` are removed from `DeployAppInput` and from the
+ `Deployment` result as of 0.12. They configured nothing at runtime. The
+ private state actor still stores and returns legacy region/scaling metadata
+ so existing SQLite rows keep resolving; the host ignores it.
- `endpoint`, `namespace`, deterministic `pool`, and an optional namespace-scoped
publishable `token` are returned. The pool is
`dynamic-apps-${sha256(appId).slice(0, 16)}`. Direct HTTP does not execute in
@@ -264,7 +255,6 @@ The resolved value contains exactly these enumerable keys:
namespace: string;
pool: string;
token?: string;
- regions: string[];
}
```
@@ -349,12 +339,11 @@ part of the retained API.
## App-defined RivetKit actor contract
An application that declares `rivetkit` mounts `registry.handler()` at
-`/api/rivet/*` in its default exported fetch router. When
-`RIVETKIT_RUNTIME_MODE=serverless`, it must also listen on the numeric `PORT`
-provided by Dynamic Apps. The same router serves ordinary direct requests. The
-server entrypoint should await its listening callback. Dynamic Apps treats
-successful module evaluation as readiness and does not poll an application
-health route.
+`/api/rivet/*` in its default exported fetch router. Application code does not
+open a listener. Dynamic Apps starts a platform-owned HTTP server around that
+export, waits for its listening callback, and sends both ordinary application
+requests and Rivet callbacks through the same cached process. It does not poll
+an application health route.
On activation, deployment configures the returned `namespace` and `pool` with
an authenticated serverless callback to the private `dynamicAppsApp` actor.
@@ -370,6 +359,10 @@ an idle-cache target, not a callback admission limit; active callbacks are not
rejected or evicted because the target is full. Guest runtimes do not receive
the deployment actor's control credential.
+Ordinary HTTP for an actor-enabled release uses that same runtime key and HTTP
+listener. The application module is imported once per warm runtime. Apps
+without RivetKit continue to use the direct evaluation modes below.
+
Rivet Engine's `/start` response is an SSE control stream. It contains the
exact encoded runner ID/protocol version once, then keepalive pings, and stays
open for the serverless runner lifespan; actor requests travel separately over
diff --git a/packages/dynamic-apps/README.md b/packages/dynamic-apps/README.md
index 7e1356543..e582b2e3e 100644
--- a/packages/dynamic-apps/README.md
+++ b/packages/dynamic-apps/README.md
@@ -37,15 +37,13 @@ bodies are not supported.
## App-defined actors
-An app that declares `rivetkit` mounts the registry handler in its normal fetch
-router and starts an HTTP server on `PORT` when it runs in serverless mode.
-Dynamic Apps waits for the server entrypoint's listening callback before
-forwarding callbacks:
+An app that declares `rivetkit` mounts the registry handler in its exported
+fetch router. Dynamic Apps owns the HTTP listener and uses the same cached
+agentOS process for ordinary routes and Rivet callbacks:
```ts
import { actor, setup } from "rivetkit";
import { Hono } from "hono";
-import { serve } from "@hono/node-server";
const counter = actor({
state: { value: 0 },
@@ -60,24 +58,12 @@ const registry = setup({ use: { counter } });
const app = new Hono();
app.all("/api/rivet/*", (c) => registry.handler(c.req.raw));
app.all("*", () => new Response("ok"));
-if (process.env.RIVETKIT_RUNTIME_MODE === "serverless") {
- await new Promise((resolve, reject) => {
- const server = serve(
- {
- fetch: app.fetch,
- port: Number(process.env.PORT),
- hostname: "0.0.0.0",
- },
- resolve,
- );
- server.once("error", reject);
- });
-}
export default app;
```
-Awaiting Hono's listening callback makes module completion the readiness signal;
-Dynamic Apps does not poll an application health route.
+Do not call `serve()`, `registry.start()`, or bind a port in application code.
+Dynamic Apps starts the platform listener and waits for its listening callback;
+it does not poll an application health route.
Use the unchanged `deployApp` result to create the app client:
@@ -119,8 +105,9 @@ receiver that accepts child-namespace lifecycle callbacks; Dynamic Apps appends
Actor requests follow the normal Rivet Engine path. The app's serverless
callback mounts its verified artifact in agentOS and runs the bundled RivetKit
WebAssembly runtime in serverless mode. State, actions, events, connections,
-and streaming actor responses are handled by RivetKit inside the sandbox;
-ordinary HTTP for the same app uses the agentOS evaluation path.
+and streaming actor responses are handled by RivetKit inside the sandbox. The
+same listener serves the app's ordinary HTTP routes without importing the app a
+second time.
Rivet Engine requires `/api/rivet/start` to return a long-lived SSE control
stream containing the real runner-init packet and keepalive pings. Actor traffic
@@ -170,6 +157,9 @@ const deployment = await deployApp({
`DYNAMIC_APPS_EXECUTION_MODE` selects the request execution strategy:
+These modes apply to apps without RivetKit actors. Actor-enabled apps use one
+cached server process for both ordinary HTTP and Rivet callbacks.
+
| Mode | Cached object | Cache-hit request |
| --- | --- | --- |
| `pooled` (default) | verified artifact, one agentOS VM, and up to N retained contexts | lease a context, evaluate once, reset and reinitialize it, then return it to the pool |
@@ -242,10 +232,9 @@ blocking network request in the callback.
## Memory and trust boundary
Each immutable release owns one bounded agentOS VM with a read-only mounted
-artifact. Context count, runtime entries, artifact bytes, idle TTLs, execution
-concurrency, queues, and cgroup high-water eviction are independently bounded.
-Successful pooled contexts are reset and reinitialized; failed, timed-out, or
-aborted contexts are deleted.
+artifact. Apps without actors use resettable evaluation contexts. Actor-enabled
+apps use one cached server process so ordinary HTTP and Rivet's streaming
+callback share a single module instance.
agentOS supplies the filesystem, process, environment, and network permission
boundary for both direct HTTP and app-defined actors. RivetKit actors use the
diff --git a/packages/dynamic-apps/package.json b/packages/dynamic-apps/package.json
index 3d61a5197..ed9d2c053 100644
--- a/packages/dynamic-apps/package.json
+++ b/packages/dynamic-apps/package.json
@@ -33,7 +33,7 @@
"dependencies": {
"@rivet-dev/agentos-core": "0.2.18",
"@rivet-dev/dynamic-apps-core": "workspace:0.3.0",
- "hono": "^4.7.0",
+ "hono": "^4.13.5",
"rivetkit": "2.3.11"
},
"devDependencies": {
diff --git a/packages/dynamic-apps/src/actor-runtime.ts b/packages/dynamic-apps/src/actor-runtime.ts
index c595b522d..25c78ccf3 100644
--- a/packages/dynamic-apps/src/actor-runtime.ts
+++ b/packages/dynamic-apps/src/actor-runtime.ts
@@ -3,6 +3,10 @@ import { chmod, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { AgentOs } from "@rivet-dev/agentos-core";
+import type {
+ ApplicationServerRuntime,
+ ApplicationServerRuntimeRequest,
+} from "@rivet-dev/dynamic-apps-core";
import { emitDynamicAppsLog } from "./logging.js";
const ACTOR_HTTP_PORT = 3000;
@@ -27,14 +31,22 @@ interface ActorRuntimeConfig {
requestTimeoutMs: number;
}
-export interface ActorRuntimeRequest {
+export interface ActorRuntimeRequest
+ extends Omit<
+ ApplicationServerRuntimeRequest,
+ "appId" | "release" | "environment"
+ > {
key: string;
appId?: string;
release?: string;
loadArtifact: () => Promise;
- endpoint: string;
- namespace: string;
- pool: string;
+ environment?: Record;
+ /** @deprecated Use environment. */
+ endpoint?: string;
+ /** @deprecated Use environment. */
+ namespace?: string;
+ /** @deprecated Use environment. */
+ pool?: string;
request: Request;
}
@@ -51,7 +63,7 @@ interface RuntimeEntry {
}
/** Runs RivetKit callbacks in cached agentOS VMs using its native HTTP stream. */
-export class DynamicActorRuntime {
+export class DynamicActorRuntime implements ApplicationServerRuntime {
readonly config: ActorRuntimeConfig;
readonly #entries = new Map();
readonly #creating = new Map>();
@@ -121,7 +133,7 @@ export class DynamicActorRuntime {
async request(input: ActorRuntimeRequest): Promise {
const body = await readBoundedBody(
input.request.body,
- this.config.maxStartPayloadBytes,
+ input.maxRequestBytes ?? this.config.maxStartPayloadBytes,
);
if (!body)
return new Response("RivetKit actor start payload exceeds limit", {
@@ -150,6 +162,7 @@ export class DynamicActorRuntime {
}
let settled = false;
+ let responseBytes = 0;
const settle = () => {
if (settled) return;
settled = true;
@@ -169,6 +182,20 @@ export class DynamicActorRuntime {
if (settled) return controller.close();
try {
const chunk = await entry.vm.fetchStreamRead(head.streamId);
+ responseBytes += chunk.body.byteLength;
+ if (
+ input.maxResponseBytes !== undefined &&
+ responseBytes > input.maxResponseBytes
+ ) {
+ await entry.vm.fetchStreamCancel(head.streamId).catch(() => {});
+ controller.error(
+ new RangeError(
+ "Dynamic App response exceeds the configured limit",
+ ),
+ );
+ settle();
+ return;
+ }
if (chunk.body.byteLength > 0) controller.enqueue(chunk.body);
if (chunk.done) {
controller.close();
@@ -276,6 +303,7 @@ export class DynamicActorRuntime {
const artifactPath = join(directory, "release.aospkg");
let vm: AgentOs | undefined;
try {
+ const environment = resolveWorkerEnvironment(input);
await chmod(directory, 0o700);
await writeFile(artifactPath, await input.loadArtifact(), {
mode: 0o600,
@@ -283,7 +311,7 @@ export class DynamicActorRuntime {
vm = await AgentOs.create({
sidecar: { kind: "shared", pool: "dynamic-apps-actors" },
defaultSoftware: false,
- loopbackExemptPorts: loopbackExemptPorts(input.endpoint),
+ loopbackExemptPorts: loopbackExemptPorts(environment.RIVET_ENDPOINT),
mounts: [
{
path: "/app",
@@ -322,7 +350,8 @@ export class DynamicActorRuntime {
const process = await vm.process.spawn("node", ["/app/actor/main.mjs"], {
cwd: "/app/actor",
env: stringEnvironment({
- ...actorWorkerEnvironment(input),
+ ...environment,
+ PORT: String(ACTOR_HTTP_PORT),
DYNAMIC_APPS_READY_NONCE: readyNonce,
}),
onStdout: (data) => {
@@ -519,9 +548,12 @@ async function withTimeout(
}
/** @internal */
-export function actorWorkerEnvironment(
- input: Pick,
-): NodeJS.ProcessEnv {
+export function actorWorkerEnvironment(input: {
+ endpoint: string;
+ key: string;
+ namespace: string;
+ pool: string;
+}): NodeJS.ProcessEnv {
const endpoint = new URL(input.endpoint);
const endpointNamespace = endpoint.username
? decodeURIComponent(endpoint.username)
@@ -551,6 +583,23 @@ export function actorWorkerEnvironment(
};
}
+function resolveWorkerEnvironment(
+ input: ActorRuntimeRequest,
+): Record {
+ if (input.environment) return { ...input.environment };
+ if (!input.endpoint || !input.namespace || !input.pool) {
+ throw new Error("Dynamic App server runtime environment is missing");
+ }
+ return stringEnvironment(
+ actorWorkerEnvironment({
+ endpoint: input.endpoint,
+ key: input.key,
+ namespace: input.namespace,
+ pool: input.pool,
+ }),
+ );
+}
+
function stringEnvironment(env: NodeJS.ProcessEnv): Record {
return Object.fromEntries(
Object.entries(env).filter(
@@ -559,7 +608,8 @@ function stringEnvironment(env: NodeJS.ProcessEnv): Record {
);
}
-function loopbackExemptPorts(endpoint: string): number[] {
+function loopbackExemptPorts(endpoint: string | undefined): number[] {
+ if (!endpoint) return [];
const url = new URL(endpoint);
if (
url.hostname !== "127.0.0.1" &&
diff --git a/packages/dynamic-apps/src/actors.ts b/packages/dynamic-apps/src/actors.ts
index de5da8bb7..e8b22f39f 100644
--- a/packages/dynamic-apps/src/actors.ts
+++ b/packages/dynamic-apps/src/actors.ts
@@ -1,8 +1,5 @@
import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
-import type {
- AppScaling,
- BuildArtifactCache,
-} from "@rivet-dev/dynamic-apps-core";
+import type { BuildArtifactCache } from "@rivet-dev/dynamic-apps-core";
import {
buildAppRelease,
DIRECT_ENTRYPOINT,
@@ -16,11 +13,13 @@ import {
configureAppNamespaceRunner,
provisionAppNamespace,
resolveDefaultRivetConnection,
+ unprovisionedAppNamespace,
} from "./control-plane.js";
import { APP_CALLBACK_SECRET_HEADER } from "./runtime.js";
import type {
AppReleaseInfo,
AppRouteResolution,
+ AppScaling,
Deployment,
PreparedDeployAppInput,
} from "./types.js";
@@ -89,6 +88,7 @@ interface BeginReleasePublishInput {
artifactHash: string;
artifactBytes: number;
usesRivetKit: boolean;
+ createNamespace?: boolean;
regions?: string[];
scaling?: AppScaling;
createdAt: number;
@@ -547,11 +547,19 @@ function validateBeginInput(
input.artifactBytes < 1 ||
input.artifactBytes > MAX_ARTIFACT_BYTES ||
typeof input.usesRivetKit !== "boolean" ||
+ (input.createNamespace !== undefined &&
+ typeof input.createNamespace !== "boolean") ||
!Number.isSafeInteger(input.createdAt) ||
input.createdAt < 0
) {
fail("dynamic_apps_publish_invalid", "release publish metadata is invalid");
}
+ if (input.createNamespace === false && input.usesRivetKit) {
+ fail(
+ "dynamic_apps_publish_invalid",
+ "apps that use rivetkit require a namespace; remove createNamespace: false",
+ );
+ }
return appId;
}
@@ -594,14 +602,15 @@ async function beginReleasePublishLocked(
const state = c.state as AppState;
const regions = normalizeRegions(input.regions, c.region);
const scaling = normalizeScaling(input.scaling);
- const runtime = await provisionAppNamespace(
- appId,
- resolveDefaultRivetConnection(),
- {
- namespace: state.namespace,
- cloudNamespace: state.cloudNamespace,
- },
- );
+ // createNamespace: false disables provisioning entirely; unset keeps the
+ // default behavior of giving every app its own stable namespace.
+ const runtime =
+ input.createNamespace === false
+ ? unprovisionedAppNamespace(appId)
+ : await provisionAppNamespace(appId, resolveDefaultRivetConnection(), {
+ namespace: state.namespace,
+ cloudNamespace: state.cloudNamespace,
+ });
state.namespace = runtime.namespace;
state.cloudNamespace = runtime.cloudNamespace ?? null;
state.runnerToken = runtime.runnerToken ?? null;
@@ -896,7 +905,6 @@ function deploymentForRelease(
namespace: release.namespace,
pool: release.runtimePool,
...(state.publicToken ? { token: state.publicToken } : {}),
- regions: [...release.regions],
appActorId: c.actorId,
usesRivetKit: release.usesRivetKit,
};
@@ -1020,8 +1028,7 @@ export function createAppsActors(
artifactHash: built.artifact.hash,
artifactBytes: built.artifact.byteLength,
usesRivetKit: built.artifact.usesRivetKit,
- regions: input.regions,
- scaling: input.scaling,
+ createNamespace: input.createNamespace,
createdAt: Date.now(),
};
const begin = await beginReleasePublishLocked(c, publishInput);
@@ -1103,6 +1110,12 @@ export function createAppsActors(
maxRequestBytes: DEFAULT_MAX_REQUEST_BYTES,
maxResponseBytes: DEFAULT_MAX_RESPONSE_BYTES,
usesRivetKit: release.usesRivetKit,
+ ...(release.usesRivetKit
+ ? {
+ serverlessEndpoint: actorPublicEndpoint(release, state),
+ runtimePool: release.runtimePool,
+ }
+ : {}),
};
},
getArtifactManifest: async (c: AnyActorContext, releaseId: string) => {
diff --git a/packages/dynamic-apps/src/control-plane.ts b/packages/dynamic-apps/src/control-plane.ts
index 58d7555c8..bd214b37a 100644
--- a/packages/dynamic-apps/src/control-plane.ts
+++ b/packages/dynamic-apps/src/control-plane.ts
@@ -304,6 +304,19 @@ export async function provisionAppNamespace(
};
}
+/** Runtime coordinates for an app deployed with createNamespace: false. */
+export function unprovisionedAppNamespace(
+ appId: string,
+ connection = resolveDefaultRivetConnection(),
+): ProvisionedAppNamespace {
+ return {
+ namespace: connection.namespace,
+ endpoint: connection.endpoint,
+ pool: appRunnerPool(appId),
+ controlToken: connection.token,
+ };
+}
+
function serverlessAppCallback(
appActorId: string,
connection: ResolvedRivetConnection,
diff --git a/packages/dynamic-apps/src/default.ts b/packages/dynamic-apps/src/default.ts
index adefa7d39..7281af061 100644
--- a/packages/dynamic-apps/src/default.ts
+++ b/packages/dynamic-apps/src/default.ts
@@ -1,10 +1,19 @@
import { createDynamicApps } from "@rivet-dev/dynamic-apps-core";
-import { createRivetReleaseStore } from "./release-store.js";
+import { getDefaultActorRuntime } from "./actor-runtime.js";
+import {
+ createRivetReleaseStore,
+ type RivetDeployOptions,
+} from "./release-store.js";
+import type { Deployment } from "./types.js";
const releaseStore = createRivetReleaseStore();
-export const defaultDynamicApps = createDynamicApps({
+export const defaultDynamicApps = createDynamicApps<
+ Deployment,
+ RivetDeployOptions
+>({
...releaseStore,
+ serverRuntime: getDefaultActorRuntime(),
logger: {
info: (event) => console.log(JSON.stringify(event)),
error: (event) => console.error(JSON.stringify(event)),
diff --git a/packages/dynamic-apps/src/deploy.ts b/packages/dynamic-apps/src/deploy.ts
index 13eabb5e9..e340fac61 100644
--- a/packages/dynamic-apps/src/deploy.ts
+++ b/packages/dynamic-apps/src/deploy.ts
@@ -37,13 +37,16 @@ export async function deployApp(
): Promise {
// The ordinary path uses core's build + release hooks. An injected structural
// client must keep calling the legacy actor action for declaration compatibility.
- if (!options.client) return defaultDynamicApps.deployApp(input);
+ if (!options.client) {
+ return defaultDynamicApps.deployApp(input, {
+ createNamespace: input.createNamespace,
+ });
+ }
const files = await prepareSource(input);
const prepared: PreparedDeployAppInput = {
appId: input.appId,
files,
- regions: input.regions,
- scaling: input.scaling,
+ createNamespace: input.createNamespace,
};
const result = await deployThroughStableActor(
options.client.dynamicAppsApp,
@@ -57,7 +60,6 @@ export async function deployApp(
namespace: result.namespace,
pool: result.pool,
...(result.token ? { token: result.token } : {}),
- regions: result.regions,
};
}
diff --git a/packages/dynamic-apps/src/release-store.ts b/packages/dynamic-apps/src/release-store.ts
index 55076b4db..721ae64d9 100644
--- a/packages/dynamic-apps/src/release-store.ts
+++ b/packages/dynamic-apps/src/release-store.ts
@@ -1,7 +1,6 @@
import { createHash } from "node:crypto";
import type {
ActiveRelease,
- AppScaling,
PublishReleaseInput,
ReleaseInvalidation,
ReleaseLoadContext,
@@ -12,6 +11,7 @@ import {
DIRECT_RUNTIME_FORMAT,
} from "@rivet-dev/dynamic-apps-core/internal";
import { createClient } from "rivetkit/client";
+import { actorWorkerEnvironment } from "./actor-runtime.js";
import { ensurePrivateAppsRegistry } from "./registry.js";
import type { AppRouteResolution, Deployment } from "./types.js";
@@ -31,8 +31,7 @@ interface BeginReleasePublishInput {
artifactHash: string;
artifactBytes: number;
usesRivetKit: boolean;
- regions?: string[];
- scaling?: AppScaling;
+ createNamespace?: boolean;
createdAt: number;
}
@@ -115,8 +114,15 @@ interface DriverEntry {
unsubscribe?: Unsubscribe;
}
+export interface RivetDeployOptions {
+ createNamespace?: boolean;
+}
+
export interface RivetReleaseStore {
- publishRelease(input: PublishReleaseInput): Promise;
+ publishRelease(
+ input: PublishReleaseInput,
+ options?: RivetDeployOptions,
+ ): Promise;
loadActiveRelease(
appId: string,
context: ReleaseLoadContext,
@@ -137,6 +143,7 @@ export function createRivetReleaseStore(
const publishRelease = async (
input: PublishReleaseInput,
+ options?: RivetDeployOptions,
): Promise => {
await ensurePrivateAppsRegistry();
const group = getClient().dynamicAppsApp;
@@ -161,8 +168,7 @@ export function createRivetReleaseStore(
artifactHash: input.artifact.hash,
artifactBytes: input.artifact.byteLength,
usesRivetKit: input.artifact.usesRivetKit,
- regions: input.regions,
- scaling: input.scaling,
+ createNamespace: options?.createNamespace,
createdAt: input.createdAt,
};
let begin: BeginReleasePublishResult;
@@ -265,10 +271,24 @@ export function createRivetReleaseStore(
byteLength: bytes.byteLength,
usesRivetKit: resolution.usesRivetKit,
},
- regions: [...resolution.regions],
- scaling: { ...resolution.scaling },
maxRequestBytes: resolution.maxRequestBytes,
maxResponseBytes: resolution.maxResponseBytes,
+ ...(resolution.usesRivetKit &&
+ resolution.serverlessEndpoint &&
+ resolution.runtimePool
+ ? {
+ server: {
+ environment: definedEnvironment(
+ actorWorkerEnvironment({
+ endpoint: resolution.serverlessEndpoint,
+ key: `${resolution.release}:${resolution.artifactHash}`,
+ namespace: resolution.namespace,
+ pool: resolution.runtimePool,
+ }),
+ ),
+ },
+ }
+ : {}),
};
};
@@ -321,6 +341,14 @@ export function createRivetReleaseStore(
return { publishRelease, loadActiveRelease, watchActiveRelease };
}
+function definedEnvironment(env: NodeJS.ProcessEnv): Record {
+ return Object.fromEntries(
+ Object.entries(env).filter(
+ (entry): entry is [string, string] => typeof entry[1] === "string",
+ ),
+ );
+}
+
async function connectDriver(
entry: DriverEntry,
group: ReleaseActorGroup,
@@ -399,7 +427,6 @@ function projectDeployment(input: Deployment): Deployment {
namespace: input.namespace,
pool: input.pool,
...(input.token ? { token: input.token } : {}),
- regions: [...input.regions],
};
}
diff --git a/packages/dynamic-apps/src/skills.ts b/packages/dynamic-apps/src/skills.ts
index db5f584a9..111cbd3de 100644
--- a/packages/dynamic-apps/src/skills.ts
+++ b/packages/dynamic-apps/src/skills.ts
@@ -1,5 +1,5 @@
/** Instructions and a complete starter for a buildable TypeScript HTTP app. */
-export const webServerSkill = `Build a Node.js web server in TypeScript. Start from this complete project and modify it for the user's request.
+export const webServerSkill = `Build a Dynamic Apps fetch handler in TypeScript. Start from this complete project and modify it for the user's request.
package.json
~~~json
@@ -10,16 +10,14 @@ package.json
"type": "module",
"main": "dist/index.js",
"scripts": {
- "build": "tsc",
- "start": "node dist/index.js"
+ "build": "tsc"
},
"dependencies": {
- "@hono/node-server": "2.0.11",
- "hono": "4.12.9"
+ "hono": "4.13.5"
},
"devDependencies": {
- "@types/node": "22.19.15",
- "typescript": "5.7.3"
+ "@types/node": "22.20.1",
+ "typescript": "5.9.3"
}
}
~~~
@@ -43,7 +41,6 @@ tsconfig.json
src/index.ts
~~~ts
-import { serve } from "@hono/node-server";
import { Hono } from "hono";
const app = new Hono();
@@ -52,16 +49,13 @@ app.get("/", (context) =>
context.json({ ok: true, message: "Hello from Dynamic Apps" }),
);
-serve({
- fetch: app.fetch,
- port: Number(process.env.PORT ?? 3000),
-});
+export default app;
~~~
-Keep the build script as "tsc" and the entrypoint as dist/index.js. The deployment runs npm run build, so invalid generated TypeScript returns compiler diagnostics that can be used to repair the files.`;
+Export the fetch handler and do not open a port; Dynamic Apps owns the HTTP server. Keep the build script as "tsc" and the entrypoint as dist/index.js. The deployment runs npm run build, so invalid generated TypeScript returns compiler diagnostics that can be used to repair the files.`;
/** Instructions and a complete starter for a TypeScript app with Rivet actors. */
-export const rivetActorsSkill = `Build a Node.js TypeScript web server with Rivet Actors. Start from this complete project and modify the actor state, actions, and HTTP routes for the user's request.
+export const rivetActorsSkill = `Build a Dynamic Apps fetch handler with Rivet Actors. Start from this complete project and modify the actor state, actions, and HTTP routes for the user's request.
package.json
~~~json
@@ -72,17 +66,15 @@ package.json
"type": "module",
"main": "dist/index.js",
"scripts": {
- "build": "tsc",
- "start": "node dist/index.js"
+ "build": "tsc"
},
"dependencies": {
- "@hono/node-server": "2.0.11",
- "hono": "4.12.9",
+ "hono": "4.13.5",
"rivetkit": "2.3.11"
},
"devDependencies": {
- "@types/node": "22.19.15",
- "typescript": "5.7.3"
+ "@types/node": "22.20.1",
+ "typescript": "5.9.3"
}
}
~~~
@@ -106,7 +98,6 @@ tsconfig.json
src/index.ts
~~~ts
-import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { actor, event, setup } from "rivetkit";
@@ -130,16 +121,13 @@ export const registry = setup({ use: { counter } });
const app = new Hono();
app.all("/api/rivet/*", (context) => registry.handler(context.req.raw));
app.get("/", (context) =>
- context.json({ ok: true, message: "Rivet Actors server is running" }),
+ context.json({ ok: true, message: "Rivet Actors app is running" }),
);
-serve({
- fetch: app.fetch,
- port: Number(process.env.PORT ?? 3000),
-});
+export default app;
~~~
-Do not call registry.start(): the Hono server owns the HTTP listener. Keep the build script as "tsc" so deployment failures include TypeScript diagnostics. The example above works without reading external documentation.
+Export the fetch handler and do not call serve() or registry.start(); Dynamic Apps owns the HTTP listener. Keep the build script as "tsc" so deployment failures include TypeScript diagnostics. The example above works without reading external documentation.
Optional reference links:
- Rivet Actors: https://rivet.dev/actors/docs/
diff --git a/packages/dynamic-apps/src/types.ts b/packages/dynamic-apps/src/types.ts
index a29cee11f..cf0754e8c 100644
--- a/packages/dynamic-apps/src/types.ts
+++ b/packages/dynamic-apps/src/types.ts
@@ -1,7 +1,14 @@
-import type { AppScaling, DeployAppInput } from "@rivet-dev/dynamic-apps-core";
+import type { DeployAppInput } from "@rivet-dev/dynamic-apps-core";
import type { DIRECT_ENTRYPOINT } from "@rivet-dev/dynamic-apps-core/internal";
-export type { AppScaling, DeployAppInput };
+export type { DeployAppInput };
+
+/** Legacy actor wire/storage shape. Not part of the public deploy surface. */
+export interface AppScaling {
+ minReplicas?: number;
+ maxReplicas?: number;
+ targetConcurrency?: number;
+}
export interface Deployment {
appId: string;
@@ -12,7 +19,6 @@ export interface Deployment {
pool: string;
/** Publishable token scoped to this application's namespace, when required. */
token?: string;
- regions: string[];
}
export interface AppReleaseInfo {
@@ -29,8 +35,7 @@ export interface AppReleaseInfo {
export interface PreparedDeployAppInput {
appId: string;
files: Record;
- regions?: string[];
- scaling?: AppScaling;
+ createNamespace?: boolean;
}
export interface AppRouteResolution {
@@ -47,4 +52,6 @@ export interface AppRouteResolution {
maxRequestBytes: number;
maxResponseBytes: number;
usesRivetKit: boolean;
+ serverlessEndpoint?: string;
+ runtimePool?: string;
}
diff --git a/packages/dynamic-apps/tests/agentos-inline-spike.test.ts b/packages/dynamic-apps/tests/agentos-inline-spike.test.ts
index 8d7012368..9bb70440e 100644
--- a/packages/dynamic-apps/tests/agentos-inline-spike.test.ts
+++ b/packages/dynamic-apps/tests/agentos-inline-spike.test.ts
@@ -115,7 +115,7 @@ export async function dispatch(request) {
timeoutMs: 5_000,
});
controller.abort();
- await expect(aborted).rejects.toMatchObject({ name: "AbortError" });
+ await expect(aborted).resolves.toMatchObject({ outcome: "cancelled" });
} finally {
await vm?.dispose();
await rm(directory, { recursive: true, force: true });
diff --git a/packages/dynamic-apps/tests/direct.test.ts b/packages/dynamic-apps/tests/direct.test.ts
index a7135750b..cd09fe3f7 100644
--- a/packages/dynamic-apps/tests/direct.test.ts
+++ b/packages/dynamic-apps/tests/direct.test.ts
@@ -120,7 +120,7 @@ describe("retained public surface", () => {
"package.json": '{"type":"module","main":"index.js"}',
"index.js": "export default { fetch() {} }",
},
- regions: ["us-west"],
+ createNamespace: false,
},
{
client: {
@@ -137,7 +137,6 @@ describe("retained public surface", () => {
namespace: "app-demo",
pool: "dynamic-apps-demo",
token: "pk_demo",
- regions: ["us-west"],
appActorId: "actor-1",
usesRivetKit: false,
};
@@ -156,9 +155,10 @@ describe("retained public surface", () => {
"namespace",
"pool",
"token",
- "regions",
]);
- expect(prepared).toMatchObject({ appId: "demo", regions: ["us-west"] });
+ expect(prepared).toMatchObject({ appId: "demo", createNamespace: false });
+ expect(prepared).not.toHaveProperty("regions");
+ expect(prepared).not.toHaveProperty("scaling");
});
test("deploys through an existing stable actor before creating one", async () => {
@@ -281,7 +281,6 @@ function deploymentResult(_input: unknown) {
namespace: "app-demo",
pool: "dynamic-apps-demo",
token: "pk_demo",
- regions: ["default"],
appActorId: "actor-1",
usesRivetKit: false,
};
@@ -865,7 +864,7 @@ export default {
await runtime.dispose();
await artifact.dispose();
}
- }, 5_000);
+ }, 60_000);
test("forwards actor callback bodies without eager buffering", async () => {
let pulls = 0;
@@ -1284,7 +1283,9 @@ http.createServer(async (incoming, outgoing) => {
if (!outgoing.headersSent) outgoing.writeHead(500);
if (!outgoing.writableEnded) outgoing.end("Internal Server Error");
}
-}).listen(port, "0.0.0.0");
+}).listen(port, "0.0.0.0", () => {
+ console.log("DYNAMIC_APPS_SERVER_READY:" + (process.env.DYNAMIC_APPS_READY_NONCE ?? ""));
+});
`,
);
await writeFile(
@@ -1332,8 +1333,6 @@ function fakeStateClient(
byteLength: artifact.bytes.byteLength,
usesRivetKit: false,
},
- regions: ["local"],
- scaling: { minReplicas: 0, maxReplicas: 128, targetConcurrency: 8 },
maxRequestBytes: 1024 * 1024,
maxResponseBytes: 4 * 1024 * 1024,
};
@@ -1365,8 +1364,6 @@ function fakeMultiStateClient(artifacts: Map) {
byteLength: artifact.bytes.byteLength,
usesRivetKit: false,
},
- regions: ["local"],
- scaling: { minReplicas: 0, maxReplicas: 128, targetConcurrency: 8 },
maxRequestBytes: 1024 * 1024,
maxResponseBytes: 4 * 1024 * 1024,
};
diff --git a/packages/dynamic-apps/tests/release-store.test.ts b/packages/dynamic-apps/tests/release-store.test.ts
index b7cebacba..cf18a6f61 100644
--- a/packages/dynamic-apps/tests/release-store.test.ts
+++ b/packages/dynamic-apps/tests/release-store.test.ts
@@ -79,7 +79,6 @@ describe("Rivet release store", () => {
"namespace",
"pool",
"token",
- "regions",
]);
});
@@ -114,7 +113,9 @@ describe("Rivet release store", () => {
scaling: { minReplicas: 0, maxReplicas: 1, targetConcurrency: 1 },
maxRequestBytes: 1024,
maxResponseBytes: 1024,
- usesRivetKit: false,
+ usesRivetKit: true,
+ serverlessEndpoint: "https://demo:runtime-token@example.test",
+ runtimePool: "actor-pool",
};
},
async getArtifactManifest() {
@@ -142,6 +143,14 @@ describe("Rivet release store", () => {
recordTiming: (name) => timings.push(name),
});
expect(release?.artifact.bytes).toEqual(bytes);
+ expect(release?.server?.environment).toMatchObject({
+ RIVET_ENDPOINT: "https://example.test",
+ RIVET_NAMESPACE: "demo",
+ RIVET_POOL: "actor-pool",
+ RIVET_TOKEN: "runtime-token",
+ RIVETKIT_RUNTIME: "wasm",
+ RIVETKIT_RUNTIME_MODE: "serverless",
+ });
expect(timings).toEqual([
"actor-connect",
"actor-resolve",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9005553d9..2d6860e51 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -31,8 +31,8 @@ importers:
benchmarks/dynamic-apps:
dependencies:
'@hono/node-server':
- specifier: ^2.0.11
- version: 2.1.1(hono@4.13.3)
+ specifier: ^2.1.1
+ version: 2.1.1(hono@4.13.5)
'@rivet-dev/agentos-core':
specifier: 0.2.18
version: 0.2.18(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3)
@@ -46,8 +46,8 @@ importers:
specifier: workspace:*
version: link:../../packages/dynamic-apps-core
hono:
- specifier: ^4.12.9
- version: 4.13.3
+ specifier: ^4.13.5
+ version: 4.13.5
rivetkit:
specifier: 2.3.11
version: 2.3.11(better-sqlite3@12.11.1)(ws@8.21.3)
@@ -71,183 +71,186 @@ importers:
examples/apps-ai-builder:
dependencies:
'@ai-sdk/anthropic':
- specifier: ^4.0.19
- version: 4.0.39(zod@4.4.3)
+ specifier: ^4.0.46
+ version: 4.0.46(zod@4.5.4)
'@hono/node-server':
- specifier: ^2.0.11
- version: 2.1.1(hono@4.13.3)
+ specifier: ^2.1.1
+ version: 2.1.1(hono@4.13.5)
'@rivet-dev/dynamic-apps':
specifier: workspace:*
version: link:../../packages/dynamic-apps
ai:
- specifier: ^7.0.37
- version: 7.0.68(zod@4.4.3)
+ specifier: ^7.0.86
+ version: 7.0.86(zod@4.5.4)
hono:
- specifier: ^4.12.9
- version: 4.13.3
+ specifier: ^4.13.5
+ version: 4.13.5
+ zod:
+ specifier: ^4.5.4
+ version: 4.5.4
devDependencies:
'@types/node':
- specifier: ^22.19.15
+ specifier: ^22.20.1
version: 22.20.1
tsx:
- specifier: ^4.20.6
- version: 4.23.12
+ specifier: ^4.23.13
+ version: 4.23.13
typescript:
- specifier: ^5.7.3
+ specifier: ^5.9.3
version: 5.9.3
examples/apps-core-quickstart:
dependencies:
'@hono/node-server':
- specifier: ^2.0.11
- version: 2.1.1(hono@4.13.3)
+ specifier: ^2.1.1
+ version: 2.1.1(hono@4.13.5)
'@rivet-dev/dynamic-apps-core':
specifier: workspace:*
version: link:../../packages/dynamic-apps-core
hono:
- specifier: ^4.12.9
- version: 4.13.3
+ specifier: ^4.13.5
+ version: 4.13.5
devDependencies:
'@types/node':
- specifier: ^22.19.15
+ specifier: ^22.20.1
version: 22.20.1
tsx:
- specifier: ^4.20.6
- version: 4.23.12
+ specifier: ^4.23.13
+ version: 4.23.13
typescript:
- specifier: ^5.7.3
+ specifier: ^5.9.3
version: 5.9.3
examples/apps-hello-world:
dependencies:
'@hono/node-server':
- specifier: ^2.0.11
- version: 2.1.1(hono@4.13.3)
+ specifier: ^2.1.1
+ version: 2.1.1(hono@4.13.5)
'@rivet-dev/dynamic-apps':
specifier: workspace:*
version: link:../../packages/dynamic-apps
hono:
- specifier: ^4.12.9
- version: 4.13.3
+ specifier: ^4.13.5
+ version: 4.13.5
devDependencies:
'@types/node':
- specifier: ^22.19.15
+ specifier: ^22.20.1
version: 22.20.1
tsx:
- specifier: ^4.20.6
- version: 4.23.12
+ specifier: ^4.23.13
+ version: 4.23.13
typescript:
- specifier: ^5.7.3
+ specifier: ^5.9.3
version: 5.9.3
examples/apps-multiplayer:
dependencies:
'@hono/node-server':
- specifier: ^2.0.11
- version: 2.1.1(hono@4.13.3)
+ specifier: ^2.1.1
+ version: 2.1.1(hono@4.13.5)
'@rivet-dev/dynamic-apps':
specifier: workspace:*
version: link:../../packages/dynamic-apps
hono:
- specifier: ^4.12.9
- version: 4.13.3
+ specifier: ^4.13.5
+ version: 4.13.5
rivetkit:
specifier: 2.3.11
version: 2.3.11(better-sqlite3@12.11.1)(ws@8.21.3)
devDependencies:
'@types/node':
- specifier: ^22.19.15
+ specifier: ^22.20.1
version: 22.20.1
tsx:
- specifier: ^4.20.6
- version: 4.23.12
+ specifier: ^4.23.13
+ version: 4.23.13
typescript:
- specifier: ^5.7.3
+ specifier: ^5.9.3
version: 5.9.3
examples/apps-sqlite:
dependencies:
'@hono/node-server':
- specifier: ^2.0.11
- version: 2.1.1(hono@4.13.3)
+ specifier: ^2.1.1
+ version: 2.1.1(hono@4.13.5)
'@rivet-dev/dynamic-apps':
specifier: workspace:*
version: link:../../packages/dynamic-apps
hono:
- specifier: ^4.12.9
- version: 4.13.3
+ specifier: ^4.13.5
+ version: 4.13.5
rivetkit:
specifier: 2.3.11
version: 2.3.11(better-sqlite3@12.11.1)(ws@8.21.3)
devDependencies:
'@types/node':
- specifier: ^22.19.15
+ specifier: ^22.20.1
version: 22.20.1
tsx:
- specifier: ^4.20.6
- version: 4.23.12
+ specifier: ^4.23.13
+ version: 4.23.13
typescript:
- specifier: ^5.7.3
+ specifier: ^5.9.3
version: 5.9.3
examples/apps-static-website:
dependencies:
'@hono/node-server':
- specifier: ^2.0.11
- version: 2.1.1(hono@4.13.3)
+ specifier: ^2.1.1
+ version: 2.1.1(hono@4.13.5)
'@rivet-dev/dynamic-apps':
specifier: workspace:*
version: link:../../packages/dynamic-apps
hono:
- specifier: ^4.12.9
- version: 4.13.3
+ specifier: ^4.13.5
+ version: 4.13.5
devDependencies:
'@types/node':
- specifier: ^22.19.15
+ specifier: ^22.20.1
version: 22.20.1
tsx:
- specifier: ^4.20.6
- version: 4.23.12
+ specifier: ^4.23.13
+ version: 4.23.13
typescript:
- specifier: ^5.7.3
+ specifier: ^5.9.3
version: 5.9.3
examples/apps-workflows:
dependencies:
'@hono/node-server':
- specifier: ^2.0.11
- version: 2.1.1(hono@4.13.3)
+ specifier: ^2.1.1
+ version: 2.1.1(hono@4.13.5)
'@rivet-dev/dynamic-apps':
specifier: workspace:*
version: link:../../packages/dynamic-apps
hono:
- specifier: ^4.12.9
- version: 4.13.3
+ specifier: ^4.13.5
+ version: 4.13.5
rivetkit:
specifier: 2.3.11
version: 2.3.11(better-sqlite3@12.11.1)(ws@8.21.3)
devDependencies:
'@types/node':
- specifier: ^22.19.15
+ specifier: ^22.20.1
version: 22.20.1
tsx:
- specifier: ^4.20.6
- version: 4.23.12
+ specifier: ^4.23.13
+ version: 4.23.13
typescript:
- specifier: ^5.7.3
+ specifier: ^5.9.3
version: 5.9.3
packages/dynamic-apps:
dependencies:
'@rivet-dev/agentos-core':
specifier: 0.2.18
- version: 0.2.18(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3)
+ version: 0.2.18(@modelcontextprotocol/sdk@1.30.0(zod@4.5.4))(ws@8.21.3)
'@rivet-dev/dynamic-apps-core':
specifier: workspace:0.3.0
version: link:../dynamic-apps-core
hono:
- specifier: ^4.7.0
- version: 4.13.3
+ specifier: ^4.13.5
+ version: 4.13.5
rivetkit:
specifier: 2.3.11
version: 2.3.11(better-sqlite3@12.11.1)(ws@8.21.3)
@@ -260,7 +263,7 @@ importers:
version: 22.20.1
tsup:
specifier: ^8.4.0
- version: 8.5.1(postcss@8.5.26)(tsx@4.23.12)(typescript@5.9.3)(yaml@2.9.0)
+ version: 8.5.1(postcss@8.5.26)(tsx@4.23.13)(typescript@5.9.3)(yaml@2.9.0)
typescript:
specifier: ^5.7.3
version: 5.9.3
@@ -270,19 +273,25 @@ importers:
packages/dynamic-apps-builder:
dependencies:
+ '@hono/node-server':
+ specifier: 2.1.1
+ version: 2.1.1(hono@4.13.5)
'@rivetkit/rivetkit-wasm':
specifier: 2.3.11
version: 2.3.11
esbuild-wasm:
specifier: 0.27.4
version: 0.27.4
+ hono:
+ specifier: 4.13.5
+ version: 4.13.5
rivetkit:
specifier: 2.3.11
version: 2.3.11(better-sqlite3@12.11.1)(ws@8.21.3)
devDependencies:
'@rivet-dev/agentos-core':
specifier: 0.2.18
- version: 0.2.18(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3)
+ version: 0.2.18(@modelcontextprotocol/sdk@1.30.0(zod@4.5.4))(ws@8.21.3)
'@rivet-dev/agentos-toolchain':
specifier: 0.2.18
version: 0.2.18
@@ -306,7 +315,7 @@ importers:
version: 0.3.5
'@rivet-dev/agentos-core':
specifier: 0.2.18
- version: 0.2.18(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3)
+ version: 0.2.18(@modelcontextprotocol/sdk@1.30.0(zod@4.5.4))(ws@8.21.3)
'@rivet-dev/agentos-toolchain':
specifier: 0.2.18
version: 0.2.18
@@ -314,15 +323,15 @@ importers:
specifier: workspace:0.3.0
version: link:../dynamic-apps-builder
hono:
- specifier: ^4.7.0
- version: 4.13.3
+ specifier: ^4.13.5
+ version: 4.13.5
devDependencies:
'@types/node':
specifier: ^22.19.15
version: 22.20.1
tsup:
specifier: ^8.4.0
- version: 8.5.1(postcss@8.5.26)(tsx@4.23.12)(typescript@5.9.3)(yaml@2.9.0)
+ version: 8.5.1(postcss@8.5.26)(tsx@4.23.13)(typescript@5.9.3)(yaml@2.9.0)
typescript:
specifier: ^5.7.3
version: 5.9.3
@@ -416,26 +425,26 @@ packages:
'@agentos-software/tar@0.3.5':
resolution: {integrity: sha512-hSf6PY4q1luIomFSDgVHxVDDIPdAVZxdgwrQFEYH4aIE6TXX9qatVmzR36uYLWpnFGAZTR6srREGoTnmOGV4Lg==}
- '@ai-sdk/anthropic@4.0.39':
- resolution: {integrity: sha512-JAMGtYeEuaBzqbsPO4fkho6vQyNoVhsHASM4o59wmJRU6Vh7prjOp490Kmc7YQTY+ioU1/xYzXvWOtxZBup0Xw==}
+ '@ai-sdk/anthropic@4.0.46':
+ resolution: {integrity: sha512-/q/wWLArkavQHeOYdv8kZyJRTuasTRwrzoAnvUaY0sHx/BGDYDWW9ENn7lu/H5iUeYId6NhtYAVv+TlqHpp9Cg==}
engines: {node: '>=22'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
- '@ai-sdk/gateway@4.0.54':
- resolution: {integrity: sha512-x4fAXDqCtYzB/M5vsIQLYcyrzpJuaRgcIwDSw+lpTMMbgH19fU3ds75GSlHNLzfx6Z5yL4Z9+EMr0GJcqVy9QA==}
+ '@ai-sdk/gateway@4.0.70':
+ resolution: {integrity: sha512-0tzAH2vwXOs/kVktAZRS04dATEQJk1hf1QR+VuVfvo9QmW3UPgcjhhJD9QFgP8HZLxkrEGDImwLIQ7sUfQTIsA==}
engines: {node: '>=22'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
- '@ai-sdk/provider-utils@5.0.27':
- resolution: {integrity: sha512-EzAn4pdgG5g0xXtH6lE2zyNmfjDQIDjATkfqzuidEI35g++hh4+07vnjzkT/RmGmIClPZiRj/Q2GMPV2V7mkHw==}
+ '@ai-sdk/provider-utils@5.0.34':
+ resolution: {integrity: sha512-tRBdgRcys/4d8wyQdOdyYScq1AxfMdMd0hIlwolxJKVIbBwXUgClZuQT0VIsz4e7pylY8FE6utYCCZ494UAMJQ==}
engines: {node: '>=22'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
- '@ai-sdk/provider@4.0.7':
- resolution: {integrity: sha512-6or44XprPzKbr8zkmzosowSE0pxkvJcoojBL+mCZvPUt3kvXp3XSNqeVun9golb1acEfSo6yaEBRT18h2VU+1Q==}
+ '@ai-sdk/provider@4.0.9':
+ resolution: {integrity: sha512-XnGXPWiBIfqjsVEud5pOaVneRByJQOu2sYNwlSVJTPCvakdCDkVuYKKfNuStkIpMUYl7JIkBZGBx+B5YfNeVjA==}
engines: {node: '>=22'}
'@anthropic-ai/claude-agent-sdk@0.2.87':
@@ -1824,8 +1833,8 @@ packages:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
- ai@7.0.68:
- resolution: {integrity: sha512-9QuZOT77wzoxxUC0NcueXhCo3HUHA/1pApIJ9VRyE+9/K+3Innkq4kVhrd9aEnwIviJz2Nga063m+UTsPSdOyw==}
+ ai@7.0.86:
+ resolution: {integrity: sha512-11Hovs3BI98tPJiOuA85Be+ktxbZ2QUIqqLJqfHJ55zz4106pjRkEP9OQ95glyjBXPEtTdr6/z4ISsk6G13rvw==}
engines: {node: '>=22'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
@@ -2588,8 +2597,8 @@ packages:
hmac-drbg@1.0.1:
resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==}
- hono@4.13.3:
- resolution: {integrity: sha512-r8AO2mYHoLxSHkgafNeC/BXyb2vWRxD3jem4Ts+ptav8oTG5FIRifAjuJEmZI4bSvvc2ns0GxmIYiZnHqN3mMw==}
+ hono@4.13.5:
+ resolution: {integrity: sha512-O6+/eCYRkzzzy0rPWwKLiGBR1nFuUPZynnwjxN1MBA62NNqbT0wQEzQyK2gSO5yDIDB336sXQleAhOHrzlYyKw==}
engines: {node: '>=16.9.0'}
hosted-git-info@9.0.3:
@@ -3477,6 +3486,11 @@ packages:
engines: {node: '>=18.0.0'}
hasBin: true
+ tsx@4.23.13:
+ resolution: {integrity: sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==}
+ engines: {node: '>=18.0.0'}
+ hasBin: true
+
tty-browserify@0.0.1:
resolution: {integrity: sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==}
@@ -3695,17 +3709,24 @@ packages:
zod@4.4.3:
resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
+ zod@4.5.4:
+ resolution: {integrity: sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA==}
+
snapshots:
'@agentclientprotocol/sdk@0.16.1(zod@4.4.3)':
dependencies:
zod: 4.4.3
+ '@agentclientprotocol/sdk@0.16.1(zod@4.5.4)':
+ dependencies:
+ zod: 4.5.4
+
'@agentos-software/claude-code@0.2.7':
dependencies:
- '@agentclientprotocol/sdk': 0.16.1(zod@4.4.3)
- '@anthropic-ai/claude-agent-sdk': 0.2.87(zod@4.4.3)
- zod: 4.4.3
+ '@agentclientprotocol/sdk': 0.16.1(zod@4.5.4)
+ '@anthropic-ai/claude-agent-sdk': 0.2.87(zod@4.5.4)
+ zod: 4.5.4
transitivePeerDependencies:
- '@cfworker/json-schema'
- supports-color
@@ -3752,43 +3773,56 @@ snapshots:
- ws
- zod
+ '@agentos-software/pi@0.2.7(@modelcontextprotocol/sdk@1.30.0(zod@4.5.4))(ws@8.21.3)(zod@4.4.3)':
+ dependencies:
+ '@agentclientprotocol/sdk': 0.16.1(zod@4.4.3)
+ '@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.30.0(zod@4.5.4))(ws@8.21.3)(zod@4.4.3)
+ '@mariozechner/pi-coding-agent': 0.60.0(@modelcontextprotocol/sdk@1.30.0(zod@4.5.4))(ws@8.21.3)(zod@4.4.3)
+ transitivePeerDependencies:
+ - '@modelcontextprotocol/sdk'
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+ - ws
+ - zod
+
'@agentos-software/sed@0.3.4': {}
'@agentos-software/sh@0.2.18': {}
'@agentos-software/tar@0.3.5': {}
- '@ai-sdk/anthropic@4.0.39(zod@4.4.3)':
+ '@ai-sdk/anthropic@4.0.46(zod@4.5.4)':
dependencies:
- '@ai-sdk/provider': 4.0.7
- '@ai-sdk/provider-utils': 5.0.27(zod@4.4.3)
- zod: 4.4.3
+ '@ai-sdk/provider': 4.0.9
+ '@ai-sdk/provider-utils': 5.0.34(zod@4.5.4)
+ zod: 4.5.4
- '@ai-sdk/gateway@4.0.54(zod@4.4.3)':
+ '@ai-sdk/gateway@4.0.70(zod@4.5.4)':
dependencies:
- '@ai-sdk/provider': 4.0.7
- '@ai-sdk/provider-utils': 5.0.27(zod@4.4.3)
+ '@ai-sdk/provider': 4.0.9
+ '@ai-sdk/provider-utils': 5.0.34(zod@4.5.4)
'@vercel/oidc': 3.2.0
- zod: 4.4.3
+ zod: 4.5.4
- '@ai-sdk/provider-utils@5.0.27(zod@4.4.3)':
+ '@ai-sdk/provider-utils@5.0.34(zod@4.5.4)':
dependencies:
- '@ai-sdk/provider': 4.0.7
+ '@ai-sdk/provider': 4.0.9
'@standard-schema/spec': 1.1.0
'@workflow/serde': 4.1.0
eventsource-parser: 3.1.1
undici: 7.29.0
- zod: 4.4.3
+ zod: 4.5.4
- '@ai-sdk/provider@4.0.7':
+ '@ai-sdk/provider@4.0.9':
dependencies:
json-schema: 0.4.0
- '@anthropic-ai/claude-agent-sdk@0.2.87(zod@4.4.3)':
+ '@anthropic-ai/claude-agent-sdk@0.2.87(zod@4.5.4)':
dependencies:
- '@anthropic-ai/sdk': 0.74.0(zod@4.4.3)
- '@modelcontextprotocol/sdk': 1.30.0(zod@4.4.3)
- zod: 4.4.3
+ '@anthropic-ai/sdk': 0.74.0(zod@4.5.4)
+ '@modelcontextprotocol/sdk': 1.30.0(zod@4.5.4)
+ zod: 4.5.4
optionalDependencies:
'@img/sharp-darwin-arm64': 0.34.5
'@img/sharp-darwin-x64': 0.34.5
@@ -3809,11 +3843,11 @@ snapshots:
optionalDependencies:
zod: 4.4.3
- '@anthropic-ai/sdk@0.74.0(zod@4.4.3)':
+ '@anthropic-ai/sdk@0.74.0(zod@4.5.4)':
dependencies:
json-schema-to-ts: 3.1.1
optionalDependencies:
- zod: 4.4.3
+ zod: 4.5.4
'@asteasolutions/zod-to-openapi@9.1.0(zod@4.4.3)':
dependencies:
@@ -4328,21 +4362,34 @@ snapshots:
- supports-color
- utf-8-validate
- '@hono/node-server@2.1.1(hono@4.13.3)':
+ '@google/genai@1.52.0(@modelcontextprotocol/sdk@1.30.0(zod@4.5.4))':
dependencies:
- hono: 4.13.3
+ google-auth-library: 10.9.1
+ p-retry: 4.6.2
+ protobufjs: 7.6.5
+ ws: 8.21.3
+ optionalDependencies:
+ '@modelcontextprotocol/sdk': 1.30.0(zod@4.5.4)
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
- '@hono/zod-openapi@1.6.0(hono@4.13.3)(zod@4.4.3)':
+ '@hono/node-server@2.1.1(hono@4.13.5)':
+ dependencies:
+ hono: 4.13.5
+
+ '@hono/zod-openapi@1.6.0(hono@4.13.5)(zod@4.4.3)':
dependencies:
'@asteasolutions/zod-to-openapi': 9.1.0(zod@4.4.3)
- '@hono/zod-validator': 0.9.0(hono@4.13.3)(zod@4.4.3)
- hono: 4.13.3
+ '@hono/zod-validator': 0.9.0(hono@4.13.5)(zod@4.4.3)
+ hono: 4.13.5
openapi3-ts: 4.6.1
zod: 4.4.3
- '@hono/zod-validator@0.9.0(hono@4.13.3)(zod@4.4.3)':
+ '@hono/zod-validator@0.9.0(hono@4.13.5)(zod@4.4.3)':
dependencies:
- hono: 4.13.3
+ hono: 4.13.5
zod: 4.4.3
'@img/sharp-darwin-arm64@0.34.5':
@@ -4481,6 +4528,17 @@ snapshots:
- ws
- zod
+ '@mariozechner/pi-agent-core@0.60.0(@modelcontextprotocol/sdk@1.30.0(zod@4.5.4))(ws@8.21.3)(zod@4.4.3)':
+ dependencies:
+ '@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.30.0(zod@4.5.4))(ws@8.21.3)(zod@4.4.3)
+ transitivePeerDependencies:
+ - '@modelcontextprotocol/sdk'
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+ - ws
+ - zod
+
'@mariozechner/pi-ai@0.60.0(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3)(zod@4.4.3)':
dependencies:
'@anthropic-ai/sdk': 0.73.0(zod@4.4.3)
@@ -4504,6 +4562,29 @@ snapshots:
- ws
- zod
+ '@mariozechner/pi-ai@0.60.0(@modelcontextprotocol/sdk@1.30.0(zod@4.5.4))(ws@8.21.3)(zod@4.4.3)':
+ dependencies:
+ '@anthropic-ai/sdk': 0.73.0(zod@4.4.3)
+ '@aws-sdk/client-bedrock-runtime': 3.1113.0
+ '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.30.0(zod@4.5.4))
+ '@mistralai/mistralai': 1.14.1
+ '@sinclair/typebox': 0.34.52
+ ajv: 8.20.0
+ ajv-formats: 3.0.1(ajv@8.20.0)
+ chalk: 5.6.2
+ openai: 6.26.0(ws@8.21.3)(zod@4.4.3)
+ partial-json: 0.1.7
+ proxy-agent: 6.5.0
+ undici: 7.29.0
+ zod-to-json-schema: 3.25.2(zod@4.4.3)
+ transitivePeerDependencies:
+ - '@modelcontextprotocol/sdk'
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+ - ws
+ - zod
+
'@mariozechner/pi-coding-agent@0.60.0(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3)(zod@4.4.3)':
dependencies:
'@mariozechner/jiti': 2.6.5
@@ -4535,6 +4616,37 @@ snapshots:
- ws
- zod
+ '@mariozechner/pi-coding-agent@0.60.0(@modelcontextprotocol/sdk@1.30.0(zod@4.5.4))(ws@8.21.3)(zod@4.4.3)':
+ dependencies:
+ '@mariozechner/jiti': 2.6.5
+ '@mariozechner/pi-agent-core': 0.60.0(@modelcontextprotocol/sdk@1.30.0(zod@4.5.4))(ws@8.21.3)(zod@4.4.3)
+ '@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.30.0(zod@4.5.4))(ws@8.21.3)(zod@4.4.3)
+ '@mariozechner/pi-tui': 0.60.0
+ '@silvia-odwyer/photon-node': 0.3.4
+ chalk: 5.6.2
+ cli-highlight: 2.1.11
+ diff: 8.0.4
+ extract-zip: 2.0.1
+ file-type: 21.3.4
+ glob: 13.0.6
+ hosted-git-info: 9.0.3
+ ignore: 7.0.6
+ marked: 15.0.12
+ minimatch: 10.2.6
+ proper-lockfile: 4.1.2
+ strip-ansi: 7.2.0
+ undici: 7.29.0
+ yaml: 2.9.0
+ optionalDependencies:
+ '@mariozechner/clipboard': 0.3.9
+ transitivePeerDependencies:
+ - '@modelcontextprotocol/sdk'
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+ - ws
+ - zod
+
'@mariozechner/pi-tui@0.60.0':
dependencies:
'@types/mime-types': 2.1.4
@@ -4548,15 +4660,15 @@ snapshots:
'@mistralai/mistralai@1.14.1':
dependencies:
ws: 8.21.3
- zod: 4.4.3
- zod-to-json-schema: 3.25.2(zod@4.4.3)
+ zod: 4.5.4
+ zod-to-json-schema: 3.25.2(zod@4.5.4)
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@modelcontextprotocol/sdk@1.30.0(zod@4.4.3)':
dependencies:
- '@hono/node-server': 2.1.1(hono@4.13.3)
+ '@hono/node-server': 2.1.1(hono@4.13.5)
ajv: 8.20.0
ajv-formats: 3.0.1(ajv@8.20.0)
content-type: 1.0.5
@@ -4566,7 +4678,7 @@ snapshots:
eventsource-parser: 3.1.1
express: 5.2.1
express-rate-limit: 8.6.2(express@5.2.1)
- hono: 4.13.3
+ hono: 4.13.5
jose: 6.2.9
json-schema-typed: 8.0.2
pkce-challenge: 5.0.1
@@ -4575,6 +4687,29 @@ snapshots:
zod-to-json-schema: 3.25.2(zod@4.4.3)
transitivePeerDependencies:
- supports-color
+ optional: true
+
+ '@modelcontextprotocol/sdk@1.30.0(zod@4.5.4)':
+ dependencies:
+ '@hono/node-server': 2.1.1(hono@4.13.5)
+ ajv: 8.20.0
+ ajv-formats: 3.0.1(ajv@8.20.0)
+ content-type: 1.0.5
+ cors: 2.8.6
+ cross-spawn: 7.0.6
+ eventsource: 3.0.7
+ eventsource-parser: 3.1.1
+ express: 5.2.1
+ express-rate-limit: 8.6.2(express@5.2.1)
+ hono: 4.13.5
+ jose: 6.2.9
+ json-schema-typed: 8.0.2
+ pkce-challenge: 5.0.1
+ raw-body: 3.0.2
+ zod: 4.5.4
+ zod-to-json-schema: 3.25.2(zod@4.5.4)
+ transitivePeerDependencies:
+ - supports-color
'@napi-rs/cli@2.18.4': {}
@@ -4654,11 +4789,41 @@ snapshots:
- utf-8-validate
- ws
+ '@rivet-dev/agentos-core@0.2.18(@modelcontextprotocol/sdk@1.30.0(zod@4.5.4))(ws@8.21.3)':
+ dependencies:
+ '@agentclientprotocol/sdk': 0.16.1(zod@4.4.3)
+ '@agentos-software/claude-code': 0.2.7
+ '@agentos-software/codex-cli': 0.3.4
+ '@agentos-software/common': 0.2.18
+ '@agentos-software/manifest': 0.2.18
+ '@agentos-software/opencode': 0.2.7
+ '@agentos-software/pi': 0.2.7(@modelcontextprotocol/sdk@1.30.0(zod@4.5.4))(ws@8.21.3)(zod@4.4.3)
+ '@aws-sdk/client-s3': 3.1113.0
+ '@rivet-dev/agentos-runtime-core': 0.2.18
+ '@rivet-dev/agentos-sidecar': 0.2.18
+ '@rivetkit/bare-ts': 0.6.2
+ '@xterm/headless': 6.0.0
+ better-sqlite3: 12.11.1
+ croner: 10.0.1
+ googleapis: 144.0.0
+ long-timeout: 0.1.1
+ minimatch: 10.2.6
+ zod: 4.4.3
+ zod-to-json-schema: 3.25.2(zod@4.4.3)
+ transitivePeerDependencies:
+ - '@cfworker/json-schema'
+ - '@modelcontextprotocol/sdk'
+ - bufferutil
+ - encoding
+ - supports-color
+ - utf-8-validate
+ - ws
+
'@rivet-dev/agentos-runtime-core@0.2.18':
dependencies:
'@rivet-dev/agentos-runtime-sidecar': 0.2.18
'@rivetkit/bare-ts': 0.6.2
- zod: 4.4.3
+ zod: 4.5.4
'@rivet-dev/agentos-runtime-sidecar-darwin-arm64@0.2.18':
optional: true
@@ -5019,12 +5184,12 @@ snapshots:
agent-base@7.1.4: {}
- ai@7.0.68(zod@4.4.3):
+ ai@7.0.86(zod@4.5.4):
dependencies:
- '@ai-sdk/gateway': 4.0.54(zod@4.4.3)
- '@ai-sdk/provider': 4.0.7
- '@ai-sdk/provider-utils': 5.0.27(zod@4.4.3)
- zod: 4.4.3
+ '@ai-sdk/gateway': 4.0.70(zod@4.5.4)
+ '@ai-sdk/provider': 4.0.9
+ '@ai-sdk/provider-utils': 5.0.34(zod@4.5.4)
+ zod: 4.5.4
ajv-formats@3.0.1(ajv@8.20.0):
optionalDependencies:
@@ -5881,7 +6046,7 @@ snapshots:
minimalistic-assert: 1.0.1
minimalistic-crypto-utils: 1.0.1
- hono@4.13.3: {}
+ hono@4.13.5: {}
hosted-git-info@9.0.3:
dependencies:
@@ -6332,12 +6497,12 @@ snapshots:
possible-typed-array-names@1.1.0: {}
- postcss-load-config@6.0.1(postcss@8.5.26)(tsx@4.23.12)(yaml@2.9.0):
+ postcss-load-config@6.0.1(postcss@8.5.26)(tsx@4.23.13)(yaml@2.9.0):
dependencies:
lilconfig: 3.1.3
optionalDependencies:
postcss: 8.5.26
- tsx: 4.23.12
+ tsx: 4.23.13
yaml: 2.9.0
postcss@8.5.26:
@@ -6501,7 +6666,7 @@ snapshots:
rivetkit@2.3.11(better-sqlite3@12.11.1)(ws@8.21.3):
dependencies:
- '@hono/zod-openapi': 1.6.0(hono@4.13.3)(zod@4.4.3)
+ '@hono/zod-openapi': 1.6.0(hono@4.13.5)(zod@4.4.3)
'@rivet-dev/agent-os-core': 0.1.1
'@rivetkit/bare-ts': 0.6.2
'@rivetkit/engine-cli': 2.3.11
@@ -6514,7 +6679,7 @@ snapshots:
'@rivetkit/workflow-engine': 2.3.11
cbor-x: 1.6.5
drizzle-orm: 0.44.7(better-sqlite3@12.11.1)
- hono: 4.13.3
+ hono: 4.13.5
invariant: 2.2.4
p-retry: 6.2.1
pino: 9.14.0
@@ -6868,7 +7033,7 @@ snapshots:
tslib@2.8.1: {}
- tsup@8.5.1(postcss@8.5.26)(tsx@4.23.12)(typescript@5.9.3)(yaml@2.9.0):
+ tsup@8.5.1(postcss@8.5.26)(tsx@4.23.13)(typescript@5.9.3)(yaml@2.9.0):
dependencies:
bundle-require: 5.1.0(esbuild@0.27.7)
cac: 6.7.14
@@ -6879,7 +7044,7 @@ snapshots:
fix-dts-default-cjs-exports: 1.0.1
joycon: 3.1.1
picocolors: 1.1.1
- postcss-load-config: 6.0.1(postcss@8.5.26)(tsx@4.23.12)(yaml@2.9.0)
+ postcss-load-config: 6.0.1(postcss@8.5.26)(tsx@4.23.13)(yaml@2.9.0)
resolve-from: 5.0.0
rollup: 4.62.4
source-map: 0.7.6
@@ -6902,6 +7067,12 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
+ tsx@4.23.13:
+ dependencies:
+ esbuild: 0.28.2
+ optionalDependencies:
+ fsevents: 2.3.3
+
tty-browserify@0.0.1: {}
tunnel-agent@0.6.0:
@@ -7092,4 +7263,10 @@ snapshots:
dependencies:
zod: 4.4.3
+ zod-to-json-schema@3.25.2(zod@4.5.4):
+ dependencies:
+ zod: 4.5.4
+
zod@4.4.3: {}
+
+ zod@4.5.4: {}
diff --git a/scripts/test-core-quickstart.mjs b/scripts/test-core-quickstart.mjs
index b24868b10..82c6e8506 100644
--- a/scripts/test-core-quickstart.mjs
+++ b/scripts/test-core-quickstart.mjs
@@ -17,13 +17,7 @@ const port = await new Promise((resolve, reject) => {
const child = spawn(
process.execPath,
- [
- "--import",
- "tsx",
- "examples/apps-core-quickstart/src/server.ts",
- "--host",
- "0.0.0.0",
- ],
+ ["--import", "tsx", "examples/apps-core-quickstart/src/server.ts"],
{
stdio: ["ignore", "pipe", "inherit"],
env: { ...process.env, PORT: String(port) },
diff --git a/scripts/test-packed.mjs b/scripts/test-packed.mjs
index be3f4f0b8..0b409ea1d 100644
--- a/scripts/test-packed.mjs
+++ b/scripts/test-packed.mjs
@@ -226,8 +226,6 @@ const dynamicApps = core.createDynamicApps({
bytes: new Uint8Array(input.artifact.bytes),
hash: createHash("sha256").update(input.artifact.bytes).digest("hex"),
},
- regions: ["local"],
- scaling: { minReplicas: 0, maxReplicas: 1, targetConcurrency: 1 },
maxRequestBytes: 1024 * 1024,
maxResponseBytes: 4 * 1024 * 1024,
};
diff --git a/specs/agentos-inline-runtime-and-logging.md b/specs/agentos-inline-runtime-and-logging.md
index af8597232..52bc325be 100644
--- a/specs/agentos-inline-runtime-and-logging.md
+++ b/specs/agentos-inline-runtime-and-logging.md
@@ -7,9 +7,10 @@ Public API baseline: `packages/dynamic-apps/API_CONTRACT.md`
## Decision
Remove `isolated-vm` completely. Ordinary Dynamic Apps HTTP requests execute
-through the agentOS library's headless JavaScript API in the Compute process;
-they must not start an execution actor, guest process, HTTP listener, or nested
-Node server.
+through agentOS. Apps without RivetKit use the library's headless JavaScript
+API. Actor-enabled apps use one cached guest process and platform-owned HTTP
+listener for both ordinary HTTP and Rivet callbacks; the application only
+exports a fetch handler.
Keep the existing deployment implementation and durable `dynamicAppsApp`
state actor. It remains responsible for building releases in an agentOS build
@@ -39,6 +40,11 @@ warm direct request
-> ephemeral evaluation or leased retained context
-> direct fetch result
+actor-enabled ordinary HTTP
+ -> appsRouter
+ -> cached AgentOS VM and guest process
+ -> agentOS native VM HTTP stream
+
app-defined actor request
-> Rivet gateway
-> authenticated Dynamic Apps callback
@@ -47,9 +53,9 @@ app-defined actor request
-> agentOS native VM HTTP stream
```
-App-defined actors use agentOS as the mandatory hostile-code boundary. The app
-listens on the supplied `PORT`, and Dynamic Apps forwards the real response head
-and chunks with agentOS's native VM HTTP stream. The init packet is not mocked:
+App-defined actors use agentOS as the mandatory hostile-code boundary. Dynamic
+Apps starts the generated listener on the supplied `PORT` and forwards the real
+response head and chunks with agentOS's native VM HTTP stream. The init packet is not mocked:
Engine uses its encoded runner ID and protocol version. Actor traffic travels
over RivetKit's outbound WebSocket; the SSE stream carries init, keepalive, and
connection lifetime only.
@@ -59,8 +65,8 @@ connection lifetime only.
- Do not change `deployApp()` inputs, results, namespace behavior, build
rollback, release schema, artifact chunking, or state-actor identity.
- Do not reintroduce scaler or replica execution actors for ordinary HTTP.
-- Do not boot `node /app/main.mjs`, poll a readiness URL, or call `vm.fetch()`
- for direct request execution.
+- Do not boot a guest process for apps without RivetKit and do not poll a
+ readiness URL for any app.
- Do not expose the agentOS instance, actor definitions, or cache controls as
public JavaScript APIs.
- Do not promise durable log delivery. The hook is an in-process emission
@@ -218,7 +224,8 @@ within the existing size limit, and returns the response envelope as the
evaluation value.
Do not pass a JSON string through a custom host reference. Do not define custom
-Web API shims. Do not expose an HTTP listener inside the guest.
+Web API shims. Only the generated actor-enabled bootstrap may expose an HTTP
+listener inside the guest.
### Execution modes
@@ -286,9 +293,9 @@ The direct release must be a Node-targeted ESM bundle, not a browser IIFE.
- Continue rejecting native `.node` addons.
- Delete the Dynamic Apps RivetKit stub entirely.
- For an app importing RivetKit, build the direct bundle with the real RivetKit
- runtime. The app mounts `registry.handler()` in its exported fetch router. In
- serverless mode it calls `serve()` on `PORT` and awaits the listening
- callback; Dynamic Apps starts the actor entrypoint as a normal Node program.
+ runtime. The app mounts `registry.handler()` in its exported fetch router and
+ never opens a listener. Dynamic Apps wraps that fetch export in its generated
+ actor entrypoint, starts the listener, and awaits the listening callback.
- Build the separate actor entrypoint with RivetKit and its WASM asset bundled
for execution inside AgentOS.
- Continue validating both bundles before activating a release.
@@ -566,7 +573,7 @@ Replace isolated-VM-specific tests with:
- timeout/abort and poisoned-context eviction;
- release invalidation and concurrent drain;
- bounded VM/context cache and disposal;
-- no guest process or HTTP listener; and
+- no guest process or HTTP listener for apps without actors; and
- real RivetKit imports in a direct actor-enabled release.
#### `packages/dynamic-apps/tests/logging.test.ts` (new)
@@ -637,8 +644,9 @@ Do not replace agentOS with another direct V8 integration to meet the target.
- `isolated-vm` is absent from package manifests, the lockfile, runtime source,
Docker commands, and active public documentation.
-- Ordinary HTTP executes only through the agentOS headless JavaScript API.
-- No direct request starts a guest process or HTTP listener.
+- Ordinary HTTP executes only through agentOS. Actor-enabled ordinary requests
+ share the generated server runtime; other apps use the headless JavaScript API.
+- No application source opens its own HTTP listener.
- Node APIs work inside the agentOS sandbox.
- Both ephemeral and pooled modes preserve clean-request semantics.
- App-defined RivetKit actors still pass deployment, SQLite, state, action,
diff --git a/tests/e2e/dynamic-apps/src/sanity.ts b/tests/e2e/dynamic-apps/src/sanity.ts
index 1e3ef8172..b830271f6 100644
--- a/tests/e2e/dynamic-apps/src/sanity.ts
+++ b/tests/e2e/dynamic-apps/src/sanity.ts
@@ -22,14 +22,12 @@ const deployment = await deployApp({
type: "module",
main: "index.js",
dependencies: {
- "@hono/node-server": "2.1.1",
- hono: "4.13.3",
+ hono: "4.13.5",
rivetkit: "2.3.11",
},
}),
"index.js": `
import { Hono } from "hono";
-import { serve } from "@hono/node-server";
import { actor, setup } from "rivetkit";
import { db } from "rivetkit/db";
const counter = actor({
@@ -48,15 +46,6 @@ const registry = setup({ use: { counter } });
const app = new Hono();
app.all("/api/rivet/*", (c) => registry.handler(c.req.raw));
app.all("*", () => Response.json({ ok: true, path: "direct" }));
-if (process.env.RIVETKIT_RUNTIME_MODE === "serverless") {
- await new Promise((resolve, reject) => {
- const server = serve(
- { fetch: app.fetch, port: Number(process.env.PORT), hostname: "0.0.0.0" },
- resolve,
- );
- server.once("error", reject);
- });
-}
export default app;
`,
},
diff --git a/tests/e2e/dynamic-apps/src/verify.ts b/tests/e2e/dynamic-apps/src/verify.ts
index 8ceb0b62e..31c871dbd 100644
--- a/tests/e2e/dynamic-apps/src/verify.ts
+++ b/tests/e2e/dynamic-apps/src/verify.ts
@@ -149,14 +149,12 @@ async function deployActorFixture() {
type: "module",
main: "index.js",
dependencies: {
- "@hono/node-server": "2.1.1",
- hono: "4.13.3",
+ hono: "4.13.5",
rivetkit: "2.3.11",
},
}),
"index.js": `
import { Hono } from "hono";
-import { serve } from "@hono/node-server";
import { actor, event, setup } from "rivetkit";
const counter = actor({
@@ -178,23 +176,9 @@ const registry = setup({ use: { counter } });
const app = new Hono();
app.all("/api/rivet/*", (c) => registry.handler(c.req.raw));
app.all("*", () => Response.json({ ok: true, workload: "actor-and-direct-http" }));
-if (process.env.RIVETKIT_RUNTIME_MODE === "serverless") {
- await new Promise((resolve, reject) => {
- const server = serve(
- { fetch: app.fetch, port: Number(process.env.PORT), hostname: "0.0.0.0" },
- resolve,
- );
- server.once("error", reject);
- });
-}
export default app;
`,
},
- scaling: {
- minReplicas: 0,
- maxReplicas: 4,
- targetConcurrency: 8,
- },
});
}