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
4 changes: 2 additions & 2 deletions benchmarks/dynamic-apps/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
16 changes: 1 addition & 15 deletions benchmarks/dynamic-apps/src/fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -42,12 +41,6 @@ export default {
};
`,
},
scaling: {
minReplicas: 1,
maxReplicas: 1,
targetConcurrency: 128,
},
...(region ? { regions: [region] } : {}),
},
client ? { client } : undefined,
);
Expand All @@ -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,
Expand All @@ -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",
},
}),
Expand Down Expand Up @@ -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,
);
Expand Down
2 changes: 0 additions & 2 deletions benchmarks/dynamic-apps/src/runtime-stress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down
29 changes: 29 additions & 0 deletions docs/content/docs/_architecture-diagram.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<svg viewBox="0 0 680 146" role="img" aria-label="An HTTP request reaches your Node.js server. Your middleware runs first, then the Dynamic Apps router sends the request to the fetch handler in that app's agentOS VM." style="width:100%;height:auto;max-width:680px;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>
</defs>

<rect x="8" y="50" width="114" height="60" rx="12" fill="#ffffff" stroke="#1b1916" stroke-width="1.5" />
<text x="65" y="76" text-anchor="middle" font-family="var(--sl-font)" font-size="14" font-weight="600" fill="#1b1916">Request</text>
<text x="65" y="95" text-anchor="middle" font-family="var(--sl-font)" font-size="9.5" fill="#56524a">Agent · Browser · API</text>

<line x1="125" y1="80" x2="149" y2="80" stroke="#1b1916" stroke-width="1.5" marker-end="url(#apps-arrow)" />

<rect x="152" y="16" width="520" height="114" rx="14" fill="#faf8f3" stroke="#1b1916" stroke-width="1.5" />
<text x="172" y="40" font-family="var(--sl-font)" font-size="13" font-weight="600" fill="#1b1916">Your Node.js server</text>

<rect x="172" y="50" width="140" height="60" rx="8" fill="#ffffff" stroke="#1b1916" stroke-width="1.2" />
<text x="242" y="84" text-anchor="middle" font-family="var(--sl-font)" font-size="12" font-weight="600" fill="#1b1916">Middleware</text>

<line x1="315" y1="80" x2="339" y2="80" stroke="#1b1916" stroke-width="1.5" marker-end="url(#apps-arrow)" />

<rect x="342" y="50" width="140" height="60" rx="8" fill="#ffffff" stroke="#1b1916" stroke-width="1.2" />
<text x="412" y="84" text-anchor="middle" font-family="var(--sl-font)" font-size="12" font-weight="600" fill="#1b1916">Dynamic Apps router</text>

<line x1="485" y1="80" x2="509" y2="80" stroke="#1b1916" stroke-width="1.5" marker-end="url(#apps-arrow)" />

<rect x="512" y="50" width="140" height="60" rx="8" fill="#ffffff" stroke="#1b1916" stroke-width="1.2" />
<text x="582" y="84" text-anchor="middle" font-family="var(--sl-font)" font-size="12" font-weight="600" fill="#1b1916">agentOS VM</text>
</svg>
46 changes: 46 additions & 0 deletions docs/content/docs/architecture.mdx
Original file line number Diff line number Diff line change
@@ -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.

<ArchitectureDiagram />

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.

<CodeSnippet file="examples/apps-hello-world/src/server.ts" title="src/server.ts" />

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.

<Note>
agentOS provides the filesystem, process, environment, and network permission
boundary for direct requests and app-defined actor workers.
</Note>
30 changes: 30 additions & 0 deletions docs/content/docs/backends.mdx
Original file line number Diff line number Diff line change
@@ -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";

<ExampleLinkBar href="https://github.com/rivet-dev/dynamic-apps/tree/main/examples/apps-hello-world" />

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

<CodeGroup>
<CodeSnippet file="examples/apps-hello-world/fixtures/app/src/index.ts" title="src/index.ts" />
<CodeSnippet file="examples/apps-hello-world/fixtures/app/package.json" title="package.json" />
</CodeGroup>

## Deploy and route

Deploy the directory and every route the app defines is served under
`/apps/:appId` on your routing server:

<CodeSnippet file="examples/apps-hello-world/src/server.ts" title="src/server.ts" />

See [Routing](/dynamic-apps/docs/routing) for how requests reach the app and
[Deploy](/dynamic-apps/docs/deploy) for builds and releases.
42 changes: 42 additions & 0 deletions docs/content/docs/connect.mdx
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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
---

Expand Down Expand Up @@ -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).
29 changes: 29 additions & 0 deletions docs/content/docs/customize-vm.mdx
Original file line number Diff line number Diff line change
@@ -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
87 changes: 34 additions & 53 deletions docs/content/docs/deploy.mdx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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.
Loading
Loading