diff --git a/frontend/packages/shared-data/src/deploy.ts b/frontend/packages/shared-data/src/deploy.ts index e44ba180a3..afd9adfc74 100644 --- a/frontend/packages/shared-data/src/deploy.ts +++ b/frontend/packages/shared-data/src/deploy.ts @@ -1,7 +1,6 @@ import { faAws, faCloudflare, - faFunction, faGoogleCloud, faHetznerH, faKubernetes, @@ -12,6 +11,21 @@ import { faVercel, } from "@rivet-gg/icons"; +// Supabase's official monotone logo. Font Awesome has no Supabase brand icon, +// so this is a Font Awesome compatible icon definition built from the real +// brand SVG (https://simpleicons.org/?q=supabase). Renders in currentColor. +export const faSupabase = { + prefix: "fak", + iconName: "supabase", + icon: [ + 24, + 24, + [], + "", + "M11.9 1.036c-.015-.986-1.26-1.41-1.874-.637L.764 12.05C-.33 13.427.65 15.455 2.409 15.455h9.579l.113 7.51c.014.985 1.259 1.408 1.873.636l9.262-11.653c1.093-1.375.113-3.403-1.645-3.403h-9.642z", + ], +} as any; + export interface DeployOption { displayName: string; name: string; @@ -28,7 +42,7 @@ export const deployOptions = [ { displayName: "Rivet Compute", name: "rivet" as const, - href: "/docs/connect/rivet-compute", + href: "/docs/deploy/rivet-compute", description: "Deploy to Rivet's managed compute platform", icon: faRivet as any, @@ -37,7 +51,7 @@ export const deployOptions = [ { displayName: "Vercel", name: "vercel" as const, - href: "/docs/connect/vercel", + href: "/docs/deploy/vercel", description: "Deploy Next.js + RivetKit apps to Vercel's edge network", icon: faVercel as any, }, @@ -45,7 +59,7 @@ export const deployOptions = [ displayName: "Cloudflare Workers", shortTitle: "Cloudflare", name: "cloudflare-workers" as const, - href: "/docs/connect/cloudflare", + href: "/docs/deploy/cloudflare", description: "Run RivetKit on Cloudflare Workers with the WebAssembly runtime", icon: faCloudflare as any, @@ -55,23 +69,23 @@ export const deployOptions = [ displayName: "Supabase Functions", shortTitle: "Supabase", name: "supabase-functions" as const, - href: "/docs/connect/supabase", + href: "/docs/deploy/supabase", description: "Run RivetKit on Supabase Edge Functions with the WebAssembly runtime", - icon: faFunction as any, + icon: faSupabase, specializedPlatform: true, }, { displayName: "Railway", name: "railway" as const, - href: "/docs/connect/railway", + href: "/docs/deploy/railway", description: "Deploy containers to Railway's managed infrastructure", icon: faRailway as any, }, { displayName: "Kubernetes", name: "kubernetes" as const, - href: "/docs/connect/kubernetes", + href: "/docs/deploy/kubernetes", description: "Deploy to any Kubernetes cluster with container images", icon: faKubernetes as any, }, @@ -79,7 +93,7 @@ export const deployOptions = [ displayName: "AWS ECS", shortTitle: "AWS", name: "aws-ecs" as const, - href: "/docs/connect/aws-ecs", + href: "/docs/deploy/aws-ecs", description: "Run containerized workloads on Amazon Elastic Container Service", icon: faAws as any, @@ -88,14 +102,14 @@ export const deployOptions = [ displayName: "Google Cloud Run", shortTitle: "GCP", name: "gcp-cloud-run" as const, - href: "/docs/connect/gcp-cloud-run", + href: "/docs/deploy/gcp-cloud-run", description: "Deploy containers to Google Cloud Run for auto-scaling", icon: faGoogleCloud, }, { displayName: "Hetzner", name: "hetzner" as const, - href: "/docs/connect/hetzner", + href: "/docs/deploy/hetzner", description: "Deploy to Hetzner's cost-effective cloud infrastructure", icon: faHetznerH as any, }, @@ -103,7 +117,7 @@ export const deployOptions = [ displayName: "VM & Bare Metal", name: "custom" as const, shortTitle: "VM", - href: "/docs/connect/vm-and-bare-metal", + href: "/docs/deploy/vm-and-bare-metal", description: "Run on virtual machines or bare metal servers with full control", icon: faServer as any, @@ -111,7 +125,7 @@ export const deployOptions = [ { displayName: "Custom Platform", name: "custom-platform" as const, - href: "/docs/connect/custom", + href: "/docs/deploy/custom", description: "Integrate RivetKit with any other hosting platform of your choice", icon: faRocket as any, diff --git a/website/redirects.mjs b/website/redirects.mjs index a77d8605c0..9659c6d309 100644 --- a/website/redirects.mjs +++ b/website/redirects.mjs @@ -26,6 +26,23 @@ export const redirects = { '/docs/platforms/next-js': '/docs/clients/javascript/', // Registry configuration moved '/docs/connect/registry-configuration': '/docs/general/registry-configuration/', + // Quickstart index merged into the Actors introduction + '/docs/actors/quickstart': '/docs/actors/', + // Connect tab renamed to Deploy + '/docs/connect': '/docs/deploy/', + '/docs/connect/aws-ecs': '/docs/deploy/aws-ecs/', + '/docs/connect/aws-lambda': '/docs/deploy/aws-lambda/', + '/docs/connect/cloudflare': '/docs/deploy/cloudflare/', + '/docs/connect/custom': '/docs/deploy/custom/', + '/docs/connect/freestyle': '/docs/deploy/freestyle/', + '/docs/connect/gcp-cloud-run': '/docs/deploy/gcp-cloud-run/', + '/docs/connect/hetzner': '/docs/deploy/hetzner/', + '/docs/connect/kubernetes': '/docs/deploy/kubernetes/', + '/docs/connect/railway': '/docs/deploy/railway/', + '/docs/connect/rivet-compute': '/docs/deploy/rivet-compute/', + '/docs/connect/supabase': '/docs/deploy/supabase/', + '/docs/connect/vercel': '/docs/deploy/vercel/', + '/docs/connect/vm-and-bare-metal': '/docs/deploy/vm-and-bare-metal/', // Cloud docs removed - redirect to relevant sections '/docs/cloud': '/docs/self-hosting/', '/docs/cloud/api/actors/create': '/docs/actors/', diff --git a/website/src/components/DocsTabs.tsx b/website/src/components/DocsTabs.tsx index d0e2c1b4ef..71d04fe355 100644 --- a/website/src/components/DocsTabs.tsx +++ b/website/src/components/DocsTabs.tsx @@ -27,7 +27,7 @@ export function DocsTabs() { > {tab.title} {tab.badge && ( - + {tab.badge} )} diff --git a/website/src/components/docs/DocsLanding.tsx b/website/src/components/docs/DocsLanding.tsx new file mode 100644 index 0000000000..b6f2dc8a7b --- /dev/null +++ b/website/src/components/docs/DocsLanding.tsx @@ -0,0 +1,134 @@ +import { Icon } from "@rivet-gg/icons"; +import { AnimatedAgentOSLogo } from "@/components/marketing/solutions/AgentOSPage"; +import actorsLogo from "@/images/products/actors-logo.svg"; + +export interface DocsLandingItem { + title: string; + href: string; + icon: any; + description?: string; + badge?: string; +} + +export interface DocsLandingSection { + title: string; + items: DocsLandingItem[]; +} + +export interface DocsLandingData { + title: string; + subtitle?: string; + // Optional product logo shown above the title in the hero. "agentos" renders + // the animated agentOS logo reused from the marketing page; "actors" renders + // the static actors logo. + logo?: "agentos" | "actors"; + sections: DocsLandingSection[]; +} + +function HeroTitle({ + title, + logo, +}: { + title: string; + logo?: "agentos" | "actors"; +}) { + if (logo === "agentos") { + // The animated agentOS logo is the wordmark, so it stands in for the title. + // The source wordmark is black; invert it to white for the dark docs theme. + return ( +
+ +
+ ); + } + if (logo === "actors") { + return ( +
+ +

+ {title} +

+
+ ); + } + return ( +

+ {title} +

+ ); +} + +// Faint grid backdrop for the card illustration area, evoking the line-art +// panels on Mintlify's docs home. Masked with a radial fade so the grid is +// strongest behind the icon and dissolves toward the edges. +const gridStyle = { + backgroundImage: + "linear-gradient(to right, rgba(255,255,255,0.06) 1px, transparent 1px), linear-gradient(to bottom, rgba(255,255,255,0.06) 1px, transparent 1px)", + backgroundSize: "24px 24px", + maskImage: "radial-gradient(ellipse 60% 60% at 50% 50%, black 20%, transparent 80%)", + WebkitMaskImage: + "radial-gradient(ellipse 60% 60% at 50% 50%, black 20%, transparent 80%)", +}; + +function LandingCard({ item }: { item: DocsLandingItem }) { + return ( + +
+
+ +
+
+
+ {item.title} + {item.badge && ( + + {item.badge} + + )} +
+ {item.description && ( +

+ {item.description} +

+ )} +
+
+ ); +} + +export function DocsLanding({ title, subtitle, logo, sections }: DocsLandingData) { + const showHeaders = sections.length > 1; + + return ( +
+
+ + {subtitle && ( +

{subtitle}

+ )} +
+
+ {sections.map((section) => ( +
+ {showHeaders && ( +

+ {section.title} +

+ )} +
+ {section.items.map((item) => ( + + ))} +
+
+ ))} +
+
+ ); +} diff --git a/website/src/components/docs/docsLandings.ts b/website/src/components/docs/docsLandings.ts new file mode 100644 index 0000000000..3027bcc69a --- /dev/null +++ b/website/src/components/docs/docsLandings.ts @@ -0,0 +1,73 @@ +import { + faCloudflare, + faFastForward, + faLayerGroup, + faLightbulb, + faNextjs, + faNodeJs, + faReact, + faRust, + faScaleBalanced, +} from "@rivet-gg/icons"; +import { deployOptions, faSupabase } from "@rivetkit/shared-data"; +import type { DocsLandingData } from "./DocsLanding"; + +const actors: DocsLandingData = { + title: "Actors", + subtitle: + "Long-lived processes with durable state, realtime events, and built-in hibernation. Pick a stack to start building.", + logo: "actors", + sections: [ + { + title: "Get Started", + items: [ + { title: "Node.js & Bun", href: "/docs/actors/quickstart/backend", icon: faNodeJs, description: "Set up actors with Node.js, Bun, and web frameworks." }, + { title: "React", href: "/docs/actors/quickstart/react", icon: faReact, description: "Build realtime React applications backed by actors." }, + { title: "Next.js", href: "/docs/actors/quickstart/next-js", icon: faNextjs, description: "Server-rendered Next.js experiences backed by actors." }, + { title: "Rust", href: "/docs/actors/quickstart/rust", icon: faRust, badge: "Beta", description: "Native Rust with the typed rivetkit crate." }, + { title: "Effect.ts", href: "/docs/actors/quickstart/effect", icon: faLayerGroup, badge: "Beta", description: "The Effect SDK with typed Schema actions." }, + { title: "Cloudflare Workers", href: "/docs/actors/quickstart/cloudflare", icon: faCloudflare, description: "Run RivetKit on Cloudflare Workers." }, + { title: "Supabase Functions", href: "/docs/actors/quickstart/supabase", icon: faSupabase, description: "Run RivetKit on Supabase Edge Functions." }, + ], + }, + ], +}; + +const deploy: DocsLandingData = { + title: "Deploy", + subtitle: "Run RivetKit anywhere, from serverless functions to your own infrastructure.", + sections: [ + { + title: "Platforms", + items: deployOptions.map((option) => ({ + title: option.shortTitle ?? option.displayName, + href: option.href, + icon: option.icon, + badge: option.badge, + })), + }, + ], +}; + +const agentOs: DocsLandingData = { + title: "agentOS", + subtitle: + "Run coding agents inside isolated VMs with full filesystem, process, and network control.", + logo: "agentos", + sections: [ + { + title: "Get Started", + items: [ + { title: "Quickstart", href: "/docs/agent-os/quickstart", icon: faFastForward, description: "Boot a VM and run your first coding agent." }, + { title: "Crash Course", href: "/docs/agent-os/crash-course", icon: faLightbulb, description: "Learn the core agentOS concepts." }, + { title: "agentOS vs Sandbox", href: "/docs/agent-os/versus-sandbox", icon: faScaleBalanced, description: "How agentOS compares to a plain sandbox." }, + ], + }, + ], +}; + +export const docsLandings: Record = { + actors, + deploy, + "agent-os": agentOs, +}; diff --git a/website/src/components/marketing/components/PlatformIcons.tsx b/website/src/components/marketing/components/PlatformIcons.tsx index 999c873810..e35384c0d7 100644 --- a/website/src/components/marketing/components/PlatformIcons.tsx +++ b/website/src/components/marketing/components/PlatformIcons.tsx @@ -142,25 +142,25 @@ export function PlatformIcons() { tooltip: 'Railway' }, { - href: '/docs/connect/kubernetes', + href: '/docs/deploy/kubernetes', src: kubernetesLogo, alt: 'Kubernetes', tooltip: 'Kubernetes' }, { - href: '/docs/connect/aws-ecs', + href: '/docs/deploy/aws-ecs', src: awsLogo, alt: 'AWS ECS', tooltip: 'AWS ECS' }, { - href: '/docs/connect/gcp-cloud-run', + href: '/docs/deploy/gcp-cloud-run', src: gcpLogo, alt: 'GCP Cloud Run', tooltip: 'GCP Cloud Run' }, { - href: '/docs/connect/hetzner', + href: '/docs/deploy/hetzner', src: hetznerLogo, alt: 'Hetzner', tooltip: 'Hetzner' diff --git a/website/src/components/marketing/sections/TechSection.tsx b/website/src/components/marketing/sections/TechSection.tsx index 0b636fcdc2..9592e546f0 100644 --- a/website/src/components/marketing/sections/TechSection.tsx +++ b/website/src/components/marketing/sections/TechSection.tsx @@ -168,7 +168,7 @@ export function TechSection() { - + - + { +export const AnimatedAgentOSLogo = ({ className, displayedAgent }: AnimatedAgentOSLogoProps) => { const containerRef = useRef(null); const [isReady, setIsReady] = useState(false); const osLayerRef = useRef(null); diff --git a/website/src/content/docs/actors/crash-course.mdx b/website/src/content/docs/actors/crash-course.mdx new file mode 100644 index 0000000000..eaf153c440 --- /dev/null +++ b/website/src/content/docs/actors/crash-course.mdx @@ -0,0 +1,863 @@ +--- +title: "Crash Course" +description: "Learn the core concepts of Rivet Actors: state, actions, realtime events, and clients." +skill: false +--- + +{/* SKILL_OVERVIEW_START */} + +## Features + +- **Long-Lived, Stateful Compute**: Each unit of compute is like a tiny server that remembers things between requests – no need to re-fetch data from a database or worry about timeouts. Like AWS Lambda, but with memory and no timeouts. +- **Blazing-Fast Reads & Writes**: State is stored on the same machine as your compute, so reads and writes are ultra-fast. No database round trips, no latency spikes. State is persisted to Rivet for long term storage, so it survives server restarts. +- **Realtime**: Update state and broadcast changes in realtime with WebSockets. No external pub/sub systems, no polling – just built-in low-latency events. +- **Infinitely Scalable**: Automatically scale from zero to millions of concurrent actors. Pay only for what you use with instant scaling and no cold starts. +- **Fault Tolerant**: Built-in error handling and recovery. Actors automatically restart on failure while preserving state integrity and continuing operations. + +## When to Use Rivet Actors + +- **AI agents & sandboxes**: multi-step toolchains, conversation memory, sandbox orchestration. +- **Multiplayer or collaborative apps**: CRDT docs, shared cursors, realtime dashboards, chat. +- **Workflow automation**: background jobs, cron, rate limiters, durable queues, backpressure control. +- **Data-intensive backends**: geo-distributed or per-tenant databases, in-memory caches, sharded SQL. +- **Networking workloads**: WebSocket servers, custom protocols, local-first sync, edge fanout. + +## Minimal Project + +### Backend + +**index.ts** + +```ts +import { actor, event, setup } from "rivetkit"; + +const counter = actor({ + state: { count: 0 }, + events: { + count: event(), + }, + actions: { + increment: (c, amount: number) => { + c.state.count += amount; + c.broadcast("count", c.state.count); + return c.state.count; + }, + }, +}); + +export const registry = setup({ + use: { counter }, +}); + +registry.start(); +``` + +### Client Docs + +Use the client SDK that matches your app: + +- [JavaScript Client](/docs/clients/javascript) +- [React Client](/docs/clients/react) +- [Swift Client](/docs/clients/swift) + +## Actor Quick Reference + +### In-Memory State + +Persistent data that survives restarts, crashes, and deployments. State is persisted on Rivet Cloud or Rivet self-hosted, so it survives restarts if the current process crashes or exits. + + + +```ts +import { actor } from "rivetkit"; + +const counter = actor({ +state: { count: 0 }, +actions: { +increment: (c) => c.state.count += 1, +}, +}); + +```` + + +```ts +import { actor } from "rivetkit"; + +interface CounterState { + count: number; +} + +const counter = actor({ + createState: (c, input: { start?: number }): CounterState => ({ + count: input.start ?? 0, + }), + actions: { + increment: (c) => c.state.count += 1, + }, +}); +```` + + + + +[Documentation](/docs/actors/state) + +### Keys + +Keys uniquely identify actor instances. Use compound keys (arrays) for hierarchical addressing: + +```ts +import { actor, setup } from "rivetkit"; +import { createClient } from "rivetkit/client"; + +const chatRoom = actor({ + state: { messages: [] as string[] }, + actions: { + getRoomInfo: (c) => ({ org: c.key[0], room: c.key[1] }), + }, +}); + +const registry = setup({ use: { chatRoom } }); +const client = createClient("http://localhost:6420"); + +// Compound key: [org, room] +client.chatRoom.getOrCreate(["org-acme", "general"]); + +// Access key inside actor via c.key +``` + +Don't build keys with string interpolation like `"org:${userId}"` when `userId` contains user data. Use arrays instead to prevent key injection attacks. + +[Documentation](/docs/actors/keys) + +### Input + +Pass initialization data when creating actors. Input is only available in `createState` and `onCreate`, so store it in state if you need it later. + +```ts +import { actor, setup } from "rivetkit"; +import { createClient } from "rivetkit/client"; + +const game = actor({ + state: { mode: "" }, + createState: (c, input: { mode: string }) => ({ + mode: input.mode, // Store input in state for later access + }), + actions: { + getMode: (c) => c.state.mode, + }, +}); + +const registry = setup({ use: { game } }); +const client = createClient("http://localhost:6420"); + +// Client usage +const gameHandle = client.game.getOrCreate(["game-1"], { + createWithInput: { mode: "ranked" }, +}); +``` + +[Documentation](/docs/actors/input) + +### Temporary Variables + +Temporary data that doesn't survive restarts. Use for non-serializable objects (event emitters, connections, etc). + + + +```ts +import { actor } from "rivetkit"; + +const counter = actor({ +state: { count: 0 }, +vars: { lastAccess: 0 }, +actions: { +increment: (c) => { +c.vars.lastAccess = Date.now(); +return c.state.count += 1; +}, +}, +}); + +```` + + +```ts +import { actor } from "rivetkit"; + +const counter = actor({ + state: { count: 0 }, + createVars: () => ({ + emitter: new EventTarget(), + }), + actions: { + increment: (c) => { + c.vars.emitter.dispatchEvent(new Event("change")); + return c.state.count += 1; + }, + }, +}); +```` + + + + +[Documentation](/docs/actors/state) + +### Actions + +Actions are the primary way clients and other actors communicate with an actor. + +```ts +import { actor } from "rivetkit"; + +const counter = actor({ + state: { count: 0 }, + actions: { + increment: (c, amount: number) => (c.state.count += amount), + getCount: (c) => c.state.count, + }, +}); +``` + +[Documentation](/docs/actors/actions) + +### Events & Broadcasts + +Events enable real-time communication from actors to connected clients. + +```ts +import { actor, event } from "rivetkit"; + +const chatRoom = actor({ + state: { messages: [] as string[] }, + events: { + newMessage: event<{ text: string }>(), + }, + actions: { + sendMessage: (c, text: string) => { + // Broadcast to ALL connected clients + c.broadcast("newMessage", { text }); + }, + }, +}); +``` + +[Documentation](/docs/actors/events) + +### Connections + +Access the current connection via `c.conn` or all connected clients via `c.conns`. Use `c.conn.id` or `c.conn.state` to securely identify who is calling an action. `c.conn` is only available for actions invoked through a connected client; stateless actor-handle calls run without a connection, so guard against that. Connection state is initialized via `connState` or `createConnState`, which receives parameters passed by the client on connect. + + + +```ts +import { actor } from "rivetkit"; + +const chatRoom = actor({ +state: {}, +connState: { visitorId: 0 }, +onConnect: (c, conn) => { +conn.state.visitorId = Math.random(); +}, +actions: { +whoAmI: (c) => c.conn.state.visitorId, +}, +}); + +```` + + +```ts +import { actor } from "rivetkit"; + +const chatRoom = actor({ + state: {}, + // params passed from client + createConnState: (c, params: { userId: string }) => ({ + userId: params.userId, + }), + actions: { + // Access current connection's state and params + whoAmI: (c) => ({ + state: c.conn.state, + params: c.conn.params, + }), + // Iterate all connections with c.conns + notifyOthers: (c, text: string) => { + for (const conn of c.conns.values()) { + if (conn !== c.conn) conn.send("notification", { text }); + } + }, + }, +}); +```` + + + + +[Documentation](/docs/actors/connections) + +### Queues + +Use queues to process durable messages in order inside a `run` loop. + +```ts +import { actor, queue } from "rivetkit"; + +const counter = actor({ + state: { value: 0 }, + queues: { + increment: queue<{ amount: number }>(), + }, + run: async (c) => { + for await (const message of c.queue.iter()) { + c.state.value += message.body.amount; + } + }, +}); +``` + +[Documentation](/docs/actors/queues) + +### Workflows + +Use workflows when your `run` logic needs durable, replayable multi-step execution. + +```ts +import { actor, queue } from "rivetkit"; +import { workflow } from "rivetkit/workflow"; + +const worker = actor({ + state: { processed: 0 }, + queues: { + tasks: queue<{ url: string }>(), + }, + run: workflow(async (ctx) => { + await ctx.loop("task-loop", async (loopCtx) => { + const message = await loopCtx.queue.next("wait-task"); + + await loopCtx.step("process-task", async () => { + await processTask(message.body.url); + loopCtx.state.processed += 1; + }); + }); + }), +}); + +async function processTask(url: string): Promise { + const res = await fetch(url, { method: "POST" }); + if (!res.ok) throw new Error(`Task failed: ${res.status}`); +} +``` + +[Documentation](/docs/actors/workflows) + +### Actor-to-Actor Communication + +Actors can call other actors using `c.client()`. + +```ts +import { actor, setup } from "rivetkit"; + +const inventory = actor({ + state: { stock: 100 }, + actions: { + reserve: (c, amount: number) => { + c.state.stock -= amount; + }, + }, +}); + +const order = actor({ + state: {}, + actions: { + process: async (c) => { + const client = c.client(); + await client.inventory.getOrCreate(["main"]).reserve(1); + }, + }, +}); + +const registry = setup({ use: { inventory, order } }); +``` + +[Documentation](/docs/actors/communicating-between-actors) + +### Scheduling + +Schedule actions to run after a delay or at a specific time. Schedules persist across restarts, upgrades, and crashes. + +```ts +import { actor, event } from "rivetkit"; + +const reminder = actor({ + state: { message: "" }, + events: { + reminder: event<{ message: string }>(), + }, + actions: { + // Schedule action to run after delay (ms) + setReminder: (c, message: string, delayMs: number) => { + c.state.message = message; + c.schedule.after(delayMs, "sendReminder"); + }, + // Schedule action to run at specific timestamp + setReminderAt: (c, message: string, timestamp: number) => { + c.state.message = message; + c.schedule.at(timestamp, "sendReminder"); + }, + sendReminder: (c) => { + c.broadcast("reminder", { message: c.state.message }); + }, + }, +}); +``` + +[Documentation](/docs/actors/schedule) + +### Destroying Actors + +Permanently delete an actor and its state using `c.destroy()`. + +```ts +import { actor } from "rivetkit"; + +const userAccount = actor({ + state: { email: "", name: "" }, + onDestroy: (c) => { + console.log(`Account ${c.state.email} deleted`); + }, + actions: { + deleteAccount: (c) => { + c.destroy(); + }, + }, +}); +``` + +[Documentation](/docs/actors/destroy) + +### Lifecycle Hooks + +Actors support hooks for initialization, background processing, connections, networking, and state changes. Use `run` for long-lived background loops, and use `c.aborted` or `c.abortSignal` for graceful shutdown. + +```ts +import { actor, event, queue } from "rivetkit"; + +interface RoomState { + users: Record; + name?: string; +} + +interface RoomInput { + roomName: string; +} + +interface ConnState { + userId: string; + joinedAt: number; +} + +const chatRoom = actor({ + events: { + stateChanged: event(), + }, + queues: { + work: queue<{ task: string }>(), + }, + + // State & vars initialization + createState: (c, input: RoomInput): RoomState => ({ + users: {}, + name: input.roomName, + }), + createVars: () => ({ startTime: Date.now() }), + + // Actor lifecycle + onCreate: (c) => console.log("created", c.key), + onDestroy: (c) => console.log("destroyed"), + onWake: (c) => console.log("actor started"), + onSleep: (c) => console.log("actor sleeping"), + run: async (c) => { + for await (const message of c.queue.iter()) { + console.log("processing", message.body.task); + } + }, + onStateChange: (c, newState) => c.broadcast("stateChanged", newState), + + // Connection lifecycle + createConnState: (c, params): ConnState => ({ + userId: (params as { userId: string }).userId, + joinedAt: Date.now(), + }), + onBeforeConnect: (c, params) => { + /* validate auth */ + }, + onConnect: (c, conn) => console.log("connected:", conn.state.userId), + onDisconnect: (c, conn) => console.log("disconnected:", conn.state.userId), + + // Networking + onRequest: (c, req) => new Response(JSON.stringify(c.state)), + onWebSocket: (c, socket) => socket.addEventListener("message", console.log), + + // Response transformation + onBeforeActionResponse: ( + c: unknown, + name: string, + args: unknown[], + output: Out, + ): Out => output, + + actions: {}, +}); +``` + +[Documentation](/docs/actors/lifecycle) + +### Context Types + +When writing helper functions outside the actor definition, use `*ContextOf` to extract the correct context type. Helpers like `ActionContextOf`, `CreateContextOf`, `ConnContextOf`, and `ConnInitContextOf` are exported from `"rivetkit"`. Do not manually define your own context interface. Always derive it from the actor definition. + +```ts +import { actor, ActionContextOf } from "rivetkit"; + +const gameRoom = actor({ + state: { players: [] as string[], score: 0 }, + actions: { + addPlayer: (c, playerId: string) => { + validatePlayer(c, playerId); + c.state.players.push(playerId); + }, + }, +}); + +// Good: derive context type from actor definition +function validatePlayer(c: ActionContextOf, playerId: string) { + if (c.state.players.includes(playerId)) { + throw new Error("Player already in room"); + } +} + +// Bad: don't manually define context types like this +// type MyContext = { state: { players: string[] }; ... }; +``` + +[Documentation](/docs/actors/types) + +### Errors + +Use `UserError` to throw errors that are safely returned to clients. Pass `metadata` to include structured data. Other errors are converted to generic "internal error" for security. + + + +```ts +import { actor, UserError } from "rivetkit"; + +const user = actor({ +state: { username: "" }, +actions: { +updateUsername: (c, username: string) => { +if (username.length < 3) { +throw new UserError("Username too short", { +code: "username_too_short", +metadata: { minLength: 3, actual: username.length }, +}); +} +c.state.username = username; +}, +}, +}); + +```` + + +```ts +import { actor, setup, UserError } from "rivetkit"; +import { createClient, ActorError } from "rivetkit/client"; + +const user = actor({ + state: { username: "" }, + actions: { + updateUsername: (c, username: string) => { + if (username.length < 3) { + throw new UserError("Username too short", { + code: "username_too_short", + metadata: { minLength: 3, actual: username.length }, + }); + } + c.state.username = username; + }, + }, +}); + +const registry = setup({ use: { user } }); +const client = createClient("http://localhost:6420"); + +try { + await client.user.getOrCreate([]).updateUsername("ab"); +} catch (error) { + if (error instanceof ActorError) { + console.log(error.code); // "username_too_short" + console.log(error.metadata); // { minLength: 3, actual: 2 } + } +} +```` + + + + +[Documentation](/docs/actors/errors) + +### Low-Level HTTP & WebSocket Handlers + +For custom protocols or integrating libraries that need direct access to HTTP `Request`/`Response` or WebSocket connections, use `onRequest` and `onWebSocket`. + +[HTTP Handler Documentation](/docs/actors/request-handler) · [WebSocket Handler Documentation](/docs/actors/websocket-handler) + +### Icons & Names + +Customize how actors appear in the UI with display names and icons. It's recommended to always provide a name and icon to actors in order to make them easier to distinguish in the dashboard. + +```typescript +import { actor } from "rivetkit"; + +const chatRoom = actor({ + options: { + name: "Chat Room", + icon: "💬", // or FontAwesome: "comments", "chart-line", etc. + }, + // ... +}); +``` + +[Documentation](/docs/actors/appearance) + +## Client Documentation + +Find the full client guides here: + +- [JavaScript Client](/docs/clients/javascript) +- [React Client](/docs/clients/react) +- [Swift Client](/docs/clients/swift) + +## Common Patterns + +Actors scale naturally through isolated state and message-passing. Structure your applications with these patterns: + +[Documentation](/docs/actors/design-patterns) + +### Actor Per Entity + +Create one actor per user, document, or room. Use compound keys to scope entities: + + +```ts client.ts +import { createClient } from "rivetkit/client"; +import type { registry } from "./index"; + +const client = createClient("http://localhost:6420"); + +// Single key: one actor per user +client.user.getOrCreate(["user-123"]); + +// Compound key: document scoped to an organization +client.document.getOrCreate(["org-acme", "doc-456"]); + +```` + +```ts index.ts +import { actor, setup } from "rivetkit"; + +export const user = actor({ + state: { name: "" }, + actions: {}, +}); + +export const document = actor({ + state: { content: "" }, + actions: {}, +}); + +export const registry = setup({ use: { user, document } }); + +registry.start(); +```` + + + +### Coordinator & Data Actors + +**Data actors** handle core logic (chat rooms, game sessions, user data). **Coordinator actors** track and manage collections of data actors—think of them as an index. + + +```ts index.ts +import { actor, setup } from "rivetkit"; + +// Coordinator: tracks chat rooms within an organization +export const chatRoomList = actor({ +state: { rooms: [] as string[] }, +actions: { +addRoom: async (c, name: string) => { +// Create the chat room actor +const client = c.client(); +await client.chatRoom.create([c.key[0], name]); +c.state.rooms.push(name); +}, +listRooms: (c) => c.state.rooms, +}, +}); + +// Data actor: handles a single chat room +export const chatRoom = actor({ +state: { messages: [] as string[] }, +actions: { +send: (c, msg: string) => { c.state.messages.push(msg); }, +}, +}); + +export const registry = setup({ use: { chatRoomList, chatRoom } }); + +registry.start(); + +```` + +```ts client.ts +import { createClient } from "rivetkit/client"; +import type { registry } from "./index"; + +const client = createClient("http://localhost:6420"); + +// Coordinator per org +const coordinator = client.chatRoomList.getOrCreate(["org-acme"]); +await coordinator.addRoom("general"); +await coordinator.addRoom("random"); + +// Access chat rooms created by coordinator +client.chatRoom.get(["org-acme", "general"]); +```` + + + +### Run Loop + +Use a `run` loop for continuous background work inside an actor. Process queue messages in order, run logic on intervals, stream AI responses, or coordinate long-running tasks. + +```ts +import { actor, queue, setup } from "rivetkit"; + +const counterWorker = actor({ + state: { value: 0 }, + queues: { + mutate: queue<{ delta: number }>(), + }, + run: async (c) => { + for await (const message of c.queue.iter()) { + c.state.value += message.body.delta; + } + }, + actions: { + getValue: (c) => c.state.value, + }, +}); + +const registry = setup({ use: { counterWorker } }); +``` + +### Workflow Loop + +Use this pattern for long-lived, durable workflows that initialize resources, process commands in a loop, then clean up. + +```ts +import { actor, queue, setup } from "rivetkit"; +import { Loop, workflow } from "rivetkit/workflow"; + +type WorkMessage = { amount: number }; +type ControlMessage = { type: "stop"; reason: string }; + +const worker = actor({ + state: { + phase: "idle" as "idle" | "running" | "stopped", + processed: 0, + total: 0, + stopReason: null as string | null, + }, + queues: { + work: queue(), + control: queue(), + }, + run: workflow(async (ctx) => { + await ctx.step("setup", async () => { + await fetch("https://api.example.com/workers/init", { + method: "POST", + }); + ctx.state.phase = "running"; + ctx.state.stopReason = null; + }); + + const stopReason = await ctx.loop("worker-loop", async (loopCtx) => { + const message = await loopCtx.queue.next("wait-command", { + names: ["work", "control"], + }); + + if (message.name === "work") { + await loopCtx.step("apply-work", async () => { + await fetch("https://api.example.com/workers/process", { + method: "POST", + body: JSON.stringify({ amount: message.body.amount }), + }); + loopCtx.state.processed += 1; + loopCtx.state.total += message.body.amount; + }); + return; + } + + return Loop.break((message.body as ControlMessage).reason); + }); + + await ctx.step("teardown", async () => { + await fetch("https://api.example.com/workers/shutdown", { + method: "POST", + }); + ctx.state.phase = "stopped"; + ctx.state.stopReason = stopReason; + }); + }), +}); + +const registry = setup({ use: { worker } }); +``` + +[Documentation](/docs/actors/workflows) + +### Actions vs Queues + +- **Actions** are not durable. Use them for realtime reads, ephemeral data, and low-latency communication like player input. +- **Queues** are durable. Use them to serialize mutations through the run loop, avoiding race conditions with SQLite and other local state. Callers can still wait for a response from queued work. + +### Authentication, Security, & CORS + +- Validate credentials in `onBeforeConnect` or `createConnState` and throw an error to reject unauthorized connections. +- Use `c.conn.state` to securely identify users in actions rather than trusting action parameters. +- For cross-origin access, validate the request origin in `onBeforeConnect`. + +[Authentication Documentation](/docs/actors/authentication) · [CORS Documentation](/docs/general/cors) + +### Versions & Upgrades + +When deploying new code, set a version number so Rivet can route new actors to the latest runner and optionally drain old ones. Use a build timestamp, git commit count, or CI build number as the version. It is very important to [configure versioning](/docs/actors/versions) before deploying to production. Without versioning, actors can regress by running on older runner versions, and existing actors will never be forced to migrate to new runners. They will continue running indefinitely on the old runners until they exit. + +[Documentation](/docs/actors/versions) + +### Anti-Patterns + +#### Never build a "god" actor + +Do not put all your logic in a single actor. A god actor serializes every operation through one bottleneck, kills parallelism, and makes the entire system fail as a unit. Split into focused actors per entity. + +#### Never create an actor per request + +Actors are long-lived and maintain state across requests. Creating a new actor for every incoming request throws away the core benefit of the model and wastes resources on actor creation and teardown. Use actors for persistent entities and regular functions for stateless work. + +{/* SKILL_OVERVIEW_END */} diff --git a/website/src/content/docs/actors/index.mdx b/website/src/content/docs/actors/index.mdx index b39d539e17..e9c0c1461c 100644 --- a/website/src/content/docs/actors/index.mdx +++ b/website/src/content/docs/actors/index.mdx @@ -1,887 +1,9 @@ --- -title: "Overview" +title: "Introduction" description: "Actors for long-lived processes with durable state, realtime, and hibernate when not in use." skill: false --- -import { faNodeJs, faReact, faNextjs } from "@rivet-gg/icons"; - -## Quickstart - - - - Set up actors with Node.js, Bun, and web frameworks - - - Build real-time React applications with actors - - - Build server-rendered Next.js experiences backed by actors - - - -{/* SKILL_OVERVIEW_START */} - -## Features - -- **Long-Lived, Stateful Compute**: Each unit of compute is like a tiny server that remembers things between requests – no need to re-fetch data from a database or worry about timeouts. Like AWS Lambda, but with memory and no timeouts. -- **Blazing-Fast Reads & Writes**: State is stored on the same machine as your compute, so reads and writes are ultra-fast. No database round trips, no latency spikes. State is persisted to Rivet for long term storage, so it survives server restarts. -- **Realtime**: Update state and broadcast changes in realtime with WebSockets. No external pub/sub systems, no polling – just built-in low-latency events. -- **Infinitely Scalable**: Automatically scale from zero to millions of concurrent actors. Pay only for what you use with instant scaling and no cold starts. -- **Fault Tolerant**: Built-in error handling and recovery. Actors automatically restart on failure while preserving state integrity and continuing operations. - -## When to Use Rivet Actors - -- **AI agents & sandboxes**: multi-step toolchains, conversation memory, sandbox orchestration. -- **Multiplayer or collaborative apps**: CRDT docs, shared cursors, realtime dashboards, chat. -- **Workflow automation**: background jobs, cron, rate limiters, durable queues, backpressure control. -- **Data-intensive backends**: geo-distributed or per-tenant databases, in-memory caches, sharded SQL. -- **Networking workloads**: WebSocket servers, custom protocols, local-first sync, edge fanout. - -## Minimal Project - -### Backend - -**index.ts** - -```ts -import { actor, event, setup } from "rivetkit"; - -const counter = actor({ - state: { count: 0 }, - events: { - count: event(), - }, - actions: { - increment: (c, amount: number) => { - c.state.count += amount; - c.broadcast("count", c.state.count); - return c.state.count; - }, - }, -}); - -export const registry = setup({ - use: { counter }, -}); - -registry.start(); -``` - -### Client Docs - -Use the client SDK that matches your app: - -- [JavaScript Client](/docs/clients/javascript) -- [React Client](/docs/clients/react) -- [Swift Client](/docs/clients/swift) - -## Actor Quick Reference - -### In-Memory State - -Persistent data that survives restarts, crashes, and deployments. State is persisted on Rivet Cloud or Rivet self-hosted, so it survives restarts if the current process crashes or exits. - - - -```ts -import { actor } from "rivetkit"; - -const counter = actor({ -state: { count: 0 }, -actions: { -increment: (c) => c.state.count += 1, -}, -}); - -```` - - -```ts -import { actor } from "rivetkit"; - -interface CounterState { - count: number; -} - -const counter = actor({ - createState: (c, input: { start?: number }): CounterState => ({ - count: input.start ?? 0, - }), - actions: { - increment: (c) => c.state.count += 1, - }, -}); -```` - - - - -[Documentation](/docs/actors/state) - -### Keys - -Keys uniquely identify actor instances. Use compound keys (arrays) for hierarchical addressing: - -```ts -import { actor, setup } from "rivetkit"; -import { createClient } from "rivetkit/client"; - -const chatRoom = actor({ - state: { messages: [] as string[] }, - actions: { - getRoomInfo: (c) => ({ org: c.key[0], room: c.key[1] }), - }, -}); - -const registry = setup({ use: { chatRoom } }); -const client = createClient("http://localhost:6420"); - -// Compound key: [org, room] -client.chatRoom.getOrCreate(["org-acme", "general"]); - -// Access key inside actor via c.key -``` - -Don't build keys with string interpolation like `"org:${userId}"` when `userId` contains user data. Use arrays instead to prevent key injection attacks. - -[Documentation](/docs/actors/keys) - -### Input - -Pass initialization data when creating actors. Input is only available in `createState` and `onCreate`, so store it in state if you need it later. - -```ts -import { actor, setup } from "rivetkit"; -import { createClient } from "rivetkit/client"; - -const game = actor({ - state: { mode: "" }, - createState: (c, input: { mode: string }) => ({ - mode: input.mode, // Store input in state for later access - }), - actions: { - getMode: (c) => c.state.mode, - }, -}); - -const registry = setup({ use: { game } }); -const client = createClient("http://localhost:6420"); - -// Client usage -const gameHandle = client.game.getOrCreate(["game-1"], { - createWithInput: { mode: "ranked" }, -}); -``` - -[Documentation](/docs/actors/input) - -### Temporary Variables - -Temporary data that doesn't survive restarts. Use for non-serializable objects (event emitters, connections, etc). - - - -```ts -import { actor } from "rivetkit"; - -const counter = actor({ -state: { count: 0 }, -vars: { lastAccess: 0 }, -actions: { -increment: (c) => { -c.vars.lastAccess = Date.now(); -return c.state.count += 1; -}, -}, -}); - -```` - - -```ts -import { actor } from "rivetkit"; - -const counter = actor({ - state: { count: 0 }, - createVars: () => ({ - emitter: new EventTarget(), - }), - actions: { - increment: (c) => { - c.vars.emitter.dispatchEvent(new Event("change")); - return c.state.count += 1; - }, - }, -}); -```` - - - - -[Documentation](/docs/actors/state) - -### Actions - -Actions are the primary way clients and other actors communicate with an actor. - -```ts -import { actor } from "rivetkit"; - -const counter = actor({ - state: { count: 0 }, - actions: { - increment: (c, amount: number) => (c.state.count += amount), - getCount: (c) => c.state.count, - }, -}); -``` - -[Documentation](/docs/actors/actions) - -### Events & Broadcasts - -Events enable real-time communication from actors to connected clients. - -```ts -import { actor, event } from "rivetkit"; - -const chatRoom = actor({ - state: { messages: [] as string[] }, - events: { - newMessage: event<{ text: string }>(), - }, - actions: { - sendMessage: (c, text: string) => { - // Broadcast to ALL connected clients - c.broadcast("newMessage", { text }); - }, - }, -}); -``` - -[Documentation](/docs/actors/events) - -### Connections - -Access the current connection via `c.conn` or all connected clients via `c.conns`. Use `c.conn.id` or `c.conn.state` to securely identify who is calling an action. `c.conn` is only available for actions invoked through a connected client; stateless actor-handle calls run without a connection, so guard against that. Connection state is initialized via `connState` or `createConnState`, which receives parameters passed by the client on connect. - - - -```ts -import { actor } from "rivetkit"; - -const chatRoom = actor({ -state: {}, -connState: { visitorId: 0 }, -onConnect: (c, conn) => { -conn.state.visitorId = Math.random(); -}, -actions: { -whoAmI: (c) => c.conn.state.visitorId, -}, -}); - -```` - - -```ts -import { actor } from "rivetkit"; - -const chatRoom = actor({ - state: {}, - // params passed from client - createConnState: (c, params: { userId: string }) => ({ - userId: params.userId, - }), - actions: { - // Access current connection's state and params - whoAmI: (c) => ({ - state: c.conn.state, - params: c.conn.params, - }), - // Iterate all connections with c.conns - notifyOthers: (c, text: string) => { - for (const conn of c.conns.values()) { - if (conn !== c.conn) conn.send("notification", { text }); - } - }, - }, -}); -```` - - - - -[Documentation](/docs/actors/connections) - -### Queues - -Use queues to process durable messages in order inside a `run` loop. - -```ts -import { actor, queue } from "rivetkit"; - -const counter = actor({ - state: { value: 0 }, - queues: { - increment: queue<{ amount: number }>(), - }, - run: async (c) => { - for await (const message of c.queue.iter()) { - c.state.value += message.body.amount; - } - }, -}); -``` - -[Documentation](/docs/actors/queues) - -### Workflows - -Use workflows when your `run` logic needs durable, replayable multi-step execution. - -```ts -import { actor, queue } from "rivetkit"; -import { workflow } from "rivetkit/workflow"; - -const worker = actor({ - state: { processed: 0 }, - queues: { - tasks: queue<{ url: string }>(), - }, - run: workflow(async (ctx) => { - await ctx.loop("task-loop", async (loopCtx) => { - const message = await loopCtx.queue.next("wait-task"); - - await loopCtx.step("process-task", async () => { - await processTask(message.body.url); - loopCtx.state.processed += 1; - }); - }); - }), -}); - -async function processTask(url: string): Promise { - const res = await fetch(url, { method: "POST" }); - if (!res.ok) throw new Error(`Task failed: ${res.status}`); -} -``` - -[Documentation](/docs/actors/workflows) - -### Actor-to-Actor Communication - -Actors can call other actors using `c.client()`. - -```ts -import { actor, setup } from "rivetkit"; - -const inventory = actor({ - state: { stock: 100 }, - actions: { - reserve: (c, amount: number) => { - c.state.stock -= amount; - }, - }, -}); - -const order = actor({ - state: {}, - actions: { - process: async (c) => { - const client = c.client(); - await client.inventory.getOrCreate(["main"]).reserve(1); - }, - }, -}); - -const registry = setup({ use: { inventory, order } }); -``` - -[Documentation](/docs/actors/communicating-between-actors) - -### Scheduling - -Schedule actions to run after a delay or at a specific time. Schedules persist across restarts, upgrades, and crashes. - -```ts -import { actor, event } from "rivetkit"; - -const reminder = actor({ - state: { message: "" }, - events: { - reminder: event<{ message: string }>(), - }, - actions: { - // Schedule action to run after delay (ms) - setReminder: (c, message: string, delayMs: number) => { - c.state.message = message; - c.schedule.after(delayMs, "sendReminder"); - }, - // Schedule action to run at specific timestamp - setReminderAt: (c, message: string, timestamp: number) => { - c.state.message = message; - c.schedule.at(timestamp, "sendReminder"); - }, - sendReminder: (c) => { - c.broadcast("reminder", { message: c.state.message }); - }, - }, -}); -``` - -[Documentation](/docs/actors/schedule) - -### Destroying Actors - -Permanently delete an actor and its state using `c.destroy()`. - -```ts -import { actor } from "rivetkit"; - -const userAccount = actor({ - state: { email: "", name: "" }, - onDestroy: (c) => { - console.log(`Account ${c.state.email} deleted`); - }, - actions: { - deleteAccount: (c) => { - c.destroy(); - }, - }, -}); -``` - -[Documentation](/docs/actors/destroy) - -### Lifecycle Hooks - -Actors support hooks for initialization, background processing, connections, networking, and state changes. Use `run` for long-lived background loops, and use `c.aborted` or `c.abortSignal` for graceful shutdown. - -```ts -import { actor, event, queue } from "rivetkit"; - -interface RoomState { - users: Record; - name?: string; -} - -interface RoomInput { - roomName: string; -} - -interface ConnState { - userId: string; - joinedAt: number; -} - -const chatRoom = actor({ - events: { - stateChanged: event(), - }, - queues: { - work: queue<{ task: string }>(), - }, - - // State & vars initialization - createState: (c, input: RoomInput): RoomState => ({ - users: {}, - name: input.roomName, - }), - createVars: () => ({ startTime: Date.now() }), - - // Actor lifecycle - onCreate: (c) => console.log("created", c.key), - onDestroy: (c) => console.log("destroyed"), - onWake: (c) => console.log("actor started"), - onSleep: (c) => console.log("actor sleeping"), - run: async (c) => { - for await (const message of c.queue.iter()) { - console.log("processing", message.body.task); - } - }, - onStateChange: (c, newState) => c.broadcast("stateChanged", newState), - - // Connection lifecycle - createConnState: (c, params): ConnState => ({ - userId: (params as { userId: string }).userId, - joinedAt: Date.now(), - }), - onBeforeConnect: (c, params) => { - /* validate auth */ - }, - onConnect: (c, conn) => console.log("connected:", conn.state.userId), - onDisconnect: (c, conn) => console.log("disconnected:", conn.state.userId), - - // Networking - onRequest: (c, req) => new Response(JSON.stringify(c.state)), - onWebSocket: (c, socket) => socket.addEventListener("message", console.log), - - // Response transformation - onBeforeActionResponse: ( - c: unknown, - name: string, - args: unknown[], - output: Out, - ): Out => output, - - actions: {}, -}); -``` - -[Documentation](/docs/actors/lifecycle) - -### Context Types - -When writing helper functions outside the actor definition, use `*ContextOf` to extract the correct context type. Helpers like `ActionContextOf`, `CreateContextOf`, `ConnContextOf`, and `ConnInitContextOf` are exported from `"rivetkit"`. Do not manually define your own context interface. Always derive it from the actor definition. - -```ts -import { actor, ActionContextOf } from "rivetkit"; - -const gameRoom = actor({ - state: { players: [] as string[], score: 0 }, - actions: { - addPlayer: (c, playerId: string) => { - validatePlayer(c, playerId); - c.state.players.push(playerId); - }, - }, -}); - -// Good: derive context type from actor definition -function validatePlayer(c: ActionContextOf, playerId: string) { - if (c.state.players.includes(playerId)) { - throw new Error("Player already in room"); - } -} - -// Bad: don't manually define context types like this -// type MyContext = { state: { players: string[] }; ... }; -``` - -[Documentation](/docs/actors/types) - -### Errors - -Use `UserError` to throw errors that are safely returned to clients. Pass `metadata` to include structured data. Other errors are converted to generic "internal error" for security. - - - -```ts -import { actor, UserError } from "rivetkit"; - -const user = actor({ -state: { username: "" }, -actions: { -updateUsername: (c, username: string) => { -if (username.length < 3) { -throw new UserError("Username too short", { -code: "username_too_short", -metadata: { minLength: 3, actual: username.length }, -}); -} -c.state.username = username; -}, -}, -}); - -```` - - -```ts -import { actor, setup, UserError } from "rivetkit"; -import { createClient, ActorError } from "rivetkit/client"; - -const user = actor({ - state: { username: "" }, - actions: { - updateUsername: (c, username: string) => { - if (username.length < 3) { - throw new UserError("Username too short", { - code: "username_too_short", - metadata: { minLength: 3, actual: username.length }, - }); - } - c.state.username = username; - }, - }, -}); - -const registry = setup({ use: { user } }); -const client = createClient("http://localhost:6420"); - -try { - await client.user.getOrCreate([]).updateUsername("ab"); -} catch (error) { - if (error instanceof ActorError) { - console.log(error.code); // "username_too_short" - console.log(error.metadata); // { minLength: 3, actual: 2 } - } -} -```` - - - - -[Documentation](/docs/actors/errors) - -### Low-Level HTTP & WebSocket Handlers - -For custom protocols or integrating libraries that need direct access to HTTP `Request`/`Response` or WebSocket connections, use `onRequest` and `onWebSocket`. - -[HTTP Handler Documentation](/docs/actors/request-handler) · [WebSocket Handler Documentation](/docs/actors/websocket-handler) - -### Icons & Names - -Customize how actors appear in the UI with display names and icons. It's recommended to always provide a name and icon to actors in order to make them easier to distinguish in the dashboard. - -```typescript -import { actor } from "rivetkit"; - -const chatRoom = actor({ - options: { - name: "Chat Room", - icon: "💬", // or FontAwesome: "comments", "chart-line", etc. - }, - // ... -}); -``` - -[Documentation](/docs/actors/appearance) - -## Client Documentation - -Find the full client guides here: - -- [JavaScript Client](/docs/clients/javascript) -- [React Client](/docs/clients/react) -- [Swift Client](/docs/clients/swift) - -## Common Patterns - -Actors scale naturally through isolated state and message-passing. Structure your applications with these patterns: - -[Documentation](/docs/actors/design-patterns) - -### Actor Per Entity - -Create one actor per user, document, or room. Use compound keys to scope entities: - - -```ts client.ts -import { createClient } from "rivetkit/client"; -import type { registry } from "./index"; - -const client = createClient("http://localhost:6420"); - -// Single key: one actor per user -client.user.getOrCreate(["user-123"]); - -// Compound key: document scoped to an organization -client.document.getOrCreate(["org-acme", "doc-456"]); - -```` - -```ts index.ts -import { actor, setup } from "rivetkit"; - -export const user = actor({ - state: { name: "" }, - actions: {}, -}); - -export const document = actor({ - state: { content: "" }, - actions: {}, -}); - -export const registry = setup({ use: { user, document } }); - -registry.start(); -```` - - - -### Coordinator & Data Actors - -**Data actors** handle core logic (chat rooms, game sessions, user data). **Coordinator actors** track and manage collections of data actors—think of them as an index. - - -```ts index.ts -import { actor, setup } from "rivetkit"; - -// Coordinator: tracks chat rooms within an organization -export const chatRoomList = actor({ -state: { rooms: [] as string[] }, -actions: { -addRoom: async (c, name: string) => { -// Create the chat room actor -const client = c.client(); -await client.chatRoom.create([c.key[0], name]); -c.state.rooms.push(name); -}, -listRooms: (c) => c.state.rooms, -}, -}); - -// Data actor: handles a single chat room -export const chatRoom = actor({ -state: { messages: [] as string[] }, -actions: { -send: (c, msg: string) => { c.state.messages.push(msg); }, -}, -}); - -export const registry = setup({ use: { chatRoomList, chatRoom } }); - -registry.start(); - -```` - -```ts client.ts -import { createClient } from "rivetkit/client"; -import type { registry } from "./index"; - -const client = createClient("http://localhost:6420"); - -// Coordinator per org -const coordinator = client.chatRoomList.getOrCreate(["org-acme"]); -await coordinator.addRoom("general"); -await coordinator.addRoom("random"); - -// Access chat rooms created by coordinator -client.chatRoom.get(["org-acme", "general"]); -```` - - - -### Run Loop - -Use a `run` loop for continuous background work inside an actor. Process queue messages in order, run logic on intervals, stream AI responses, or coordinate long-running tasks. - -```ts -import { actor, queue, setup } from "rivetkit"; - -const counterWorker = actor({ - state: { value: 0 }, - queues: { - mutate: queue<{ delta: number }>(), - }, - run: async (c) => { - for await (const message of c.queue.iter()) { - c.state.value += message.body.delta; - } - }, - actions: { - getValue: (c) => c.state.value, - }, -}); - -const registry = setup({ use: { counterWorker } }); -``` - -### Workflow Loop - -Use this pattern for long-lived, durable workflows that initialize resources, process commands in a loop, then clean up. - -```ts -import { actor, queue, setup } from "rivetkit"; -import { Loop, workflow } from "rivetkit/workflow"; - -type WorkMessage = { amount: number }; -type ControlMessage = { type: "stop"; reason: string }; - -const worker = actor({ - state: { - phase: "idle" as "idle" | "running" | "stopped", - processed: 0, - total: 0, - stopReason: null as string | null, - }, - queues: { - work: queue(), - control: queue(), - }, - run: workflow(async (ctx) => { - await ctx.step("setup", async () => { - await fetch("https://api.example.com/workers/init", { - method: "POST", - }); - ctx.state.phase = "running"; - ctx.state.stopReason = null; - }); - - const stopReason = await ctx.loop("worker-loop", async (loopCtx) => { - const message = await loopCtx.queue.next("wait-command", { - names: ["work", "control"], - }); - - if (message.name === "work") { - await loopCtx.step("apply-work", async () => { - await fetch("https://api.example.com/workers/process", { - method: "POST", - body: JSON.stringify({ amount: message.body.amount }), - }); - loopCtx.state.processed += 1; - loopCtx.state.total += message.body.amount; - }); - return; - } - - return Loop.break((message.body as ControlMessage).reason); - }); - - await ctx.step("teardown", async () => { - await fetch("https://api.example.com/workers/shutdown", { - method: "POST", - }); - ctx.state.phase = "stopped"; - ctx.state.stopReason = stopReason; - }); - }), -}); - -const registry = setup({ use: { worker } }); -``` - -[Documentation](/docs/actors/workflows) - -### Actions vs Queues - -- **Actions** are not durable. Use them for realtime reads, ephemeral data, and low-latency communication like player input. -- **Queues** are durable. Use them to serialize mutations through the run loop, avoiding race conditions with SQLite and other local state. Callers can still wait for a response from queued work. - -### Authentication, Security, & CORS - -- Validate credentials in `onBeforeConnect` or `createConnState` and throw an error to reject unauthorized connections. -- Use `c.conn.state` to securely identify users in actions rather than trusting action parameters. -- For cross-origin access, validate the request origin in `onBeforeConnect`. - -[Authentication Documentation](/docs/actors/authentication) · [CORS Documentation](/docs/general/cors) - -### Versions & Upgrades - -When deploying new code, set a version number so Rivet can route new actors to the latest runner and optionally drain old ones. Use a build timestamp, git commit count, or CI build number as the version. It is very important to [configure versioning](/docs/actors/versions) before deploying to production. Without versioning, actors can regress by running on older runner versions, and existing actors will never be forced to migrate to new runners. They will continue running indefinitely on the old runners until they exit. - -[Documentation](/docs/actors/versions) - -### Anti-Patterns - -#### Never build a "god" actor - -Do not put all your logic in a single actor. A god actor serializes every operation through one bottleneck, kills parallelism, and makes the entire system fail as a unit. Split into focused actors per entity. - -#### Never create an actor per request - -Actors are long-lived and maintain state across requests. Creating a new actor for every incoming request throws away the core benefit of the model and wastes resources on actor creation and teardown. Use actors for persistent entities and regular functions for stateless work. - -{/* SKILL_OVERVIEW_END */} +{/* The Actors overview renders as an icon-grid landing via the DocsLanding +React component (see src/components/docs/DocsLanding.tsx + docsLandings.ts), +wired up in src/pages/docs/[...slug].astro. No markdown body here. */} diff --git a/website/src/content/docs/actors/quickstart/backend.mdx b/website/src/content/docs/actors/quickstart/backend.mdx index ea7b3559f3..b247c57664 100644 --- a/website/src/content/docs/actors/quickstart/backend.mdx +++ b/website/src/content/docs/actors/quickstart/backend.mdx @@ -28,12 +28,6 @@ npx skills add rivet-dev/skills npm install rivetkit ``` -If you plan to connect from a React frontend, also install `@rivetkit/react`: - -```sh -npm install @rivetkit/react -``` - diff --git a/website/src/content/docs/actors/quickstart/effect.mdx b/website/src/content/docs/actors/quickstart/effect.mdx index c62310dea5..aaac945ec0 100644 --- a/website/src/content/docs/actors/quickstart/effect.mdx +++ b/website/src/content/docs/actors/quickstart/effect.mdx @@ -197,6 +197,62 @@ See the [JavaScript client documentation](/docs/clients/javascript) for more inf +## Feature Support + +The Effect SDK wraps the most common actor features with typed, schema-validated APIs. Everything else is still fully usable through the raw RivetKit context (see [Raw Escape Hatch](#raw-escape-hatch) below), so no feature is off limits, it just isn't typed yet. + +| Feature | Effect-native API | Access | +| --- | --- | --- | +| Actor contract & actions | `Actor.make`, `Action.make` | Typed | +| Persisted state | `State.get` / `set` / `update` / `updateAndGet` / `changes` | Typed | +| Typed client | `Actor.client`, `Client.layer` | Typed | +| Typed errors | `RivetError` | Typed | +| Logging | `Logger` | Typed | +| Sleep request | `Actor.Sleep` | Typed | +| Actor address (`actorId` / `name` / `key`) | `Actor.CurrentAddress` | Typed | +| Registry serve / test / web handler | `Registry` | Typed | +| [Events & broadcast](/docs/actors/events) | Not yet wrapped | `rawRivetkitContext.broadcast(...)` | +| [Schedule](/docs/actors/schedule) | Not yet wrapped | `rawRivetkitContext.schedule.*` | +| [Embedded SQLite](/docs/actors/sqlite) | Not yet wrapped | `rawRivetkitContext.db.execute(...)` | +| [Destroy](/docs/actors/lifecycle) | Not yet wrapped | `rawRivetkitContext.destroy()` | +| Queues, connections, vars, alarms | Not yet wrapped | `rawRivetkitContext.*` | +| Lifecycle hooks (`onSleep` / `onDestroy`) | Not yet wrapped | `rawRivetkitContext.*` | +| Raw HTTP / WebSocket handlers | Not yet wrapped | `rawRivetkitContext.*` | + +### Raw Escape Hatch + +Every wake function receives `rawRivetkitContext`, the underlying RivetKit [actor context](/docs/actors). Reach for it to use any feature that does not have a typed wrapper yet. The typed `state` argument and the raw context point at the same actor, so you can mix both: + +```ts src/actors/counter/live.ts @nocheck +export const CounterLive = Counter.toLayer( + Effect.fnUntraced(function* ({ rawRivetkitContext, state }) { + return Counter.of({ + Increment: Effect.fnUntraced(function* ({ payload }) { + // Typed state wrapper + const next = yield* State.updateAndGet(state, (current) => ({ + count: current.count + payload.amount, + })).pipe(Effect.orDie); + + // Untyped features run through the raw context + rawRivetkitContext.broadcast("newCount", next.count); + rawRivetkitContext.schedule.after(1_000, "tick", {}); + + return next.count; + }), + }); + }), + { + state: { + schema: Schema.Struct({ count: Schema.Number }), + initialValue: () => ({ count: 0 }), + }, + name: "Counter", + }, +); +``` + +Calls through `rawRivetkitContext` are not validated by `effect/Schema` and their payloads are typed as they are in the base RivetKit API. + ## Next Steps diff --git a/website/src/content/docs/actors/quickstart/index.mdx b/website/src/content/docs/actors/quickstart/index.mdx deleted file mode 100644 index 0421a0d0d0..0000000000 --- a/website/src/content/docs/actors/quickstart/index.mdx +++ /dev/null @@ -1,74 +0,0 @@ ---- -title: "Quickstart" -description: "Set up actors with Node.js, Bun, and web frameworks" -skill: false ---- - -import { - faCloudflare, - faFunction, - faLayerGroup, - faNodeJs, - faReact, - faNextjs, - faRust, -} from "@rivet-gg/icons"; - - -**Using an AI coding assistant?** Add Rivet skills for enhanced development assistance: -```sh -npx skills add rivet-dev/skills -``` - - - - - Set up actors with Node.js, Bun, and web frameworks - - - Build real-time React applications with actors - - - Build server-rendered Next.js experiences backed by actors - - - Build a Rivet Actor in Rust with the typed `rivetkit` crate - - - Build a Rivet Actor with the Effect SDK and `effect/Schema` - - - Run RivetKit on Cloudflare Workers with the WebAssembly runtime - - - Run RivetKit on Supabase Edge Functions with the WebAssembly runtime - - diff --git a/website/src/content/docs/actors/quickstart/next-js.mdx b/website/src/content/docs/actors/quickstart/next-js.mdx index f1ec9f85df..ed210a5496 100644 --- a/website/src/content/docs/actors/quickstart/next-js.mdx +++ b/website/src/content/docs/actors/quickstart/next-js.mdx @@ -140,7 +140,7 @@ For information about the Next.js client API, see the [React Client API Referenc -See the [Vercel deployment guide](/docs/connect/vercel) for detailed instructions on deploying your Rivet app to Vercel. +See the [Vercel deployment guide](/docs/deploy/vercel) for detailed instructions on deploying your Rivet app to Vercel. diff --git a/website/src/content/docs/actors/sqlite-drizzle.mdx b/website/src/content/docs/actors/sqlite-drizzle.mdx index 62f1fe2646..f059cae95d 100644 --- a/website/src/content/docs/actors/sqlite-drizzle.mdx +++ b/website/src/content/docs/actors/sqlite-drizzle.mdx @@ -254,6 +254,6 @@ actions: { - [Drizzle SQLite quickstart](https://orm.drizzle.team/docs/get-started-sqlite) - [Drizzle `drizzle-kit generate`](https://orm.drizzle.team/docs/drizzle-kit-generate) -- [Drizzle + Cloudflare D1](https://orm.drizzle.team/docs/connect-cloudflare-d1) -- [Drizzle + Cloudflare Durable Objects](https://orm.drizzle.team/docs/connect-cloudflare-do) +- [Drizzle + Cloudflare D1](https://orm.drizzle.team/docs/deploy-cloudflare-d1) +- [Drizzle + Cloudflare Durable Objects](https://orm.drizzle.team/docs/deploy-cloudflare-do) - [Cloudflare Durable Objects SQLite storage](https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/) diff --git a/website/src/content/docs/agent-os/crash-course.mdx b/website/src/content/docs/agent-os/crash-course.mdx new file mode 100644 index 0000000000..a93e27865a --- /dev/null +++ b/website/src/content/docs/agent-os/crash-course.mdx @@ -0,0 +1,590 @@ +--- +title: "Crash Course" +description: "Run coding agents inside isolated VMs with full filesystem, process, and network control." +skill: true +--- + + +agentOS is in preview and the API is subject to change. If you run into issues, please [report them on GitHub](https://github.com/rivet-dev/rivet/issues) or [join our Discord](https://rivet.dev/discord). + + +{/* SKILL_OVERVIEW_START */} + +## Features + +- **Isolated VMs**: Each agent gets its own filesystem, processes, and networking. No shared state, no cross-contamination. +- **Multi-Agent Support**: Run Amp, Claude Code, Codex, OpenCode, and PI with a unified API. Swap agents without changing your code. +- **Host Tools**: Expose your JavaScript functions to agents as CLI commands. Direct binding with near-zero latency and automatic code mode for up to 80% token reduction. +- **Persistent State**: Filesystem and transcripts survive sleep/wake cycles automatically. No external database needed. +- **Orchestration**: Workflows, queues, cron jobs, and multi-agent coordination built on Rivet Actors. +- **Hybrid Sandboxes**: Run agents in the lightweight VM by default. Spin up a full sandbox on demand for browsers, compilation, and desktop automation. + +## When to Use agentOS + +- **Coding agents**: Run any coding agent with full OS access, file editing, shell execution, and tool use. +- **Automated pipelines**: CI-like workflows where agents clone repos, fix bugs, run tests, and open PRs. +- **Multi-agent systems**: Coordinators dispatching to specialized agents, review pipelines, planning chains. +- **Scheduled maintenance**: Cron-based agents that audit code, update dependencies, or generate reports. +- **Collaborative workspaces**: Multiple users observing and interacting with the same agent session in realtime. + +## Minimal Project + + +```ts @nocheck client.ts +import { createClient } from "rivetkit/client"; +import type { registry } from "./server"; + +const client = createClient("http://localhost:6420"); +const agent = client.vm.getOrCreate(["my-agent"]); + +// Subscribe to streaming events +agent.on("sessionEvent", (data) => { + console.log(data.event); +}); + +// Create a session and send a prompt +const session = await agent.createSession("pi", { + env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! }, +}); +const response = await agent.sendPrompt( + session.sessionId, + "Write a hello world script to /home/user/hello.js", +); +console.log(response); + +// Read the file the agent created +const content = await agent.readFile("/home/user/hello.js"); +console.log(new TextDecoder().decode(content)); +``` + +```ts @nocheck server.ts +import { agentOs } from "rivetkit/agent-os"; +import { setup } from "rivetkit"; +import common from "@rivet-dev/agent-os-common"; +import pi from "@rivet-dev/agent-os-pi"; + +const vm = agentOs({ + options: { software: [common, pi] }, +}); + +export const registry = setup({ use: { vm } }); +registry.start(); +``` + + +After the quickstart, customize your agent with the [Registry](/agent-os/registry). + +## Quick Reference + +### Sessions & Transcripts + +Create agent sessions, send prompts, and stream responses in realtime. Transcripts are persisted automatically across sleep/wake cycles. + + +```ts @nocheck client.ts +import { createClient } from "rivetkit/client"; +import type { registry } from "./server"; + +const client = createClient("http://localhost:6420"); +const agent = client.vm.getOrCreate(["my-agent"]); + +// Stream events as they arrive +agent.on("sessionEvent", (data) => { + console.log(data.event.method, data.event); +}); + +// Create a session with MCP servers +const session = await agent.createSession("pi", { + env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! }, + mcpServers: [ + { + type: "local", + command: "npx", + args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user"], + env: {}, + }, + ], +}); + +// Send a prompt and wait for the response +const response = await agent.sendPrompt( + session.sessionId, + "List all files in the home directory", +); +console.log(response); +``` + +```ts @nocheck server.ts +import { agentOs } from "rivetkit/agent-os"; +import { setup } from "rivetkit"; +import common from "@rivet-dev/agent-os-common"; +import pi from "@rivet-dev/agent-os-pi"; + +const vm = agentOs({ + options: { software: [common, pi] }, +}); + +export const registry = setup({ use: { vm } }); +registry.start(); +``` + + +[Documentation](/docs/agent-os/sessions) + +### Permissions + +Approve or deny agent tool use with human-in-the-loop patterns or auto-approve for trusted workloads. + + +```ts @nocheck server.ts +import { agentOs } from "rivetkit/agent-os"; +import { setup } from "rivetkit"; +import common from "@rivet-dev/agent-os-common"; +import pi from "@rivet-dev/agent-os-pi"; + +// Auto-approve all permissions server-side +const vm = agentOs({ + onPermissionRequest: async (c, sessionId, request) => { + await c.respondPermission(sessionId, request.permissionId, "always"); + }, + options: { software: [common, pi] }, +}); + +export const registry = setup({ use: { vm } }); +registry.start(); +``` + +```ts @nocheck client.ts +import { createClient } from "rivetkit/client"; +import type { registry } from "./server"; + +const client = createClient("http://localhost:6420"); +const agent = client.vm.getOrCreate(["my-agent"]); + +// Or handle permissions client-side for human-in-the-loop +agent.on("permissionRequest", async (data) => { + console.log("Permission requested:", data.request); + // "once" | "always" | "reject" + await agent.respondPermission(data.sessionId, data.request.permissionId, "once"); +}); +``` + + +[Documentation](/docs/agent-os/permissions) + +### Tools + +Expose your JavaScript functions to agents as CLI commands inside the VM. Agents call them as shell commands with auto-generated flags from Zod schemas. + +```ts @nocheck +import { toolKit, hostTool } from "@rivet-dev/agent-os-core"; +import { z } from "zod"; + +const myTools = toolKit({ + name: "myapp", + description: "Application tools", + tools: { + createTicket: hostTool({ + description: "Create a ticket in the issue tracker", + inputSchema: z.object({ + title: z.string().describe("Ticket title"), + priority: z.enum(["low", "medium", "high"]).describe("Priority level"), + }), + execute: async (input) => { + const ticket = await db.tickets.create(input); + return { id: ticket.id, url: ticket.url }; + }, + }), + }, +}); + +// Agent calls: agentos-myapp createTicket --title "Fix login" --priority high +``` + +[Documentation](/docs/agent-os/tools) + +### Filesystem + +Read, write, and manage files inside the VM. The `/home/user` directory is persisted automatically across sleep/wake cycles. + + +```ts @nocheck client.ts +import { createClient } from "rivetkit/client"; +import type { registry } from "./server"; + +const client = createClient("http://localhost:6420"); +const agent = client.vm.getOrCreate(["my-agent"]); + +// Write a file +await agent.writeFile("/home/user/config.json", JSON.stringify({ key: "value" })); + +// Read a file +const content = await agent.readFile("/home/user/config.json"); +console.log(new TextDecoder().decode(content)); + +// List directory contents recursively +const files = await agent.readdirRecursive("/home/user", { maxDepth: 2 }); +console.log(files); +``` + +```ts @nocheck server.ts +import { agentOs } from "rivetkit/agent-os"; +import { setup } from "rivetkit"; +import common from "@rivet-dev/agent-os-common"; +import pi from "@rivet-dev/agent-os-pi"; + +const vm = agentOs({ + options: { software: [common, pi] }, +}); + +export const registry = setup({ use: { vm } }); +registry.start(); +``` + + +[Documentation](/docs/agent-os/filesystem) + +### Processes & Shell + +Execute commands, spawn long-running processes, and open interactive shells. + + +```ts @nocheck client.ts +import { createClient } from "rivetkit/client"; +import type { registry } from "./server"; + +const client = createClient("http://localhost:6420"); +const agent = client.vm.getOrCreate(["my-agent"]); + +// One-shot execution +const result = await agent.exec("echo hello && ls /home/user"); +console.log("stdout:", result.stdout); +console.log("exit code:", result.exitCode); + +// Spawn a long-running process +agent.on("processOutput", (data) => { + console.log(`[${data.processId}]`, data.output); +}); + +const proc = await agent.spawn("node", ["server.js"]); +console.log("Process ID:", proc.processId); +``` + +```ts @nocheck server.ts +import { agentOs } from "rivetkit/agent-os"; +import { setup } from "rivetkit"; +import common from "@rivet-dev/agent-os-common"; +import pi from "@rivet-dev/agent-os-pi"; + +const vm = agentOs({ + options: { software: [common, pi] }, +}); + +export const registry = setup({ use: { vm } }); +registry.start(); +``` + + +[Documentation](/docs/agent-os/processes) + +### Networking & Previews + +Proxy HTTP requests into VMs with `vmFetch`. Create preview URLs for port forwarding VM services to shareable public URLs. + + +```ts @nocheck client.ts +import { createClient } from "rivetkit/client"; +import type { registry } from "./server"; + +const client = createClient("http://localhost:6420"); +const agent = client.vm.getOrCreate(["my-agent"]); + +// Fetch from a service running inside the VM +const response = await agent.vmFetch(3000, "/api/health"); +console.log("Status:", response.status); + +// Create a preview URL (port forwarding to a public URL) +const preview = await agent.createSignedPreviewUrl(3000); +console.log("Public URL:", preview.path); +console.log("Expires at:", new Date(preview.expiresAt)); +``` + +```ts @nocheck server.ts +import { agentOs } from "rivetkit/agent-os"; +import { setup } from "rivetkit"; +import common from "@rivet-dev/agent-os-common"; +import pi from "@rivet-dev/agent-os-pi"; + +const vm = agentOs({ + options: { software: [common, pi] }, +}); + +export const registry = setup({ use: { vm } }); +registry.start(); +``` + + +[Documentation](/docs/agent-os/networking) + +### Cron Jobs + +Schedule recurring commands and agent sessions with cron expressions. + + +```ts @nocheck client.ts +import { createClient } from "rivetkit/client"; +import type { registry } from "./server"; + +const client = createClient("http://localhost:6420"); +const agent = client.vm.getOrCreate(["my-agent"]); + +// Schedule a command every hour +await agent.scheduleCron({ + schedule: "0 * * * *", + action: { type: "exec", command: "rm", args: ["-rf", "/tmp/cache/*"] }, +}); + +// Schedule an agent session daily at 9 AM +await agent.scheduleCron({ + schedule: "0 9 * * *", + action: { + type: "session", + agent: "pi", + prompt: "Review the codebase for security issues and write a report to /home/user/audit.md", + }, +}); +``` + +```ts @nocheck server.ts +import { agentOs } from "rivetkit/agent-os"; +import { setup } from "rivetkit"; +import common from "@rivet-dev/agent-os-common"; +import pi from "@rivet-dev/agent-os-pi"; + +const vm = agentOs({ + options: { software: [common, pi] }, +}); + +export const registry = setup({ use: { vm } }); +registry.start(); +``` + + +[Documentation](/docs/agent-os/cron) + +### Sandbox Mounting + +agentOS uses a hybrid model: agents run in a lightweight VM by default and spin up a full sandbox on demand for heavy workloads like browsers, compilation, and desktop automation. + +```ts @nocheck +import { agentOs } from "rivetkit/agent-os"; +import { setup } from "rivetkit"; +import common from "@rivet-dev/agent-os-common"; +import pi from "@rivet-dev/agent-os-pi"; + +const vm = agentOs({ + options: { software: [common, pi], + sandbox: { + enabled: true, + }, + }, +}); + +export const registry = setup({ use: { vm } }); +registry.start(); +``` + +[Documentation](/docs/agent-os/sandbox) + +### Multiplayer & Realtime + +Connect multiple clients to the same agent VM. All subscribers see session output, process logs, and shell data in realtime. + + +```ts @nocheck client.ts +import { createClient } from "rivetkit/client"; +import type { registry } from "./server"; + +// Client A: creates the session and sends prompts +const clientA = createClient("http://localhost:6420"); +const agentA = clientA.vm.getOrCreate(["shared-agent"]); +agentA.on("sessionEvent", (data) => console.log("[A]", data.event.method)); + +const session = await agentA.createSession("pi", { + env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! }, +}); +await agentA.sendPrompt(session.sessionId, "Build a REST API"); + +// Client B: observes the same session (separate process) +const clientB = createClient("http://localhost:6420"); +const agentB = clientB.vm.getOrCreate(["shared-agent"]); +agentB.on("sessionEvent", (data) => console.log("[B]", data.event.method)); +// Client B sees the same events as Client A +``` + +```ts @nocheck server.ts +import { agentOs } from "rivetkit/agent-os"; +import { setup } from "rivetkit"; +import common from "@rivet-dev/agent-os-common"; +import pi from "@rivet-dev/agent-os-pi"; + +const vm = agentOs({ + options: { software: [common, pi] }, +}); + +export const registry = setup({ use: { vm } }); +registry.start(); +``` + + +[Documentation](/docs/agent-os/multiplayer) + +### Agent-to-Agent + +Compose specialized agents into pipelines. Each agent gets its own isolated VM and filesystem. + + +```ts @nocheck server.ts +import { agentOs } from "rivetkit/agent-os"; +import { setup } from "rivetkit"; +import common from "@rivet-dev/agent-os-common"; +import pi from "@rivet-dev/agent-os-pi"; + +const coder = agentOs({ + options: { software: [common, pi] }, +}); +const reviewer = agentOs({ + options: { software: [common, pi] }, +}); + +export const registry = setup({ use: { coder, reviewer } }); +registry.start(); +``` + +```ts @nocheck client.ts +import { createClient } from "rivetkit/client"; +import type { registry } from "./server"; + +const client = createClient("http://localhost:6420"); + +// Coder writes the feature +const coderAgent = client.coder.getOrCreate(["feature-auth"]); +const coderSession = await coderAgent.createSession("pi", { + env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! }, +}); +await coderAgent.sendPrompt(coderSession.sessionId, "Implement the login feature"); + +// Pass files to the reviewer +const src = await coderAgent.readFile("/home/user/src/auth.ts"); +const reviewerAgent = client.reviewer.getOrCreate(["feature-auth"]); +await reviewerAgent.writeFile("/home/user/src/auth.ts", src); + +// Reviewer checks the code +const reviewSession = await reviewerAgent.createSession("pi", { + env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! }, +}); +await reviewerAgent.sendPrompt( + reviewSession.sessionId, + "Review auth.ts for security issues", +); +``` + + +[Documentation](/docs/agent-os/agent-to-agent) + +### Workflows + +Orchestrate multi-step agent tasks with durable workflows that survive crashes and restarts. + +```ts @nocheck +import { agentOs } from "rivetkit/agent-os"; +import common from "@rivet-dev/agent-os-common"; +import pi from "@rivet-dev/agent-os-pi"; +import { actor, setup, workflow } from "rivetkit"; + +const automator = actor({ + workflows: { + fixBug: workflow<{ repo: string; issue: string }>(), + }, + run: async (c) => { + for await (const message of c.workflow.iter("fixBug")) { + const { repo, issue } = message.body; + const agentHandle = c.actors.vm.getOrCreate([`fix-${issue}`]); + + await c.step("clone-repo", async () => { + return agentHandle.exec(`git clone ${repo} /home/user/repo`); + }); + + await c.step("fix-bug", async () => { + const session = await agentHandle.createSession("pi", { + env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! }, + }); + const response = await agentHandle.sendPrompt( + session.sessionId, + `Fix the bug described in issue: ${issue}`, + ); + await agentHandle.closeSession(session.sessionId); + return response; + }); + + await c.step("run-tests", async () => { + return agentHandle.exec("cd /home/user/repo && npm test"); + }); + + await message.complete(); + } + }, +}); + +const vm = agentOs({ + options: { software: [common, pi] }, +}); + +export const registry = setup({ use: { automator, vm } }); +registry.start(); +``` + +[Documentation](/docs/agent-os/workflows) + +### SQLite + +Use actor-local SQLite as structured long-term memory that persists across sessions and sleep/wake cycles. + +```ts @nocheck +import { actor, setup } from "rivetkit"; +import { db } from "rivetkit/db"; + +const memoryAgent = actor({ + db: db({ + onMigrate: async (db) => { + await db.execute(` + CREATE TABLE IF NOT EXISTS memories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + category TEXT NOT NULL, + content TEXT NOT NULL, + created_at INTEGER NOT NULL + ); + `); + }, + }), + actions: { + store: async (c, sessionId: string, category: string, content: string) => { + await c.db.execute( + "INSERT INTO memories (session_id, category, content, created_at) VALUES (?, ?, ?, ?)", + sessionId, category, content, Date.now(), + ); + }, + search: async (c, query: string) => { + return c.db.execute( + "SELECT category, content FROM memories WHERE content LIKE ? ORDER BY created_at DESC LIMIT 20", + `%${query}%`, + ); + }, + }, +}); +``` + +[Documentation](/docs/agent-os/sqlite) + +{/* SKILL_OVERVIEW_END */} diff --git a/website/src/content/docs/agent-os/index.mdx b/website/src/content/docs/agent-os/index.mdx index 39fa75efdb..8a01fc0479 100644 --- a/website/src/content/docs/agent-os/index.mdx +++ b/website/src/content/docs/agent-os/index.mdx @@ -1,600 +1,10 @@ --- -title: "Overview" +title: "Introduction" description: "Run coding agents inside isolated VMs with full filesystem, process, and network control." -skill: true +skill: false --- -import { faRocket } from "@rivet-gg/icons"; - - -agentOS is in preview and the API is subject to change. If you run into issues, please [report them on GitHub](https://github.com/rivet-dev/rivet/issues) or [join our Discord](https://rivet.dev/discord). - - -## Quickstart - - - - Boot a VM and run your first coding agent in minutes - - - -{/* SKILL_OVERVIEW_START */} - -## Features - -- **Isolated VMs**: Each agent gets its own filesystem, processes, and networking. No shared state, no cross-contamination. -- **Multi-Agent Support**: Run Amp, Claude Code, Codex, OpenCode, and PI with a unified API. Swap agents without changing your code. -- **Host Tools**: Expose your JavaScript functions to agents as CLI commands. Direct binding with near-zero latency and automatic code mode for up to 80% token reduction. -- **Persistent State**: Filesystem and transcripts survive sleep/wake cycles automatically. No external database needed. -- **Orchestration**: Workflows, queues, cron jobs, and multi-agent coordination built on Rivet Actors. -- **Hybrid Sandboxes**: Run agents in the lightweight VM by default. Spin up a full sandbox on demand for browsers, compilation, and desktop automation. - -## When to Use agentOS - -- **Coding agents**: Run any coding agent with full OS access, file editing, shell execution, and tool use. -- **Automated pipelines**: CI-like workflows where agents clone repos, fix bugs, run tests, and open PRs. -- **Multi-agent systems**: Coordinators dispatching to specialized agents, review pipelines, planning chains. -- **Scheduled maintenance**: Cron-based agents that audit code, update dependencies, or generate reports. -- **Collaborative workspaces**: Multiple users observing and interacting with the same agent session in realtime. - -## Minimal Project - - -```ts @nocheck client.ts -import { createClient } from "rivetkit/client"; -import type { registry } from "./server"; - -const client = createClient("http://localhost:6420"); -const agent = client.vm.getOrCreate(["my-agent"]); - -// Subscribe to streaming events -agent.on("sessionEvent", (data) => { - console.log(data.event); -}); - -// Create a session and send a prompt -const session = await agent.createSession("pi", { - env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! }, -}); -const response = await agent.sendPrompt( - session.sessionId, - "Write a hello world script to /home/user/hello.js", -); -console.log(response); - -// Read the file the agent created -const content = await agent.readFile("/home/user/hello.js"); -console.log(new TextDecoder().decode(content)); -``` - -```ts @nocheck server.ts -import { agentOs } from "rivetkit/agent-os"; -import { setup } from "rivetkit"; -import common from "@rivet-dev/agent-os-common"; -import pi from "@rivet-dev/agent-os-pi"; - -const vm = agentOs({ - options: { software: [common, pi] }, -}); - -export const registry = setup({ use: { vm } }); -registry.start(); -``` - - -After the quickstart, customize your agent with the [Registry](/agent-os/registry). - -## Quick Reference - -### Sessions & Transcripts - -Create agent sessions, send prompts, and stream responses in realtime. Transcripts are persisted automatically across sleep/wake cycles. - - -```ts @nocheck client.ts -import { createClient } from "rivetkit/client"; -import type { registry } from "./server"; - -const client = createClient("http://localhost:6420"); -const agent = client.vm.getOrCreate(["my-agent"]); - -// Stream events as they arrive -agent.on("sessionEvent", (data) => { - console.log(data.event.method, data.event); -}); - -// Create a session with MCP servers -const session = await agent.createSession("pi", { - env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! }, - mcpServers: [ - { - type: "local", - command: "npx", - args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user"], - env: {}, - }, - ], -}); - -// Send a prompt and wait for the response -const response = await agent.sendPrompt( - session.sessionId, - "List all files in the home directory", -); -console.log(response); -``` - -```ts @nocheck server.ts -import { agentOs } from "rivetkit/agent-os"; -import { setup } from "rivetkit"; -import common from "@rivet-dev/agent-os-common"; -import pi from "@rivet-dev/agent-os-pi"; - -const vm = agentOs({ - options: { software: [common, pi] }, -}); - -export const registry = setup({ use: { vm } }); -registry.start(); -``` - - -[Documentation](/docs/agent-os/sessions) - -### Permissions - -Approve or deny agent tool use with human-in-the-loop patterns or auto-approve for trusted workloads. - - -```ts @nocheck server.ts -import { agentOs } from "rivetkit/agent-os"; -import { setup } from "rivetkit"; -import common from "@rivet-dev/agent-os-common"; -import pi from "@rivet-dev/agent-os-pi"; - -// Auto-approve all permissions server-side -const vm = agentOs({ - onPermissionRequest: async (c, sessionId, request) => { - await c.respondPermission(sessionId, request.permissionId, "always"); - }, - options: { software: [common, pi] }, -}); - -export const registry = setup({ use: { vm } }); -registry.start(); -``` - -```ts @nocheck client.ts -import { createClient } from "rivetkit/client"; -import type { registry } from "./server"; - -const client = createClient("http://localhost:6420"); -const agent = client.vm.getOrCreate(["my-agent"]); - -// Or handle permissions client-side for human-in-the-loop -agent.on("permissionRequest", async (data) => { - console.log("Permission requested:", data.request); - // "once" | "always" | "reject" - await agent.respondPermission(data.sessionId, data.request.permissionId, "once"); -}); -``` - - -[Documentation](/docs/agent-os/permissions) - -### Tools - -Expose your JavaScript functions to agents as CLI commands inside the VM. Agents call them as shell commands with auto-generated flags from Zod schemas. - -```ts @nocheck -import { toolKit, hostTool } from "@rivet-dev/agent-os-core"; -import { z } from "zod"; - -const myTools = toolKit({ - name: "myapp", - description: "Application tools", - tools: { - createTicket: hostTool({ - description: "Create a ticket in the issue tracker", - inputSchema: z.object({ - title: z.string().describe("Ticket title"), - priority: z.enum(["low", "medium", "high"]).describe("Priority level"), - }), - execute: async (input) => { - const ticket = await db.tickets.create(input); - return { id: ticket.id, url: ticket.url }; - }, - }), - }, -}); - -// Agent calls: agentos-myapp createTicket --title "Fix login" --priority high -``` - -[Documentation](/docs/agent-os/tools) - -### Filesystem - -Read, write, and manage files inside the VM. The `/home/user` directory is persisted automatically across sleep/wake cycles. - - -```ts @nocheck client.ts -import { createClient } from "rivetkit/client"; -import type { registry } from "./server"; - -const client = createClient("http://localhost:6420"); -const agent = client.vm.getOrCreate(["my-agent"]); - -// Write a file -await agent.writeFile("/home/user/config.json", JSON.stringify({ key: "value" })); - -// Read a file -const content = await agent.readFile("/home/user/config.json"); -console.log(new TextDecoder().decode(content)); - -// List directory contents recursively -const files = await agent.readdirRecursive("/home/user", { maxDepth: 2 }); -console.log(files); -``` - -```ts @nocheck server.ts -import { agentOs } from "rivetkit/agent-os"; -import { setup } from "rivetkit"; -import common from "@rivet-dev/agent-os-common"; -import pi from "@rivet-dev/agent-os-pi"; - -const vm = agentOs({ - options: { software: [common, pi] }, -}); - -export const registry = setup({ use: { vm } }); -registry.start(); -``` - - -[Documentation](/docs/agent-os/filesystem) - -### Processes & Shell - -Execute commands, spawn long-running processes, and open interactive shells. - - -```ts @nocheck client.ts -import { createClient } from "rivetkit/client"; -import type { registry } from "./server"; - -const client = createClient("http://localhost:6420"); -const agent = client.vm.getOrCreate(["my-agent"]); - -// One-shot execution -const result = await agent.exec("echo hello && ls /home/user"); -console.log("stdout:", result.stdout); -console.log("exit code:", result.exitCode); - -// Spawn a long-running process -agent.on("processOutput", (data) => { - console.log(`[${data.processId}]`, data.output); -}); - -const proc = await agent.spawn("node", ["server.js"]); -console.log("Process ID:", proc.processId); -``` - -```ts @nocheck server.ts -import { agentOs } from "rivetkit/agent-os"; -import { setup } from "rivetkit"; -import common from "@rivet-dev/agent-os-common"; -import pi from "@rivet-dev/agent-os-pi"; - -const vm = agentOs({ - options: { software: [common, pi] }, -}); - -export const registry = setup({ use: { vm } }); -registry.start(); -``` - - -[Documentation](/docs/agent-os/processes) - -### Networking & Previews - -Proxy HTTP requests into VMs with `vmFetch`. Create preview URLs for port forwarding VM services to shareable public URLs. - - -```ts @nocheck client.ts -import { createClient } from "rivetkit/client"; -import type { registry } from "./server"; - -const client = createClient("http://localhost:6420"); -const agent = client.vm.getOrCreate(["my-agent"]); - -// Fetch from a service running inside the VM -const response = await agent.vmFetch(3000, "/api/health"); -console.log("Status:", response.status); - -// Create a preview URL (port forwarding to a public URL) -const preview = await agent.createSignedPreviewUrl(3000); -console.log("Public URL:", preview.path); -console.log("Expires at:", new Date(preview.expiresAt)); -``` - -```ts @nocheck server.ts -import { agentOs } from "rivetkit/agent-os"; -import { setup } from "rivetkit"; -import common from "@rivet-dev/agent-os-common"; -import pi from "@rivet-dev/agent-os-pi"; - -const vm = agentOs({ - options: { software: [common, pi] }, -}); - -export const registry = setup({ use: { vm } }); -registry.start(); -``` - - -[Documentation](/docs/agent-os/networking) - -### Cron Jobs - -Schedule recurring commands and agent sessions with cron expressions. - - -```ts @nocheck client.ts -import { createClient } from "rivetkit/client"; -import type { registry } from "./server"; - -const client = createClient("http://localhost:6420"); -const agent = client.vm.getOrCreate(["my-agent"]); - -// Schedule a command every hour -await agent.scheduleCron({ - schedule: "0 * * * *", - action: { type: "exec", command: "rm", args: ["-rf", "/tmp/cache/*"] }, -}); - -// Schedule an agent session daily at 9 AM -await agent.scheduleCron({ - schedule: "0 9 * * *", - action: { - type: "session", - agent: "pi", - prompt: "Review the codebase for security issues and write a report to /home/user/audit.md", - }, -}); -``` - -```ts @nocheck server.ts -import { agentOs } from "rivetkit/agent-os"; -import { setup } from "rivetkit"; -import common from "@rivet-dev/agent-os-common"; -import pi from "@rivet-dev/agent-os-pi"; - -const vm = agentOs({ - options: { software: [common, pi] }, -}); - -export const registry = setup({ use: { vm } }); -registry.start(); -``` - - -[Documentation](/docs/agent-os/cron) - -### Sandbox Mounting - -agentOS uses a hybrid model: agents run in a lightweight VM by default and spin up a full sandbox on demand for heavy workloads like browsers, compilation, and desktop automation. - -```ts @nocheck -import { agentOs } from "rivetkit/agent-os"; -import { setup } from "rivetkit"; -import common from "@rivet-dev/agent-os-common"; -import pi from "@rivet-dev/agent-os-pi"; - -const vm = agentOs({ - options: { software: [common, pi], - sandbox: { - enabled: true, - }, - }, -}); - -export const registry = setup({ use: { vm } }); -registry.start(); -``` - -[Documentation](/docs/agent-os/sandbox) - -### Multiplayer & Realtime - -Connect multiple clients to the same agent VM. All subscribers see session output, process logs, and shell data in realtime. - - -```ts @nocheck client.ts -import { createClient } from "rivetkit/client"; -import type { registry } from "./server"; - -// Client A: creates the session and sends prompts -const clientA = createClient("http://localhost:6420"); -const agentA = clientA.vm.getOrCreate(["shared-agent"]); -agentA.on("sessionEvent", (data) => console.log("[A]", data.event.method)); - -const session = await agentA.createSession("pi", { - env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! }, -}); -await agentA.sendPrompt(session.sessionId, "Build a REST API"); - -// Client B: observes the same session (separate process) -const clientB = createClient("http://localhost:6420"); -const agentB = clientB.vm.getOrCreate(["shared-agent"]); -agentB.on("sessionEvent", (data) => console.log("[B]", data.event.method)); -// Client B sees the same events as Client A -``` - -```ts @nocheck server.ts -import { agentOs } from "rivetkit/agent-os"; -import { setup } from "rivetkit"; -import common from "@rivet-dev/agent-os-common"; -import pi from "@rivet-dev/agent-os-pi"; - -const vm = agentOs({ - options: { software: [common, pi] }, -}); - -export const registry = setup({ use: { vm } }); -registry.start(); -``` - - -[Documentation](/docs/agent-os/multiplayer) - -### Agent-to-Agent - -Compose specialized agents into pipelines. Each agent gets its own isolated VM and filesystem. - - -```ts @nocheck server.ts -import { agentOs } from "rivetkit/agent-os"; -import { setup } from "rivetkit"; -import common from "@rivet-dev/agent-os-common"; -import pi from "@rivet-dev/agent-os-pi"; - -const coder = agentOs({ - options: { software: [common, pi] }, -}); -const reviewer = agentOs({ - options: { software: [common, pi] }, -}); - -export const registry = setup({ use: { coder, reviewer } }); -registry.start(); -``` - -```ts @nocheck client.ts -import { createClient } from "rivetkit/client"; -import type { registry } from "./server"; - -const client = createClient("http://localhost:6420"); - -// Coder writes the feature -const coderAgent = client.coder.getOrCreate(["feature-auth"]); -const coderSession = await coderAgent.createSession("pi", { - env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! }, -}); -await coderAgent.sendPrompt(coderSession.sessionId, "Implement the login feature"); - -// Pass files to the reviewer -const src = await coderAgent.readFile("/home/user/src/auth.ts"); -const reviewerAgent = client.reviewer.getOrCreate(["feature-auth"]); -await reviewerAgent.writeFile("/home/user/src/auth.ts", src); - -// Reviewer checks the code -const reviewSession = await reviewerAgent.createSession("pi", { - env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! }, -}); -await reviewerAgent.sendPrompt( - reviewSession.sessionId, - "Review auth.ts for security issues", -); -``` - - -[Documentation](/docs/agent-os/agent-to-agent) - -### Workflows - -Orchestrate multi-step agent tasks with durable workflows that survive crashes and restarts. - -```ts @nocheck -import { agentOs } from "rivetkit/agent-os"; -import common from "@rivet-dev/agent-os-common"; -import pi from "@rivet-dev/agent-os-pi"; -import { actor, setup, workflow } from "rivetkit"; - -const automator = actor({ - workflows: { - fixBug: workflow<{ repo: string; issue: string }>(), - }, - run: async (c) => { - for await (const message of c.workflow.iter("fixBug")) { - const { repo, issue } = message.body; - const agentHandle = c.actors.vm.getOrCreate([`fix-${issue}`]); - - await c.step("clone-repo", async () => { - return agentHandle.exec(`git clone ${repo} /home/user/repo`); - }); - - await c.step("fix-bug", async () => { - const session = await agentHandle.createSession("pi", { - env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! }, - }); - const response = await agentHandle.sendPrompt( - session.sessionId, - `Fix the bug described in issue: ${issue}`, - ); - await agentHandle.closeSession(session.sessionId); - return response; - }); - - await c.step("run-tests", async () => { - return agentHandle.exec("cd /home/user/repo && npm test"); - }); - - await message.complete(); - } - }, -}); - -const vm = agentOs({ - options: { software: [common, pi] }, -}); - -export const registry = setup({ use: { automator, vm } }); -registry.start(); -``` - -[Documentation](/docs/agent-os/workflows) - -### SQLite - -Use actor-local SQLite as structured long-term memory that persists across sessions and sleep/wake cycles. - -```ts @nocheck -import { actor, setup } from "rivetkit"; -import { db } from "rivetkit/db"; - -const memoryAgent = actor({ - db: db({ - onMigrate: async (db) => { - await db.execute(` - CREATE TABLE IF NOT EXISTS memories ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - session_id TEXT NOT NULL, - category TEXT NOT NULL, - content TEXT NOT NULL, - created_at INTEGER NOT NULL - ); - `); - }, - }), - actions: { - store: async (c, sessionId: string, category: string, content: string) => { - await c.db.execute( - "INSERT INTO memories (session_id, category, content, created_at) VALUES (?, ?, ?, ?)", - sessionId, category, content, Date.now(), - ); - }, - search: async (c, query: string) => { - return c.db.execute( - "SELECT category, content FROM memories WHERE content LIKE ? ORDER BY created_at DESC LIMIT 20", - `%${query}%`, - ); - }, - }, -}); -``` - -[Documentation](/docs/agent-os/sqlite) - -{/* SKILL_OVERVIEW_END */} +{/* The agentOS overview renders as an icon-grid landing via the DocsLanding +React component (see src/components/docs/DocsLanding.tsx + docsLandings.ts), +wired up in src/pages/docs/[...slug].astro. The crash course content lives in +agent-os/crash-course.mdx. No markdown body here. */} diff --git a/website/src/content/docs/connect/cloudflare.mdx b/website/src/content/docs/connect/cloudflare.mdx deleted file mode 100644 index 60dc516c84..0000000000 --- a/website/src/content/docs/connect/cloudflare.mdx +++ /dev/null @@ -1,154 +0,0 @@ ---- -title: "Deploying to Cloudflare Workers" -description: "Run RivetKit on Cloudflare Workers with the WebAssembly runtime." -skill: true ---- - -Cloudflare Workers run RivetKit through the WebAssembly runtime. Use the public `@rivetkit/rivetkit-wasm` package, pass the bindings through `setup({ wasm })`, and use remote SQLite. - -## Steps - - - - -- [Cloudflare account](https://dash.cloudflare.com/) -- [`wrangler`](https://developers.cloudflare.com/workers/wrangler/) configured for your account -- A Rivet namespace from the [Rivet Dashboard](https://dashboard.rivet.dev/) or a self-hosted Rivet Engine - - - - -```sh -npm install rivetkit @rivetkit/rivetkit-wasm -npm install --save-dev wrangler -``` - - - - -Set your Rivet connection values as Worker variables. The pool name must match the serverless runner configured in Rivet. - -```toml wrangler.toml -name = "rivetkit-cloudflare" -main = "src/index.ts" -compatibility_date = "2025-04-01" -compatibility_flags = ["nodejs_compat"] - -[vars] -RIVET_ENDPOINT = "https://api.rivet.dev" -RIVET_NAMESPACE = "your-namespace" -RIVET_POOL = "cloudflare-workers" -RIVET_TOKEN = "sk_..." -RIVET_PUBLIC_ENDPOINT = "https://your-namespace:pk_...@api.rivet.dev" -``` - - - - -This example uses raw SQL to keep the runtime setup visible. When `runtime: "wasm"` is used, unset SQLite defaults to remote SQLite, and `sqlite: "local"` is rejected. - - -```ts src/index.ts @nocheck -import { actor, setup } from "rivetkit"; -import * as wasmBindings from "@rivetkit/rivetkit-wasm"; -import wasmModule from "@rivetkit/rivetkit-wasm/rivetkit_wasm_bg.wasm"; - -interface Env { - RIVET_ENDPOINT: string; - RIVET_NAMESPACE: string; - RIVET_POOL: string; - RIVET_TOKEN: string; - RIVET_PUBLIC_ENDPOINT: string; -} - -interface SqliteDatabase { - run(sql: string, params?: unknown[]): Promise; - query(sql: string, params?: unknown[]): Promise<{ rows: unknown[][] }>; -} - -const rawSqlDatabaseProvider = { - createClient: async () => ({ - execute: async () => [], - close: async () => {}, - }), - onMigrate: async () => {}, -}; - -const counter = actor({ - db: rawSqlDatabaseProvider, - actions: { - increment: async (ctx, amount = 1) => { - const db = ctx.sql as SqliteDatabase; - await db.run( - "CREATE TABLE IF NOT EXISTS counters (id INTEGER PRIMARY KEY, count INTEGER NOT NULL)", - ); - await db.run( - "INSERT INTO counters (id, count) VALUES (1, ?) ON CONFLICT(id) DO UPDATE SET count = count + excluded.count", - [amount], - ); - - const result = await db.query("SELECT count FROM counters WHERE id = 1"); - return Number(result.rows[0]?.[0] ?? 0); - }, - }, -}); - -const use = { counter }; -let registry: { handler(request: Request): Promise } | undefined; - -function getRegistry(env: Env) { - registry ??= setup({ - runtime: "wasm", - sqlite: "remote", - wasm: { - bindings: wasmBindings, - initInput: wasmModule, - }, - use, - endpoint: env.RIVET_ENDPOINT, - namespace: env.RIVET_NAMESPACE, - token: env.RIVET_TOKEN, - envoy: { - poolName: env.RIVET_POOL, - }, - serverless: { - publicEndpoint: env.RIVET_PUBLIC_ENDPOINT, - }, - }); - - return registry; -} - -export default { - async fetch(request: Request, env: Env): Promise { - return await getRegistry(env).handler(request); - }, -}; -``` - - - - - -```sh -npx wrangler deploy -``` - -After deploy, set the Worker URL with the `/api/rivet` path as the serverless runner URL in Rivet. - - - - -## Runtime Notes - -- Use `runtime: "wasm"` in `setup(...)` for Workers. You can also set `RIVETKIT_RUNTIME=wasm` in environments where the registry config does not set `runtime`. -- Pass `wasm: { bindings, initInput }` explicitly from `@rivetkit/rivetkit-wasm`. -- Use remote SQLite on Workers. Leaving SQLite unset with `runtime: "wasm"` selects remote SQLite automatically. -- Keep `RIVET_PUBLIC_ENDPOINT` pointed at the client-facing Rivet endpoint. Register the Worker URL separately as the serverless runner URL. -- Local Workers runtimes must support outbound WebSockets for the Rivet envoy connection. - -## Related - -- [Quickstart](/docs/actors/quickstart) -- [Supabase Functions](/docs/connect/supabase) -- [SQLite](/docs/actors/sqlite) diff --git a/website/src/content/docs/connect/index.mdx b/website/src/content/docs/connect/index.mdx deleted file mode 100644 index 92b15eef30..0000000000 --- a/website/src/content/docs/connect/index.mdx +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: "Deploy" -description: "Rivet supports deployment to a wide range of platforms, from serverless functions to self-hosted infrastructure." -skill: false ---- - -import { deployOptions } from "@rivetkit/shared-data"; - - - - - {deployOptions.map((option) => ( - - {option.description} - - ))} - diff --git a/website/src/content/docs/connect/rivet-compute.mdx b/website/src/content/docs/connect/rivet-compute.mdx deleted file mode 100644 index e94a40bcfe..0000000000 --- a/website/src/content/docs/connect/rivet-compute.mdx +++ /dev/null @@ -1,163 +0,0 @@ ---- -title: "Deploying to Rivet Compute" -description: "Run your backend on Rivet Compute." -skill: true ---- - - -Rivet Compute is currently in beta. - - - -Using an AI coding agent? Open **Connect** on the [Rivet dashboard](https://dashboard.rivet.dev), select **Rivet Cloud**, and paste the one-shot prompt into your agent and have it connect with Rivet Compute for you. - - -## Steps - - - - -- Your RivetKit app in a GitHub repository - - If you don't have one, see the [Quickstart](/docs/actors/quickstart) page or our [Examples](https://github.com/rivet-dev/rivet/tree/main/examples) -- A [Rivet Cloud](https://dashboard.rivet.dev) account and project - - - - -Rivet Compute runs your app as a short-lived, serverless container. Make sure your server `serve()` or uses `handler()` instead of `startRunner()`: - -```typescript src/server.js @nocheck -import { registry } from "./actors.js"; -import { Hono } from "hono"; -import { serve } from "@hono/node-server"; - -const app = new Hono(); - -// Mount Rivet handler -app.all("/api/rivet/*", (c) => registry.handler(c.req.raw)); - -const PORT = parseInt(process.env.PORT); - -serve({ fetch: app.fetch, port: PORT }); -``` - -See [Runtime Modes](/docs/general/runtime-modes) for details on when to use each mode. - - - - -Create a `Dockerfile` in your project root: - -```dockerfile @nocheck -FROM node:24-alpine -WORKDIR /app -COPY package*.json ./ -RUN npm ci --omit=dev -COPY . . -CMD ["node", "src/server.js"] -``` - - - - -1. Open the [Rivet dashboard](https://dashboard.rivet.dev) and navigate to your project -2. Click **Connect** and select **Rivet Cloud** -3. Copy the **`RIVET_CLOUD_TOKEN`** value shown — this is all you need for deployment - - - - -Add `RIVET_CLOUD_TOKEN` as a secret in your GitHub repository (**Settings → Secrets and variables → Actions**), then create `.github/workflows/deploy.yml`: - -```yaml @nocheck -name: Rivet Deploy - -on: - pull_request: - types: [opened, synchronize, reopened, closed] - push: - branches: [main] - workflow_dispatch: - -concurrency: - group: rivet-deploy-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - rivet-deploy: - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - steps: - - uses: actions/checkout@v4 - - uses: rivet-dev/deploy-action@v1.1.1 - with: - rivet-token: ${{ secrets.RIVET_CLOUD_TOKEN }} -``` - -The `deploy-action` handles everything automatically: - -- Builds your Docker image and pushes it to Rivet's built-in container registry -- Creates a `production` namespace on pushes to `main` -- Creates an isolated `pr-{number}` namespace for each pull request -- Posts a comment on the PR with a link to the Rivet dashboard -- Cleans up the PR namespace when the pull request is closed - - - - -The dashboard shows live status as Rivet Compute provisions your backend: - -| Status | Description | -| --- | --- | -| Provisioning | Allocating compute resources | -| Initializing | Starting the runtime environment | -| Allocating | Assigning the runner to your pool | -| Deploying | Pulling and launching your container | -| Binding | Connecting the runner to the network | -| Ready | Deployment complete | - -Once the status reaches **Ready**, your backend is live and actors are available for connections. - - - -If you are an agent monitoring the deployment via API rather than the dashboard, poll the managed-pool endpoint on the Cloud API. - -The `RIVET_CLOUD_TOKEN` secret is a `cloud_api_*` management token scoped to the Cloud API at `cloud-api.rivet.dev`. Use it for `Authorization: Bearer ...` against the Cloud API. Do not confuse it with a `pk_*` publishable key, which is scoped to the Rivet Engine API at `api.rivet.dev` and will 401 against this endpoint. - -Substitute `$CLOUD_API_URL` (typically `https://cloud-api.rivet.dev`), `$PROJECT`, `$ORG`, `$CLOUD_NAMESPACE`, and `$CLOUD_TOKEN`. - -Poll every 5 seconds until `status` is `ready`. Stop and investigate if `status` is `error`. - -```bash -curl -s "$CLOUD_API_URL/projects/$PROJECT/namespaces/$CLOUD_NAMESPACE/managed-pools/default?org=$ORG" -H "Authorization: Bearer $CLOUD_TOKEN" -``` - - - - - - -## Troubleshooting - - - - -If the status stays in **Provisioning** for more than a few minutes, verify that: - -- The `RIVET_CLOUD_TOKEN` secret is correctly set in your GitHub repository -- The GitHub Actions workflow completed without errors — check the run logs - - - - -If the status shows **Error**, check that your container starts successfully and does not exit immediately. Common causes: - -- The server file is not calling `registry.startRunner()` -- A runtime crash on startup — test the image locally with `docker run` -- The Dockerfile is not listening on the `PORT` environmental variable - - - - diff --git a/website/src/content/docs/connect/supabase.mdx b/website/src/content/docs/connect/supabase.mdx deleted file mode 100644 index 6bd00481b8..0000000000 --- a/website/src/content/docs/connect/supabase.mdx +++ /dev/null @@ -1,141 +0,0 @@ ---- -title: "Deploying to Supabase Functions" -description: "Run RivetKit on Supabase Edge Functions with the WebAssembly runtime." -skill: true ---- - -Supabase Edge Functions run RivetKit through the WebAssembly runtime. Use the public `@rivetkit/rivetkit-wasm` package, load the wasm file with Deno, and use remote SQLite. - -## Steps - - - - -- [Supabase project](https://supabase.com/) -- [Supabase CLI](https://supabase.com/docs/guides/cli) configured for your project -- A Rivet namespace from the [Rivet Dashboard](https://dashboard.rivet.dev/) or a self-hosted Rivet Engine - - - - -```sh -npx supabase functions new rivet -``` - -Add the packages used by the function: - -```sh -npm install rivetkit @rivetkit/rivetkit-wasm -``` - - - - -Supabase Functions run under Deno, so load the wasm bytes from the package export and pass them to `setup({ wasm })`. - - -```ts supabase/functions/rivet/index.ts @nocheck -import { actor, setup } from "rivetkit"; -import * as wasmBindings from "@rivetkit/rivetkit-wasm"; - -interface SqliteDatabase { - run(sql: string, params?: unknown[]): Promise; - query(sql: string, params?: unknown[]): Promise<{ rows: unknown[][] }>; -} - -const wasmModule = await Deno.readFile( - new URL(import.meta.resolve("@rivetkit/rivetkit-wasm/rivetkit_wasm_bg.wasm")), -); - -const rawSqlDatabaseProvider = { - createClient: async () => ({ - execute: async () => [], - close: async () => {}, - }), - onMigrate: async () => {}, -}; - -const counter = actor({ - db: rawSqlDatabaseProvider, - actions: { - increment: async (ctx, amount = 1) => { - const db = ctx.sql as SqliteDatabase; - await db.run( - "CREATE TABLE IF NOT EXISTS counters (id INTEGER PRIMARY KEY, count INTEGER NOT NULL)", - ); - await db.run( - "INSERT INTO counters (id, count) VALUES (1, ?) ON CONFLICT(id) DO UPDATE SET count = count + excluded.count", - [amount], - ); - - const result = await db.query("SELECT count FROM counters WHERE id = 1"); - return Number(result.rows[0]?.[0] ?? 0); - }, - }, -}); - -const registry = setup({ - runtime: "wasm", - sqlite: "remote", - wasm: { - bindings: wasmBindings, - initInput: wasmModule, - }, - use: { counter }, - endpoint: Deno.env.get("RIVET_ENDPOINT"), - namespace: Deno.env.get("RIVET_NAMESPACE"), - token: Deno.env.get("RIVET_TOKEN"), - envoy: { - poolName: Deno.env.get("RIVET_POOL") ?? "supabase-functions", - }, - serverless: { - basePath: "/rivet/api/rivet", - publicEndpoint: Deno.env.get("RIVET_PUBLIC_ENDPOINT"), - }, -}); - -Deno.serve(async (request) => { - return await registry.handler(request); -}); -``` - - - - - -Set the Rivet connection values as Supabase secrets. The pool name must match the serverless runner configured in Rivet. - -```sh -npx supabase secrets set \ - RIVET_ENDPOINT=https://api.rivet.dev \ - RIVET_PUBLIC_ENDPOINT=https://your-namespace:pk_...@api.rivet.dev \ - RIVET_NAMESPACE=your-namespace \ - RIVET_POOL=supabase-functions \ - RIVET_TOKEN=sk_... -``` - - - - -```sh -npx supabase functions deploy rivet -``` - -After deploy, set the function URL with the `/api/rivet` path as the serverless runner URL in Rivet. For a function named `rivet`, this is usually `https://your-project.functions.supabase.co/functions/v1/rivet/api/rivet`. - - - - -## Runtime Notes - -- Use `runtime: "wasm"` in `setup(...)` for Supabase Functions. You can also set `RIVETKIT_RUNTIME=wasm` in environments where the registry config does not set `runtime`. -- Pass `wasm: { bindings, initInput }` explicitly from `@rivetkit/rivetkit-wasm`. -- Use remote SQLite on Supabase Functions. Leaving SQLite unset with `runtime: "wasm"` selects remote SQLite automatically. -- Keep `RIVET_PUBLIC_ENDPOINT` pointed at the client-facing Rivet endpoint. Register the function URL separately as the serverless runner URL. -- Supabase Functions run in Deno, so load the wasm module with Deno-friendly bytes, URL, response, or module input. - -## Related - -- [Quickstart](/docs/actors/quickstart) -- [Cloudflare Workers](/docs/connect/cloudflare) -- [SQLite](/docs/actors/sqlite) diff --git a/website/src/content/docs/connect/aws-ecs.mdx b/website/src/content/docs/deploy/aws-ecs.mdx similarity index 100% rename from website/src/content/docs/connect/aws-ecs.mdx rename to website/src/content/docs/deploy/aws-ecs.mdx diff --git a/website/src/content/docs/connect/aws-lambda.mdx b/website/src/content/docs/deploy/aws-lambda.mdx similarity index 100% rename from website/src/content/docs/connect/aws-lambda.mdx rename to website/src/content/docs/deploy/aws-lambda.mdx diff --git a/website/src/content/docs/deploy/cloudflare.mdx b/website/src/content/docs/deploy/cloudflare.mdx new file mode 100644 index 0000000000..39d0f3d2b8 --- /dev/null +++ b/website/src/content/docs/deploy/cloudflare.mdx @@ -0,0 +1,68 @@ +--- +title: "Deploying to Cloudflare Workers" +description: "Deploy an existing Rivet project to Cloudflare Workers." +skill: true +--- + +This guide covers deploying an existing Rivet project to Cloudflare Workers. + +## Prerequisites + +- [Cloudflare account](https://dash.cloudflare.com/) +- [`wrangler`](https://developers.cloudflare.com/workers/wrangler/) configured for your account +- A Rivet namespace from the [Rivet Dashboard](https://dashboard.rivet.dev/) or a self-hosted Rivet Engine + +## Steps + + + + +Follow the [Cloudflare Workers Quickstart](/docs/actors/quickstart/cloudflare) to set up your project locally. + + + + +Set your Rivet connection values as Worker variables. The pool name must match the serverless runner configured in Rivet. + +```toml wrangler.toml +name = "rivetkit-cloudflare" +main = "src/index.ts" +compatibility_date = "2025-04-01" +compatibility_flags = ["nodejs_compat"] + +[vars] +RIVET_ENDPOINT = "https://api.rivet.dev" +RIVET_NAMESPACE = "your-namespace" +RIVET_POOL = "cloudflare-workers" +RIVET_TOKEN = "sk_..." +RIVET_PUBLIC_ENDPOINT = "https://your-namespace:pk_...@api.rivet.dev" +``` + + + + +```sh +npx wrangler deploy +``` + + + + +After deploy, set the Worker URL with the `/api/rivet` path as the serverless runner URL in Rivet. + + + + +## Runtime Notes + +- Use `runtime: "wasm"` in `setup(...)` for Workers. You can also set `RIVETKIT_RUNTIME=wasm` in environments where the registry config does not set `runtime`. +- Pass `wasm: { bindings, initInput }` explicitly from `@rivetkit/rivetkit-wasm`. +- Use remote SQLite on Workers. Leaving SQLite unset with `runtime: "wasm"` selects remote SQLite automatically. +- Keep `RIVET_PUBLIC_ENDPOINT` pointed at the client-facing Rivet endpoint. Register the Worker URL separately as the serverless runner URL. +- Local Workers runtimes must support outbound WebSockets for the Rivet envoy connection. + +## Related + +- [Cloudflare Workers Quickstart](/docs/actors/quickstart/cloudflare) +- [Deploying to Supabase Functions](/docs/deploy/supabase) +- [SQLite](/docs/actors/sqlite) diff --git a/website/src/content/docs/connect/custom.mdx b/website/src/content/docs/deploy/custom.mdx similarity index 100% rename from website/src/content/docs/connect/custom.mdx rename to website/src/content/docs/deploy/custom.mdx diff --git a/website/src/content/docs/connect/freestyle.mdx b/website/src/content/docs/deploy/freestyle.mdx similarity index 100% rename from website/src/content/docs/connect/freestyle.mdx rename to website/src/content/docs/deploy/freestyle.mdx diff --git a/website/src/content/docs/connect/gcp-cloud-run.mdx b/website/src/content/docs/deploy/gcp-cloud-run.mdx similarity index 100% rename from website/src/content/docs/connect/gcp-cloud-run.mdx rename to website/src/content/docs/deploy/gcp-cloud-run.mdx diff --git a/website/src/content/docs/connect/hetzner.mdx b/website/src/content/docs/deploy/hetzner.mdx similarity index 100% rename from website/src/content/docs/connect/hetzner.mdx rename to website/src/content/docs/deploy/hetzner.mdx diff --git a/website/src/content/docs/deploy/index.mdx b/website/src/content/docs/deploy/index.mdx new file mode 100644 index 0000000000..145a27c81c --- /dev/null +++ b/website/src/content/docs/deploy/index.mdx @@ -0,0 +1,9 @@ +--- +title: "Deploy" +description: "Rivet supports deployment to a wide range of platforms, from serverless functions to self-hosted infrastructure." +skill: false +--- + +{/* The Deploy overview renders as an icon-grid landing via the DocsLanding +React component (see src/components/docs/DocsLanding.tsx + docsLandings.ts), +wired up in src/pages/docs/[...slug].astro. No markdown body. */} diff --git a/website/src/content/docs/connect/kubernetes.mdx b/website/src/content/docs/deploy/kubernetes.mdx similarity index 100% rename from website/src/content/docs/connect/kubernetes.mdx rename to website/src/content/docs/deploy/kubernetes.mdx diff --git a/website/src/content/docs/connect/railway.mdx b/website/src/content/docs/deploy/railway.mdx similarity index 100% rename from website/src/content/docs/connect/railway.mdx rename to website/src/content/docs/deploy/railway.mdx diff --git a/website/src/content/docs/deploy/supabase.mdx b/website/src/content/docs/deploy/supabase.mdx new file mode 100644 index 0000000000..797f12dd2c --- /dev/null +++ b/website/src/content/docs/deploy/supabase.mdx @@ -0,0 +1,63 @@ +--- +title: "Deploying to Supabase Functions" +description: "Deploy an existing Rivet project to Supabase Edge Functions." +skill: true +--- + +This guide covers deploying an existing Rivet project to Supabase Edge Functions. + +## Prerequisites + +- [Supabase project](https://supabase.com/) +- [Supabase CLI](https://supabase.com/docs/guides/cli) configured for your project +- A Rivet namespace from the [Rivet Dashboard](https://dashboard.rivet.dev/) or a self-hosted Rivet Engine + +## Steps + + + + +Follow the [Supabase Functions Quickstart](/docs/actors/quickstart/supabase) to set up your project locally. + + + + +Set the Rivet connection values as Supabase secrets. The pool name must match the serverless runner configured in Rivet. + +```sh +npx supabase secrets set \ + RIVET_ENDPOINT=https://api.rivet.dev \ + RIVET_PUBLIC_ENDPOINT=https://your-namespace:pk_...@api.rivet.dev \ + RIVET_NAMESPACE=your-namespace \ + RIVET_POOL=supabase-functions \ + RIVET_TOKEN=sk_... +``` + + + + +```sh +npx supabase functions deploy rivet +``` + + + + +After deploy, set the function URL with the `/api/rivet` path as the serverless runner URL in Rivet. For a function named `rivet`, this is usually `https://your-project.functions.supabase.co/functions/v1/rivet/api/rivet`. + + + + +## Runtime Notes + +- Use `runtime: "wasm"` in `setup(...)` for Supabase Functions. You can also set `RIVETKIT_RUNTIME=wasm` in environments where the registry config does not set `runtime`. +- Pass `wasm: { bindings, initInput }` explicitly from `@rivetkit/rivetkit-wasm`. +- Use remote SQLite on Supabase Functions. Leaving SQLite unset with `runtime: "wasm"` selects remote SQLite automatically. +- Keep `RIVET_PUBLIC_ENDPOINT` pointed at the client-facing Rivet endpoint. Register the function URL separately as the serverless runner URL. +- Supabase Functions run in Deno, so load the wasm module with Deno-friendly bytes, URL, response, or module input. + +## Related + +- [Supabase Functions Quickstart](/docs/actors/quickstart/supabase) +- [Deploying to Cloudflare Workers](/docs/deploy/cloudflare) +- [SQLite](/docs/actors/sqlite) diff --git a/website/src/content/docs/connect/vercel.mdx b/website/src/content/docs/deploy/vercel.mdx similarity index 66% rename from website/src/content/docs/connect/vercel.mdx rename to website/src/content/docs/deploy/vercel.mdx index 404a180219..0edebed17a 100644 --- a/website/src/content/docs/connect/vercel.mdx +++ b/website/src/content/docs/deploy/vercel.mdx @@ -1,87 +1,54 @@ --- title: "Deploying to Vercel" -description: "Deploy your RivetKit app to Vercel." +description: "Deploy your Next.js Rivet app to Vercel." skill: true --- -## Steps +This guide assumes a Next.js app. - - +## Prerequisites - [Vercel account](https://vercel.com/) -- Your RivetKit app - - If you don't have one, see the [Quickstart](/docs/actors/quickstart) page or our [Examples](https://github.com/rivet-dev/rivet/tree/main/examples) +- A Next.js Rivet app - Access to the [Rivet Cloud](https://dashboard.rivet.dev/) or a [self-hosted Rivet Engine](/docs/general/self-hosting) - - +## Steps -Make sure your project is configured correctly for Vercel deployment. + + - +Follow the [Next.js Quickstart](/docs/actors/quickstart/next-js) to set up your project. - + + Your Next.js project should have the following structure: - `src/app/api/rivet/[...all]/route.ts`: RivetKit route handler -- `src/actors.ts`: Actor definitions and registry +- `src/rivet/registry.ts`: Actor definitions and registry -See the [Next.js quickstart](/docs/actors/quickstart/next-js) or the [Next.js example](https://github.com/rivet-dev/rivet/tree/main/examples/next-js) to get started. +The route handler sets `maxDuration` to extend the serverless function timeout so long-lived actor requests are not cut short: - +```ts src/app/api/rivet/[...all]/route.ts @nocheck +import { toNextHandler } from "@rivetkit/next-js"; +import { registry } from "@/rivet/registry"; - +export const maxDuration = 300; -Your Hono project needs: - -1. A `vercel.json` file with the Hono framework specified: - -```json vercel.json -{ - "framework": "hono" -} +export const { GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS } = toNextHandler(registry); ``` -2. Your server file must import from `"hono"` for Vercel to recognize the framework: + + -```ts src/server.ts @nocheck -// You MUST import from "hono" for Vercel to detect this as a Hono app -import { Hono } from "hono"; -import { registry } from "./actors.ts"; +Set `RIVET_ENDPOINT` and `RIVET_PUBLIC_ENDPOINT` in your Vercel project settings using the URL auth format: -const app = new Hono(); -app.all("/api/rivet/*", (c) => registry.handler(c.req.raw)); -export default app; ``` - -3. Use `.ts` file extensions in imports and configure your `tsconfig.json`: - -```json tsconfig.json -{ - "compilerOptions": { - "allowImportingTsExtensions": true, - "rewriteRelativeImportExtensions": true - } -} +RIVET_ENDPOINT=https://my-namespace:sk_****@api.rivet.dev +RIVET_PUBLIC_ENDPOINT=https://my-namespace:pk_****@api.rivet.dev ``` -See the [Hello World example](https://github.com/rivet-dev/rivet/tree/main/examples/hello-world) for a complete example. - -For more details on Hono deployments, see [Vercel's Hono documentation](https://vercel.com/docs/frameworks/backend/hono). - - - - - -Vercel currently supports Next.js and Hono frameworks for RivetKit deployments. - -For other frameworks, consider deploying to [Railway](/docs/connect/railway), [Kubernetes](/docs/connect/kubernetes), or another platform. - - - - +`RIVET_ENDPOINT` uses the secret token for server-side access. `RIVET_PUBLIC_ENDPOINT` uses the publishable token and tells the metadata endpoint what connection info to provide to clients. @@ -182,3 +149,8 @@ If using the [preview-namespace-action](https://github.com/rivet-dev/preview-nam + +## Related + +- [Next.js Quickstart](/docs/actors/quickstart/next-js) +- [Self-Hosting](/docs/general/self-hosting) diff --git a/website/src/content/docs/connect/vm-and-bare-metal.mdx b/website/src/content/docs/deploy/vm-and-bare-metal.mdx similarity index 100% rename from website/src/content/docs/connect/vm-and-bare-metal.mdx rename to website/src/content/docs/deploy/vm-and-bare-metal.mdx diff --git a/website/src/content/docs/general/pool-configuration.mdx b/website/src/content/docs/general/pool-configuration.mdx index 553155840e..81f558e670 100644 --- a/website/src/content/docs/general/pool-configuration.mdx +++ b/website/src/content/docs/general/pool-configuration.mdx @@ -13,7 +13,11 @@ There are two pool kinds: ## Setting the Configuration -Configure a pool via the dashboard, the API directly, or the TypeScript SDK: +Configure a pool from the [Rivet dashboard](https://dashboard.rivet.dev) under your namespace's runner settings. The dashboard is the recommended way to manage pool configuration. + + + +You can also set pool configuration directly through the API or the TypeScript SDK: @@ -64,6 +68,8 @@ curl -X PUT "https://api.rivet.dev/runner-configs/default?namespace=default" \ The HTTP API uses `snake_case`. The TypeScript SDK uses `camelCase`. The field names below use `camelCase`. + + ## Common Options These options apply to both `normal` and `serverless` pools. diff --git a/website/src/content/docs/general/runtime-modes.mdx b/website/src/content/docs/general/runtime-modes.mdx index c991eec3a6..4327e483aa 100644 --- a/website/src/content/docs/general/runtime-modes.mdx +++ b/website/src/content/docs/general/runtime-modes.mdx @@ -135,6 +135,8 @@ const registry = setup({ }); ``` +See [Pool Configuration](/docs/general/pool-configuration) for how pools are scaled, drained on version upgrades, and rate-limited during actor eviction. + ## Comparison | Mode | Method | Use Case | diff --git a/website/src/content/docs/quickstart/index.mdx b/website/src/content/docs/quickstart/index.mdx index 9226d7125a..289cd6daf5 100644 --- a/website/src/content/docs/quickstart/index.mdx +++ b/website/src/content/docs/quickstart/index.mdx @@ -6,12 +6,12 @@ skill: false import { faCloudflare, - faFunction, faNodeJs, faReact, faNextjs, faRust, } from "@rivet-gg/icons"; +import { faSupabase } from "@rivetkit/shared-data"; - Run RivetKit on Cloudflare Workers with the WebAssembly runtime + Run RivetKit on Cloudflare Workers - Run RivetKit on Supabase Edge Functions with the WebAssembly runtime + Run RivetKit on Supabase Edge Functions diff --git a/website/src/content/docs/self-hosting/index.mdx b/website/src/content/docs/self-hosting/index.mdx index 0136aa3413..8b9ac005ba 100644 --- a/website/src/content/docs/self-hosting/index.mdx +++ b/website/src/content/docs/self-hosting/index.mdx @@ -22,7 +22,7 @@ Rivet supports both BYOC (Bring Your Own Cloud) and self-hosting to fit your dep | **Air-Gapped Deployments** | Yes | No | | **Best For** | Air-gapped environments, strict compliance, custom security policies | All other production deployments | | **Support** | [Contact sales](/sales) or community | Community, Slack, and email (varies by plan) | -| **Documentation** | Continue below | [See connect guides](/docs/connect) | +| **Documentation** | Continue below | [See connect guides](/docs/deploy) | ## Architecture # diff --git a/website/src/content/docs/self-hosting/render.mdx b/website/src/content/docs/self-hosting/render.mdx index ac085fa9ea..718095f3ac 100644 --- a/website/src/content/docs/self-hosting/render.mdx +++ b/website/src/content/docs/self-hosting/render.mdx @@ -116,7 +116,7 @@ RIVET_ENDPOINT=https://:@rivet-engine-xxxx.onrender.com RIVET_PUBLIC_ENDPOINT=https://@rivet-engine-xxxx.onrender.com ``` -See the [Connect guide](/docs/connect/custom) for more details on connecting your application. +See the [Connect guide](/docs/deploy/custom) for more details on connecting your application. ## Next Steps diff --git a/website/src/metadata/skills.ts b/website/src/metadata/skills.ts index 0cc2bcd56d..136568ce88 100644 --- a/website/src/metadata/skills.ts +++ b/website/src/metadata/skills.ts @@ -50,7 +50,7 @@ const BASE_SKILL_CONFIGS = { baseTemplate: skillBaseRivetkit, content: { collection: "docs", - docId: "actors/index", + docId: "actors/crash-course", fallbackDocIds: ["actors"], startMarker: "{/* SKILL_OVERVIEW_START */}", endMarker: "{/* SKILL_OVERVIEW_END */}", diff --git a/website/src/pages/docs/[...slug].astro b/website/src/pages/docs/[...slug].astro index 042e400b4c..92e04225be 100644 --- a/website/src/pages/docs/[...slug].astro +++ b/website/src/pages/docs/[...slug].astro @@ -5,6 +5,8 @@ import { DocsNavigation } from '@/components/DocsNavigation'; import { DocsTableOfContents } from '@/components/DocsTableOfContents'; import { DocsPageDropdown } from '@/components/DocsPageDropdown'; import { Prose } from '@/components/Prose'; +import { DocsLanding } from '@/components/docs/DocsLanding'; +import { docsLandings } from '@/components/docs/docsLandings'; import { Button } from '@rivet-gg/components'; import { Icon, faPencil } from '@rivet-gg/icons'; import { sitemap } from '@/sitemap/mod'; @@ -50,6 +52,11 @@ const tableOfContents = headings const { title, description } = entry.data as { title: string; description: string }; const slugPath = getContentSlugPath(entry.id); +// Section overviews (actors, deploy, agent-os) render as a custom icon-grid +// landing instead of the standard prose article. The markdown chrome +// (edit button, breadcrumb, TOC, page dropdown) is suppressed for these. +const landing = docsLandings[slugPath ?? '']; +const isLanding = Boolean(landing); const fullPath = slugPath ? `/docs/${slugPath}/` : '/docs/'; const foundTab = findActiveTab(fullPath, sitemap); const parentPage = foundTab?.page?.parent; @@ -90,46 +97,56 @@ const breadcrumbSchema = {
-
-
-
- -
-
- - {parentPage && ( -
- {parentPage.title} +
+ {landing ? ( + landing.logo === 'agentos' ? ( + + ) : ( + + ) + ) : ( + +
+
+ +
- )} -

{title}

- {description &&

{description}

} - - -
- - {lastModifiedFormatted && ( - - Last updated {lastModifiedFormatted} - - )} -
+ + {parentPage && ( +
+ {parentPage.title} +
+ )} +

{title}

+ {description &&

{description}

} + +
+
+ + {lastModifiedFormatted && ( + + Last updated {lastModifiedFormatted} + + )} +
+
+ )}
- {tableOfContents.length > 0 && ( + {!isLanding && tableOfContents.length > 0 && ( diff --git a/website/src/sitemap/mod.ts b/website/src/sitemap/mod.ts index 2df20006c9..5f886c6b56 100644 --- a/website/src/sitemap/mod.ts +++ b/website/src/sitemap/mod.ts @@ -27,7 +27,6 @@ import { faFingerprint, faFloppyDisk, faForward, - faFunction, faBoxesStacked, faGear, faGlobe, @@ -82,7 +81,7 @@ import { faHardDrive, faMessages, } from "@rivet-gg/icons"; -import { deployOptions, type DeployOption } from "@rivetkit/shared-data"; +import { deployOptions, faSupabase, type DeployOption } from "@rivetkit/shared-data"; import nextjs from "@/images/vendors/next-js.svg"; import type { SidebarItem, Sitemap } from "@/lib/sitemap"; @@ -104,7 +103,7 @@ export const sitemap = [ title: "General", pages: [ { - title: "Overview", + title: "Introduction", href: "/docs/actors", icon: faSquareInfo, }, @@ -113,11 +112,6 @@ export const sitemap = [ icon: faFastForward, collapsible: true, pages: [ - { - title: "Overview", - href: "/docs/actors/quickstart", - icon: faSquareInfo, - }, { title: "Node.js & Bun", href: "/docs/actors/quickstart/backend", @@ -145,6 +139,16 @@ export const sitemap = [ icon: faLayerGroup, badge: "Beta", }, + { + title: "Cloudflare Workers", + href: "/docs/actors/quickstart/cloudflare", + icon: faCloudflare, + }, + { + title: "Supabase Functions", + href: "/docs/actors/quickstart/supabase", + icon: faSupabase, + }, ], }, ] @@ -153,7 +157,7 @@ export const sitemap = [ title: "Features", pages: [ { - title: "State & Storage", + title: "In-Memory State", href: "/docs/actors/state", icon: faFloppyDisk, }, @@ -215,6 +219,10 @@ export const sitemap = [ { title: "Concepts", pages: [ + { + title: "Crash Course", + href: "/docs/actors/crash-course", + }, { title: "Design Patterns", // icon: faLayerGroup, @@ -321,10 +329,6 @@ export const sitemap = [ title: "Debugging", href: "/docs/actors/debugging", }, - { - title: "Custom Inspector Tabs", - href: "/docs/actors/inspector-tabs", - }, { title: "Types", href: "/docs/actors/types", @@ -343,6 +347,10 @@ export const sitemap = [ title: "Icons & Names", href: "/docs/actors/appearance", }, + { + title: "Custom Inspector Tabs", + href: "/docs/actors/inspector-tabs", + }, { title: "Limits", href: "/docs/actors/limits", @@ -494,7 +502,7 @@ export const sitemap = [ title: "General", pages: [ { - title: "Overview", + title: "Introduction", href: "/docs/agent-os", icon: faSquareInfo, }, @@ -503,6 +511,11 @@ export const sitemap = [ href: "/docs/agent-os/quickstart", icon: faRocket, }, + { + title: "Crash Course", + href: "/docs/agent-os/crash-course", + icon: faLightbulb, + }, { title: "agentOS vs Sandbox", href: "/docs/agent-os/versus-sandbox", @@ -721,15 +734,15 @@ export const sitemap = [ // }, { - title: "Connect", - href: "/docs/connect", + title: "Deploy", + href: "/docs/deploy", sidebar: [ { title: "General", pages: [ { - title: "Overview", - href: "/docs/connect", + title: "Introduction", + href: "/docs/deploy", icon: faSquareInfo, }, ]