Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,16 @@ WORKDIR /app

COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY packages/dynamic-apps/package.json packages/dynamic-apps/package.json
COPY packages/dynamic-apps-core/package.json packages/dynamic-apps-core/package.json
COPY packages/dynamic-apps-builder/package.json packages/dynamic-apps-builder/package.json
COPY benchmarks/dynamic-apps/package.json benchmarks/dynamic-apps/package.json
COPY examples/apps-ai-builder/package.json examples/apps-ai-builder/package.json
COPY examples/apps-hello-world/package.json examples/apps-hello-world/package.json
COPY examples/apps-core-quickstart/package.json examples/apps-core-quickstart/package.json
COPY examples/apps-multiplayer/package.json examples/apps-multiplayer/package.json
COPY examples/apps-sqlite/package.json examples/apps-sqlite/package.json
COPY examples/apps-static-website/package.json examples/apps-static-website/package.json
COPY examples/apps-workflows/package.json examples/apps-workflows/package.json
COPY tests/e2e/dynamic-apps/package.json tests/e2e/dynamic-apps/package.json
RUN pnpm install --frozen-lockfile

Expand Down
20 changes: 10 additions & 10 deletions benchmarks/dynamic-apps/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,20 @@
"private": true,
"type": "module",
"scripts": {
"benchmark:local": "node --import tsx src/local.ts --host 0.0.0.0",
"benchmark:cloud-stress": "node --import tsx src/cloud-stress.ts",
"benchmark:stress": "node --expose-gc --import tsx src/runtime-stress.ts",
"benchmark:suite": "node --import tsx src/suite.ts",
"deploy:fixture": "node --import tsx src/deploy-fixture.ts",
"load": "node --import tsx src/load.ts",
"start": "node --import tsx src/server.ts --host 0.0.0.0",
"benchmark:local": "npx tsx src/local.ts --host 0.0.0.0",
"benchmark:cloud-stress": "npx tsx src/cloud-stress.ts",
"benchmark:stress": "npx tsx --expose-gc src/runtime-stress.ts",
"benchmark:suite": "npx tsx src/suite.ts",
"deploy:fixture": "npx tsx src/deploy-fixture.ts",
"load": "npx tsx src/load.ts",
"start": "npx tsx src/server.ts --host 0.0.0.0",
"check-types": "tsc --noEmit",
"test": "node --import tsx --test src/load.test.ts"
"test": "npx tsx --test src/load.test.ts"
},
"dependencies": {
"@hono/node-server": "^2.0.11",
"@rivet-dev/agentos-core": "0.2.15",
"@rivet-dev/agentos-toolchain": "0.2.15",
"@rivet-dev/agentos-core": "0.2.16-rc.3",
"@rivet-dev/agentos-toolchain": "0.2.16-rc.3",
"@rivet-dev/dynamic-apps": "workspace:*",
"@rivet-dev/dynamic-apps-core": "workspace:*",
"hono": "^4.12.9",
Expand Down
8 changes: 4 additions & 4 deletions benchmarks/dynamic-apps/src/deploy-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from "./fixture.js";

const actorClient = createClient() as unknown as {
agentOSAppsApp: {
dynamicAppsApp: {
getOrCreate(key: string[]): ActorHandle;
getForId(actorId: string): ActorHandle;
};
Expand All @@ -16,10 +16,10 @@ interface ActorHandle {
}
const actorId = process.env.BENCH_APP_ACTOR_ID;
const actor = actorId
? actorClient.agentOSAppsApp.getForId(actorId)
: actorClient.agentOSAppsApp.getOrCreate([BENCHMARK_APP_ID]);
? actorClient.dynamicAppsApp.getForId(actorId)
: actorClient.dynamicAppsApp.getOrCreate([BENCHMARK_APP_ID]);
const deploymentClient = {
agentOSAppsApp: { getOrCreate: () => actor },
dynamicAppsApp: { getOrCreate: () => actor },
} as unknown as BenchmarkDeploymentClient;
const deployment = await deployBenchmarkFixture(deploymentClient);
const resolvedActorId = await actor.resolve();
Expand Down
19 changes: 7 additions & 12 deletions benchmarks/dynamic-apps/src/edge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ interface AppHandle {
}

interface StateClient {
agentOSAppsApp: {
dynamicAppsApp: {
getOrCreate(key: string[]): AppHandle;
};
}
Expand Down Expand Up @@ -84,7 +84,7 @@ export function createBenchmarkApplication(): Hono {
| undefined;
let actorApplicationClient: ActorApplicationClient | undefined;
const getActorApplicationDeployment = () => {
actorApplicationDeployment ??= deployActorBenchmarkFixture(client)
actorApplicationDeployment ??= deployActorBenchmarkFixture()
.then((deployment) => {
actorApplicationResolvedDeployment = deployment;
return deployment;
Expand All @@ -101,12 +101,7 @@ export function createBenchmarkApplication(): Hono {
) as unknown as ActorApplicationClient;
return actorApplicationClient;
};
const privateRegistry = (request: Request) => {
const headers = new Headers(request.headers);
headers.set("x-agentos-app-registry-dispatch", "1");
return appsRouter.fetch(new Request(request, { headers }));
};
app.all("/api/rivet/*", (c) => privateRegistry(c.req.raw));
app.all("/api/rivet/*", (c) => appsRouter.fetch(c.req.raw));

app.all("/bench/noop", () => {
const startedAt = performance.now();
Expand Down Expand Up @@ -148,7 +143,7 @@ export function createBenchmarkApplication(): Hono {

app.all("/bench/actor/resolve", async () => {
const startedAt = performance.now();
const handle = client.agentOSAppsApp.getOrCreate([BENCHMARK_APP_ID]);
const handle = client.dynamicAppsApp.getOrCreate([BENCHMARK_APP_ID]);
const actorId = await handle.resolve();
const response = Response.json({ ok: true, actorId });
response.headers.set(
Expand All @@ -159,7 +154,7 @@ export function createBenchmarkApplication(): Hono {
});
app.all("/bench/actor/action", async () => {
const startedAt = performance.now();
const resolution = await client.agentOSAppsApp
const resolution = await client.dynamicAppsApp
.getOrCreate([BENCHMARK_APP_ID])
.resolveDeployment();
const response = Response.json({ ok: true, resolution });
Expand Down Expand Up @@ -245,7 +240,7 @@ export function createBenchmarkApplication(): Hono {
});
app.get("/bench/setup", async () => {
try {
const deployment = await client.agentOSAppsApp
const deployment = await client.dynamicAppsApp
.getOrCreate([BENCHMARK_APP_ID])
.resolveDeployment();
return Response.json({ status: "ready", deployment });
Expand All @@ -254,7 +249,7 @@ export function createBenchmarkApplication(): Hono {
typeof error === "object" && error !== null && "code" in error
? String(error.code)
: undefined;
if (code === "agentos_apps_not_deployed") {
if (code === "dynamic_apps_not_deployed") {
return Response.json({ status: "not-started" }, { status: 404 });
}
throw error;
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/dynamic-apps/src/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export async function runLoadTest(
{
signal: AbortSignal.timeout(config.timeoutMs),
headers: {
"user-agent": "agentos-apps-load-test",
"user-agent": "dynamic-apps-load-test",
...(config.token ? { "x-rivet-token": config.token } : {}),
},
},
Expand Down
8 changes: 4 additions & 4 deletions benchmarks/dynamic-apps/src/runtime-stress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ class FakeStatePlane {
}

readonly client = {
agentOSAppsApp: {
dynamicAppsApp: {
getOrCreate: (key: string[]) => this.#handle(key[0] ?? ""),
},
};
Expand Down Expand Up @@ -641,7 +641,7 @@ async function admissionStress(
.then(
() => "ok" as const,
(error: unknown) =>
isErrorCode(error, "agentos_apps_no_capacity")
isErrorCode(error, "dynamic_apps_no_capacity")
? ("rejected" as const)
: Promise.reject(error),
),
Expand Down Expand Up @@ -841,7 +841,7 @@ async function actorChurnStress(
await response.arrayBuffer();
completed += 1;
} catch (error) {
if (!isErrorCode(error, "agentos_apps_no_capacity")) throw error;
if (!isErrorCode(error, "dynamic_apps_no_capacity")) throw error;
rejected += 1;
}
});
Expand Down Expand Up @@ -1204,7 +1204,7 @@ async function actorMemoryStress(
assert.equal(Number(await response.text()), allocationBytes);
completed += 1;
} catch (error) {
if (!isErrorCode(error, "agentos_apps_no_capacity")) throw error;
if (!isErrorCode(error, "dynamic_apps_no_capacity")) throw error;
rejected += 1;
}
});
Expand Down
2 changes: 0 additions & 2 deletions docs/content/docs/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ title: "Authentication"
description: "Authenticate requests to deployed Dynamic Apps with Hono middleware, route guards, and application-level authorization before handlers run."
---

## Authentication

Use normal Hono middleware to authenticate requests before they reach deployed
apps:

Expand Down
11 changes: 8 additions & 3 deletions docs/content/docs/deploy.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ 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. Static-only directories and native addons are not supported in this
release candidate.
available. Directories that contain only static files are rejected; see
[Static Websites](/dynamic-apps/docs/static-websites). Native addons are not
supported.

## Build repair and rollback

Expand All @@ -58,6 +59,10 @@ 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.
Expand Down Expand Up @@ -88,7 +93,7 @@ await deployApp({
| Option | Default | Meaning |
| --- | --- | --- |
| `regions` | State actor's current region | Stored compatibility metadata; it does not move direct HTTP execution |
| `createNamespace` | | Deprecated compatibility option; every app always receives its own stable namespace |
| `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 |
Expand Down
92 changes: 68 additions & 24 deletions docs/content/docs/index.mdx
Original file line number Diff line number Diff line change
@@ -1,45 +1,89 @@
---
title: "Dynamic Apps"
description: "Build user-generated HTTP apps and serve them through bounded agentOS VMs."
description: "Deploy user-generated applications in isolated agentOS VMs with SQLite, workflows, multiplayer, and static sites out of the box."
skill: true
---

Dynamic Apps builds user-generated HTTP applications in a sandboxed deployment
VM and serves ordinary HTTP through agentOS in your own server process. 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.
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.

<Note>
Dynamic Apps is in preview and its API is subject to change.
</Note>

<CardGroup>
<Card title="Quickstart" href="/dynamic-apps/docs/quickstart">
Start the host server, deploy a generated app, and visit it.
</Card>
<Card title="Deploying Apps" href="/dynamic-apps/docs/deploy">
Deploy files or a directory, repair build errors, and configure apps.
</Card>
<Card title="Static Websites" href="/dynamic-apps/docs/static-websites">
Serve an HTML, CSS, and JavaScript site.
</Card>
<Card title="SQLite" href="/dynamic-apps/docs/sqlite">
Store durable relational data in an actor-owned database.
</Card>
<Card title="Multiplayer" href="/dynamic-apps/docs/multiplayer">
Share realtime state between every connected client.
</Card>
<Card title="Workflows" href="/dynamic-apps/docs/workflows">
Run durable multi-step jobs that sleep and resume.
</Card>
</CardGroup>

## 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.
**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.

<svg viewBox="0 0 620 190" role="img" aria-label="An HTTP request reaches your Hono server, where appsRouter executes it inside an agentOS VM serving the user's app." style="width:100%;height:auto;max-width:620px;display:block;margin:2.5rem auto 1rem;">
<defs>
<marker id="apps-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#1b1916" />
</marker>
<symbol id="apps-os" viewBox="0 0 100 100">
<rect x="8" y="8" width="84" height="84" rx="26" fill="none" stroke="#1b1916" stroke-width="8" />
<text x="50" y="50" text-anchor="middle" dominant-baseline="central" font-family="var(--sl-font)" font-weight="700" font-size="38" fill="#1b1916">OS</text>
</symbol>
</defs>

<rect x="12" y="66" width="126" height="58" rx="12" fill="#ffffff" stroke="#1b1916" stroke-width="1.5" />
<text x="75" y="91" text-anchor="middle" font-family="var(--sl-font)" font-size="14" font-weight="600" fill="#1b1916">Request</text>
<text x="75" y="110" text-anchor="middle" font-family="var(--sl-font)" font-size="10.5" fill="#56524a">Agent · Browser · API</text>

Deployment and request serving are deliberately separate:
<line x1="140" y1="95" x2="184" y2="95" stroke="#1b1916" stroke-width="1.5" marker-end="url(#apps-arrow)" />

```text
deployApp -> Dynamic Apps Core -> agentOS build VM -> publish release
-> Rivet actor (default adapter)
<rect x="190" y="42" width="174" height="106" rx="14" fill="#faf8f3" stroke="#1b1916" stroke-width="1.5" />
<text x="206" y="65" font-family="var(--sl-font)" font-size="13" font-weight="600" fill="#1b1916">Your Hono server</text>
<rect x="206" y="78" width="142" height="50" rx="8" fill="#ffffff" stroke="#1b1916" stroke-width="1.2" />
<text x="277" y="99" text-anchor="middle" font-family="var(--sl-font)" font-size="12" font-weight="600" fill="#1b1916">appsRouter</text>
<text x="277" y="116" text-anchor="middle" font-family="var(--sl-font)" font-size="10" fill="#56524a">auth · route</text>

first HTTP request -> watch + load active release -> verified artifact -> cached agentOS VM
warm HTTP request -> headless JavaScript evaluation -> response (zero storage/actor calls)
```
<line x1="366" y1="95" x2="410" y2="95" stroke="#1b1916" stroke-width="1.5" marker-end="url(#apps-arrow)" />

The Rivet actor is the default storage and control plane, not a serving hop for
warm requests.
<rect x="416" y="42" width="192" height="106" rx="14" fill="#faf8f3" stroke="#1b1916" stroke-width="1.5" />
<text x="432" y="65" font-family="var(--sl-font)" font-size="13" font-weight="600" fill="#1b1916">agentOS VM</text>
<rect x="432" y="78" width="160" height="50" rx="8" fill="#ffffff" stroke="#1b1916" stroke-width="1.2" />
<use href="#apps-os" x="444" y="88" width="30" height="30" />
<text x="485" y="99" font-family="var(--sl-font)" font-size="11.5" font-weight="600" fill="#1b1916">User's app</text>
<text x="485" y="116" font-family="var(--sl-font)" font-size="10" fill="#56524a">serves the response</text>
</svg>

`appsRouter` executes each request in a clean JavaScript context. The default
mode keeps a small bounded pool of retained agentOS contexts, resetting and
reinitializing each one after use. Ephemeral mode asks agentOS for a fresh
context on every request while reusing the immutable release 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 a per-app Rivet actor;
`@rivet-dev/dynamic-apps-core` lets you supply another store.

An application may additionally export a RivetKit registry. Those app-defined
actors use normal Rivet routing for durable state, actions, events, and
connections, while the same application's ordinary HTTP handler still runs
through headless agentOS evaluation.
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.

<Note>
agentOS provides the filesystem, process, environment, and network permission
Expand Down
19 changes: 19 additions & 0 deletions docs/content/docs/multiplayer.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
title: "Multiplayer"
description: "Share realtime state between every client connected to an app."
---

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

The server is the generated app. The client shows how your own system connects
to the actors inside it.

<CodeGroup>
<CodeSnippet file="examples/apps-multiplayer/fixtures/app/src/index.ts" title="Server" />
<CodeSnippet file="examples/apps-multiplayer/src/client.ts" region="client" title="Client" />
</CodeGroup>

Dynamic Apps does not wrap RivetKit's action, event, or connection APIs. See
[Events](https://rivet.dev/actors/docs/events/) and
[Connections](https://rivet.dev/actors/docs/connections/) in Rivet Actors.
4 changes: 2 additions & 2 deletions docs/content/docs/quickstart-core.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: "Core Quick Start"
title: "Core Quickstart"
description: "Build and serve a Dynamic App with a development-only in-memory release store."
skill: true
---
Expand Down Expand Up @@ -59,7 +59,7 @@ disposes the instance during shutdown.
Pass the listening host on the command line:

```sh
node --import tsx src/server.ts --host 0.0.0.0
npx tsx src/server.ts --host 0.0.0.0
```

</Step>
Expand Down
4 changes: 2 additions & 2 deletions docs/content/docs/quickstart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ served by `appsRouter` through a cached agentOS VM.
Run the server normally:

```sh
node --import tsx src/server.ts
npx tsx src/server.ts
```

</Step>
Expand All @@ -46,7 +46,7 @@ agent, an upload endpoint, or another trusted control-plane process.
<CodeSnippet file="examples/apps-hello-world/src/deploy.ts" title="src/deploy.ts" />

```sh
node --import tsx src/deploy.ts
npx tsx src/deploy.ts
```

</Step>
Expand Down
Loading
Loading