diff --git a/CLAUDE.md b/CLAUDE.md index 8fc96a485..35084db79 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Always include a README.md for new packages. The `README.md` should always follow this structure: ```md - # RivetKit {subname, e.g. library: RivetKit Actors, driver and platform: RivetKit Redis Adapter, RivetKit Cloudflare Workers Adapter} + # RivetKit {subname, e.g. library: Rivet Actor, driver and platform: RivetKit Redis Adapter, RivetKit Cloudflare Workers Adapter} _Lightweight Libraries for Backends_ diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md new file mode 100644 index 000000000..78c4a61c5 --- /dev/null +++ b/docs/CLAUDE.md @@ -0,0 +1,49 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Documentation Project + +This is the documentation site for RivetKit built with Mintlify. The documentation covers RivetKit's actor-based stateful serverless framework and related integrations. + +## Documentation Structure + +- **actors/**: Core actor system documentation (state, actions, events, scheduling) +- **clients/**: Client libraries for JavaScript/TypeScript, React, Rust +- **drivers/**: Storage drivers (Memory, File System, Redis, Cloudflare Workers, Rivet) +- **general/**: Architecture, authentication, testing, logging, CORS +- **integrations/**: Framework integrations (Hono, Express, Elysia, tRPC, Better Auth, Vitest) +- **snippets/**: Reusable content components for landing page + +## Key Documentation Files + +- `docs.json`: Mintlify configuration with navigation structure + +## Documentation Style Guide + +### File Structure +- Use `.mdx` extension for all documentation files +- Include frontmatter with `title`, `description`, and `sidebarTitle` +- Use `icon` field for navigation icons (from Font Awesome icon set) + +### Content Guidelines +- **Concise and Direct**: Keep explanations brief and actionable +- **Code-First**: Lead with practical examples, then explain concepts +- **Use Cases Focus**: Emphasize practical applications over theoretical details +- **Progressive Disclosure**: Start simple, link to detailed guides + +### Code Examples +- Use TypeScript for all code examples +- Show complete, runnable examples when possible +- Include both actor and client code where relevant +- Follow RivetKit naming conventions (`actor`, `c` for context, etc.) + +### Navigation +- Group related concepts under clear categories +- Use descriptive but short sidebar titles +- Maintain consistent icon usage for categories + +## Development Resources + +- Refer to ../packages/core/fixtures/driver-test-suite/*.ts for examples of working actor definitions +- Refer to ../examples/* for fully working projects. This is especially helpful when writing guides for integrations, etc. diff --git a/docs/actors/actions.mdx b/docs/actors/actions.mdx index 7a771fda4..26d58a04f 100644 --- a/docs/actors/actions.mdx +++ b/docs/actors/actions.mdx @@ -17,7 +17,7 @@ actor. Actions are defined in the `actions` object when creating a actor: ```typescript -import { actor } from "rivetkit"; +import { actor } from "@rivetkit/actor"; const mathUtils = actor({ state: {}, @@ -37,7 +37,7 @@ Each action receives a context object (commonly named `c`) as its first paramete You can define helper functions outside the actions object to keep your code organized. These functions cannot be called directly by clients: ```typescript -import { actor } from "rivetkit"; +import { actor } from "@rivetkit/actor"; // Private helper function - not callable by clients const calculateFee = (amount) => { @@ -62,7 +62,7 @@ const paymentProcessor = actor({ ### Streaming Return Data -Actions have a single return value. To stream realtime data in response to an action, use [events](/concepts/events). +Actions have a single return value. To stream realtime data in response to an action, use [events](/actors/events). ## Calling Actions @@ -89,7 +89,7 @@ The actor client includes type safety out of the box. When you use `createClient ```typescript src/index.ts -import { setup } from "rivetkit"; +import { setup } from "@rivetkit/actor"; // Create simple counter const counter = actor({ @@ -140,7 +140,7 @@ For example: ```typescript actor.ts -import { actor, UserError } from "rivetkit"; +import { actor, UserError } from "@rivetkit/actor"; const user = actor({ state: { users: [] }, @@ -188,7 +188,7 @@ Data schemas are not validated by default. For production applications, use a li For example, to validate action parameters: ```typescript -import { actor, UserError } from "rivetkit"; +import { actor, UserError } from "@rivetkit/actor"; import { z } from "zod"; // Define schema for action parameters @@ -222,7 +222,7 @@ const counter = actor({ ## Authentication -By default, clients can call all actions on a actor without restriction. Make sure to implement authentication if needed. Documentation on authentication is available [here](/concepts/authentication). +By default, clients can call all actions on a actor without restriction. Make sure to implement authentication if needed. Documentation on authentication is available [here](/general/authentication). ## Using `ActionContext` Type Externally @@ -231,7 +231,7 @@ When writing complex logic for actions, you may want to extract parts of your im RivetKit provides the `ActionContextOf` utility type for exactly this purpose: ```typescript -import { actor, ActionContextOf } from "rivetkit"; +import { actor, ActionContextOf } from "@rivetkit/actor"; const counter = actor({ state: { count: 0 }, @@ -250,4 +250,4 @@ function incrementCount(c: ActionContextOf) { } ``` -See [Helper Types](/concepts/types) for more details on using `ActionContextOf` and other type utilities. +See [Helper Types](/actors/helper-types) for more details on using `ActionContextOf` and other type utilities. diff --git a/docs/actors/authentication.mdx b/docs/actors/authentication.mdx deleted file mode 100644 index 83927b854..000000000 --- a/docs/actors/authentication.mdx +++ /dev/null @@ -1,157 +0,0 @@ ---- -title: Authentication -icon: fingerprint ---- - -Authentication can be handled through the `onBeforeConnect` or `createConnState` lifecycle hook, which acts as middleware before allowing clients to interact with your actor. - -## Using `onBeforeConnect` or `createConnState` - -The `onBeforeConnect` and `createConnState` hook is called whenever a new client attempts to connect to your actor. It receives a context object that contains the client's connection parameters. `createConnState` should return an object that will become the connection state. - -Throwing an error in `onBeforeConnect` or `createConnState` will abort the connection. - -Here's a basic example: - -```typescript -import { actor } from "rivetkit"; - -const exampleActor = actor({ - state: { - // Actor state... - }, - - createConnState: async (c, { params }) => { - // Verify the token with your authentication system - const userData = await myValidateAuthToken(params.authToken); - if (!userData) { - throw new Error('Invalid auth token'); - } - - // Return the user data to store with the connection - return { - userId: userData.id, - role: userData.role - }; - }, - - actions: { - // Actor actions... - } -}); -``` - -## Accessing Connection State - -After authentication, you can access the connection state in any action through the context object: - -```typescript -import { actor } from "rivetkit"; - -const authenticatedActor = actor({ - state: { - // Actor state... - }, - - createConnState: (c) => { - // Authentication logic... - return { - userId: "user_123", - role: "admin" - }; - }, - - actions: { - exampleAdminCommand: (c) => { - // Example of validating admin access - if (c.conn.state.role !== 'admin') { - throw new Error('User must be an admin'); - } - - // Admin-only functionality... - return { success: true }; - } - } -}); -``` - -## Integration Examples - -### With API Server Authentication - -```typescript -import { actor } from "rivetkit"; - -const apiAuthenticatedActor = actor({ - state: { - // Actor state... - }, - - createConnState: async (c, { params }) => { - // Validate API key with your server - const response = await fetch('https://api.yourserver.com/validate', { - method: 'POST', - headers: { - Authorization: `Bearer ${params.apiKey}` - } - }); - - if (!response.ok) { - throw new Error('Invalid API key'); - } - - const user = await response.json(); - - return { - userId: user.id - }; - }, - - actions: { - // Actor actions... - } -}); -``` - -When authentication fails, throwing an error in `createConnState` will prevent the connection from being established, and the client will receive the error message. - -### With JWT Authentication - -```typescript -import { actor } from "rivetkit"; -import jwt from "jsonwebtoken"; - -const JWT_SECRET = process.env.JWT_SECRET; - -const jwtAuthenticatedActor = actor({ - state: { - // Actor state... - }, - - createConnState: (c, { params }) => { - try { - // Verify JWT token - const decoded = jwt.verify(params.jwt, JWT_SECRET); - - return { - userId: decoded.sub, - permissions: decoded.permissions - }; - } catch (error) { - throw new Error('Invalid or expired JWT token'); - } - }, - - actions: { - secureAction: (c, data) => { - // Check permissions before proceeding - if (!c.conn.state.permissions.includes('write')) { - throw new Error('Permission denied: requires write access'); - } - - // Perform action with data... - return { success: true }; - } - } -}); -``` diff --git a/docs/actors/communicating-between-actors.mdx b/docs/actors/communicating-between-actors.mdx new file mode 100644 index 000000000..d7831f389 --- /dev/null +++ b/docs/actors/communicating-between-actors.mdx @@ -0,0 +1,244 @@ +--- +title: Communicating Between Actors +description: Learn how actors can call other actors and share data +icon: arrow-right-arrow-left +--- + +Actors can communicate with each other using the inline client, enabling complex workflows and data sharing between different actor instances. + + +This guide focuses on communication between actors within the same application. For connecting to actors from client applications, see [Communicating with Actors](/actors/communicating-with-actors). + + +## Using the Inline Client + +The inline client allows actors to call other actors within the same registry. Access it via `c.client()` in your actor actions: + +```typescript +import { actor } from "@rivetkit/actor"; +import type { registry } from "./registry"; + +export const orderProcessor = actor({ + state: { orders: [] as Order[] }, + + actions: { + processOrder: async (c, order: Order) => { + // Get the inline client with registry types + const client = c.client(); + + // Call another actor to check inventory + const inventory = client.inventory.getOrCreate([order.productId]); + const available = await inventory.checkStock(order.quantity); + + if (!available) { + throw new UserError("Insufficient stock"); + } + + // Reserve the stock + await inventory.reserveStock(order.quantity); + + // Process payment through payment actor + const payment = client.payment.getOrCreate([order.customerId]); + const result = await payment.processPayment(order.amount); + + // Update order state + c.state.orders.push({ + ...order, + status: "processed", + paymentId: result.paymentId, + }); + + return { success: true, orderId: order.id }; + } + } +}); +``` + +## Communication Patterns + +The inline client supports the same communication patterns as external clients. See [Communicating with Actors - Actor Handles](/actors/communicating-with-actors#actor-handles) for details on: + +- `getOrCreate()` for stateless request-response +- `.connect()` for real-time communication with events +- `get()` and `create()` for explicit actor lifecycle management + +## Error Handling + +Handle errors gracefully when calling other actors. Error handling works the same as with external clients - see [Communicating with Actors - Error Handling](/actors/communicating-with-actors#error-handling) for details. + +```typescript +export const orderActor = actor({ + state: { orders: [] }, + + actions: { + createOrder: async (c, orderData: CreateOrderData) => { + const client = c.client(); + + try { + const inventory = client.inventory.getOrCreate([orderData.productId]); + await inventory.validateOrder(orderData); + + const order = { id: generateId(), ...orderData, status: "validated" }; + c.state.orders.push(order); + return order; + + } catch (error) { + throw new UserError(`Order validation failed: ${error.message}`); + } + } + } +}); +``` + +## Use Cases and Patterns + +### Actor Orchestration + +Use a coordinator actor to manage complex workflows: + +```typescript +export const workflowActor = actor({ + state: { workflows: new Map() }, + + actions: { + executeUserOnboarding: async (c, userId: string) => { + const client = c.client(); + + // Coordinate multiple actors for complex workflow + const user = client.user.getOrCreate([userId]); + await user.createProfile(); + + const notification = client.notification.getOrCreate(["system"]); + await notification.sendWelcomeEmail(userId); + + const preferences = client.preferences.getOrCreate([userId]); + await preferences.setDefaults(); + + return { status: "completed" }; + } + } +}); +``` + +### Data Aggregation + +Collect data from multiple actors: + +```typescript +export const analyticsActor = actor({ + state: { reports: [] }, + + actions: { + generateUserReport: async (c, userId: string) => { + const client = c.client(); + + // Collect data from multiple sources in parallel + const [profile, orders, preferences] = await Promise.all([ + client.user.getOrCreate([userId]).getProfile(), + client.orders.getOrCreate([userId]).getHistory(), + client.preferences.getOrCreate([userId]).getAll(), + ]); + + return { profile, orders, preferences }; + } + } +}); +``` + +### Event-Driven Architecture + +Use connections to listen for events from other actors: + +```typescript +export const auditLogActor = actor({ + state: { logs: [] }, + + actions: { + startAuditing: async (c, userId: string) => { + const client = c.client(); + const user = client.user.getOrCreate([userId]); + const connection = user.connect(); + + // Listen for events from the user actor + connection.on("profileUpdated", (profile) => { + c.state.logs.push({ + type: "profile_update", + userId, + timestamp: Date.now(), + data: profile, + }); + }); + + return { status: "auditing_started" }; + } + } +}); +``` + +## Advanced Features + +### Type Safety + +The inline client maintains full type safety across actor boundaries: + +```typescript +export const typedActor = actor({ + actions: { + processData: async (c) => { + const client = c.client(); + + // TypeScript validates action signatures + const counter = client.counter.getOrCreate(["stats"]); + + const count: number = await counter.increment(1); // ✓  + const invalid = await counter.increment("1"); // ✗ Type error + + return count; + } + } +}); +``` + +### Performance Optimization + +**Batch Operations**: Process multiple items in parallel: + +```typescript +// Process items in parallel +const results = await Promise.all( + items.map(item => client.processor.getOrCreate([item.type]).process(item)) +); +``` + +**Connection Reuse**: Reuse connections for multiple operations: + +```typescript +const connection = client.targetActor.getOrCreate(["shared"]).connect(); +try { + for (const op of operations) { + await connection.performOperation(op); + } +} finally { + await connection.dispose(); +} +``` + +### Testing + +Mock the inline client for unit testing: + +```typescript +const mockClient = { + inventory: { + getOrCreate: jest.fn().mockReturnValue({ + checkStock: jest.fn().mockResolvedValue(true), + }), + }, +}; + +// Test with mocked dependencies +const result = await orderProcessor.processOrder.call( + { client: () => mockClient }, + orderData +); +``` diff --git a/docs/actors/communicating-with-actors.mdx b/docs/actors/communicating-with-actors.mdx new file mode 100644 index 000000000..38b9f9536 --- /dev/null +++ b/docs/actors/communicating-with-actors.mdx @@ -0,0 +1,518 @@ +--- +title: Communicating with Actors +description: Learn how to call actions and connect to actors from client applications +icon: arrow-right-arrow-left +--- + +This guide covers how to connect to and interact with actors from client applications using RivetKit's JavaScript/TypeScript client library. + +## Client Setup + +### Creating a Client + +There are several ways to create a client for communicating with actors: + + + + For frontend applications or external services connecting to your RivetKit backend: + + ```typescript + import { createClient } from "@rivetkit/actor/client"; + import type { registry } from "./registry"; + + const client = createClient("http://localhost:8080"); + ``` + + This client communicates over HTTP/WebSocket and requires authentication. + + + + From your backend server that hosts the registry: + + ```typescript + import { registry } from "./registry"; + + const { client } = registry.createServer(); + ``` + + This client bypasses network calls and doesn't require authentication. + + + + From within an actor to communicate with other actors: + + ```typescript + const myActor = actor({ + actions: { + callOtherActor: (c) => { + const client = c.client(); + return client.otherActor.getOrCreate(["id"]); + } + } + }); + ``` + + Read more about [communicating between actors](/actors/communicating-between-actors). + + + +### Client Configuration + +Configure the client with additional options: + +```typescript +const client = createClient("http://localhost:8080", { + // Data serialization format + encoding: "cbor", // or "json" + + // Network transports in order of preference + supportedTransports: ["websocket", "sse"] +}); +``` + +## Actor Handles + +### `get(tags, opts?)` - Find Existing Actor + +Returns a handle to an existing actor or `null` if it doesn't exist: + +```typescript +// Get existing actor by tags +const handle = client.myActor.get(["actor-id"]); + +if (handle) { + const result = await handle.someAction(); +} else { + console.log("Actor doesn't exist"); +} +``` + +### `getOrCreate(tags, input?, opts?)` - Find or Create Actor + +Returns a handle to an existing actor or creates a new one if it doesn't exist: + +```typescript +// Get or create actor (synchronous) +const counter = client.counter.getOrCreate(["my-counter"]); + +// With initialization input +const game = client.game.getOrCreate(["game-123"], { + gameMode: "tournament", + maxPlayers: 8, +}); + +// Call actions immediately +const count = await counter.increment(5); +``` + + +`get()` and `getOrCreate()` are synchronous and return immediately. The actor is created lazily when you first call an action. + + +### `create(tags, input?, opts?)` - Create New Actor + +Explicitly creates a new actor instance, failing if one already exists: + +```typescript +// Create new actor (async) +const newGame = await client.game.create(["game-456"], { + gameMode: "classic", + maxPlayers: 4, +}); + +// Actor is guaranteed to be newly created +await newGame.initialize(); +``` + +### `getWithId(id, opts?)` - Find by Internal ID + +Connect to an actor using its internal system ID: + +```typescript +// Connect by internal ID +const actorId = "55425f42-82f8-451f-82c1-6227c83c9372"; +const actor = client.myActor.getWithId(actorId); + +await actor.performAction(); +``` + + +Prefer using tags over internal IDs for actor discovery. IDs are primarily for debugging and advanced use cases. + + +## Actions + +### Calling Actions + +Once you have an actor handle, call actions directly. All action calls are async: + +```typescript +const counter = client.counter.getOrCreate(["my-counter"]); + +// Call action with no arguments +const currentCount = await counter.getCount(); + +// Call action with arguments +const newCount = await counter.increment(5); + +// Call action with object parameter +await counter.updateSettings({ + step: 2, + maximum: 100, +}); +``` + +### Action Parameters + +Actions receive parameters exactly as defined in the actor: + +```typescript +// Actor definition +const chatRoom = actor({ + actions: { + sendMessage: (c, userId: string, message: string, metadata?: object) => { + // Action implementation + } + } +}); + +// Client usage - parameters match exactly +await chatRoom.sendMessage("user-123", "Hello!", { priority: "high" }); +``` + +### Error Handling + +Handle action errors appropriately: + +```typescript +try { + const result = await counter.increment(5); +} catch (error) { + if (error.code === "RATE_LIMITED") { + console.log("Too many requests, try again later"); + } else { + console.error("Action failed:", error.message); + } +} +``` + +## Real-time Connections + +Real-time connections enable bidirectional communication between clients and actors through persistent connections. RivetKit automatically negotiates between WebSocket (preferred for full duplex) and Server-Sent Events (SSE) as a fallback for restrictive environments. + +### `connect(params?)` - Establish Stateful Connection + +For real-time communication with events, use `.connect()`: + +```typescript +const counter = client.counter.getOrCreate(["live-counter"]); +const connection = counter.connect(); + +// Listen for events +connection.on("countChanged", (newCount: number) => { + console.log("Count updated:", newCount); +}); + +// Call actions through the connection +const result = await connection.increment(1); + +// Clean up when done +await connection.dispose(); +``` + +### Events + +#### `on(eventName, callback)` - Listen for Events + +Listen for events from the actor: + +```typescript +// Listen for chat messages +connection.on("messageReceived", (message) => { + console.log(`${message.from}: ${message.text}`); +}); + +// Listen for game state updates +connection.on("gameStateChanged", (gameState) => { + updateUI(gameState); +}); + +// Listen for player events +connection.on("playerJoined", (player) => { + console.log(`${player.name} joined the game`); +}); +``` + +#### `once(eventName, callback)` - Listen Once + +Listen for an event only once: + +```typescript +// Wait for game to start +connection.once("gameStarted", () => { + console.log("Game has started!"); +}); +``` + +#### `off(eventName, callback?)` - Stop Listening + +Remove event listeners: + +```typescript +const messageHandler = (message) => console.log(message); + +// Add listener +connection.on("messageReceived", messageHandler); + +// Remove specific listener +connection.off("messageReceived", messageHandler); + +// Remove all listeners for event +connection.off("messageReceived"); +``` + +### `dispose()` - Clean Up Connection + +Always dispose of connections when finished to free up resources: + +```typescript +const connection = actor.connect(); + +try { + // Use the connection + connection.on("event", handler); + await connection.someAction(); +} finally { + // Clean up the connection + await connection.dispose(); +} + +// Or with automatic cleanup in React/frameworks +useEffect(() => { + const connection = actor.connect(); + + return () => { + connection.dispose(); + }; +}, []); +``` + +**Important:** Disposing a connection: +- Closes the underlying WebSocket or SSE connection +- Removes all event listeners +- Cancels any pending reconnection attempts +- Prevents memory leaks in long-running applications + +### Transports + +Connections automatically negotiate the best available transport: + +#### WebSocket Transport +- **Full duplex**: Client can send and receive +- **Low latency**: Immediate bidirectional communication +- **Preferred**: Used when available + +#### Server-Sent Events (SSE) +- **Server-to-client**: Events only, actions via HTTP +- **Fallback**: Used when WebSocket unavailable +- **Compatibility**: Works in restrictive environments + +### Reconnections + +Connections automatically handle network failures with robust reconnection logic: + +**Automatic Behavior:** +- **Exponential backoff**: Retry delays increase progressively to avoid overwhelming the server +- **Action queuing**: Actions called while disconnected are queued and sent once reconnected +- **Event resubscription**: Event listeners are automatically restored on reconnection +- **State synchronization**: Connection state is preserved and synchronized after reconnection + +## Authentication + +### Connection Parameters + +Pass authentication data when connecting to actors: + +```typescript +// With connection parameters +const chat = client.chatRoom.getOrCreate(["general"]); +const connection = chat.connect({ + authToken: "jwt-token-here", + userId: "user-123", + displayName: "Alice" +}); + +// Parameters available in actor via onAuth hook +// Or for action calls +const result = await chat.sendMessage("Hello world!", { + authToken: "jwt-token-here" +}); +``` + +### onAuth Hook Validation + +Actors can validate authentication using the `onAuth` hook: + +```typescript +import { actor, Forbidden } from "@rivetkit/actor"; + +const protectedActor = actor({ + onAuth: async (opts) => { + const { req, params } = opts; + + // Extract token from params or headers + const token = params.authToken || req.headers.get("Authorization"); + + if (!token) { + throw new Forbidden("Authentication required"); + } + + // Validate and return user data + const user = await validateJWT(token); + return { userId: user.id, role: user.role }; + }, + + actions: { + protectedAction: (c, data: string) => { + // Access auth data via c.conn.auth + const { userId, role } = c.conn.auth; + + if (role !== "admin") { + throw new Forbidden("Admin access required"); + } + + return `Hello admin ${userId}`; + } + } +}); +``` + +Learn more about [authentication patterns](/general/authentication). + +## Type Safety + +RivetKit provides end-to-end type safety between clients and actors: + +### Action Type Safety + +TypeScript validates action signatures and return types: + +```typescript +// TypeScript knows the action signatures +const counter = client.counter.getOrCreate(["my-counter"]); + +const count: number = await counter.increment(5); // ✓ Correct +const invalid = await counter.increment("5"); // ✗ Type error + +// IDE autocomplete shows available actions +counter./* */ +``` + +### Client Type Safety + +Import types from your registry for full type safety: + +```typescript +import type { registry } from "./registry"; + +// Client is fully typed +const client = createClient("http://localhost:8080"); + +// IDE provides autocomplete for all actors +client./* */ +``` + +## Best Practices + +### Actions vs Connections + +**Use Stateless Actions For:** +- Simple request-response operations +- One-off operations +- Server-side integration +- Minimal overhead required + +```typescript +// Good for simple operations +const result = await counter.increment(1); +const status = await server.getStatus(); +``` + +**Use Stateful Connections For:** +- Real-time updates needed +- Multiple related operations +- Event-driven interactions +- Long-lived client sessions + +```typescript +// Good for real-time features +const connection = chatRoom.connect(); +connection.on("messageReceived", updateUI); +await connection.sendMessage("Hello!"); +``` + +### Resource Management + +Always clean up connections when finished: + +```typescript +// Manual cleanup +const connection = actor.connect(); +try { + // Use connection + connection.on("event", handler); + await connection.action(); +} finally { + await connection.dispose(); +} + +// Automatic cleanup with lifecycle +connection.on("disconnected", () => { + console.log("Connection cleaned up"); +}); +``` + +### Error Handling + +Implement proper error handling for both actions and connections: + +```typescript +// Action error handling +try { + const result = await counter.increment(5); +} catch (error) { + if (error.code === "RATE_LIMITED") { + console.log("Rate limited, try again later"); + } else if (error.code === "UNAUTHORIZED") { + redirectToLogin(); + } else { + console.error("Action failed:", error.message); + } +} + +// Connection error handling +connection.on("error", (error) => { + console.error("Connection error:", error); + // Implement reconnection logic if needed +}); +``` + +### Performance Optimization + +Use appropriate patterns for optimal performance: + +```typescript +// Batch multiple operations through a connection +const connection = actor.connect(); +await Promise.all([ + connection.operation1(), + connection.operation2(), + connection.operation3(), +]); + +// Use getOrCreate for actors you expect to exist +const existing = client.counter.getOrCreate(["known-counter"]); + +// Use create only when you need a fresh instance +const fresh = await client.counter.create(["new-counter"]); +``` diff --git a/docs/actors/connections.mdx b/docs/actors/connections.mdx index 2246b49bb..71730a497 100644 --- a/docs/actors/connections.mdx +++ b/docs/actors/connections.mdx @@ -14,7 +14,7 @@ For example: ```typescript actor.ts -import { actor } from "rivetkit"; +import { actor } from "@rivetkit/actor"; const gameRoom = actor({ state: {}, @@ -54,63 +54,67 @@ const gameRoom = await client.gameRoom.get({ There are two ways to define a actor's connection state: -### Method 1: `ConnState` constant - -```typescript -import { actor } from "rivetkit"; - -const chatRoom = actor({ - state: { messages: [] }, - - // Define default connection state as a constant - connState: { - role: "guest", - joinedAt: 0 - }, - - onConnect: (c) => { - // Update join timestamp when a client connects - c.conn.state.joinedAt = Date.now(); - }, - - actions: { - // ... - } -}); -``` - -### Method 2: `createConnState` function - -The data returned from `createConnState` is used as the initial state of the connection. The connection state can be accessed through `conn.state`. - -```typescript -import { actor } from "rivetkit"; - -const chatRoom = actor({ - state: { messages: [] }, - - // Create connection state dynamically - createConnState: (c) => { - // Validate any connection parameters - // ... - - // Return the connection state - return { - userId: generateUserId(), - role: "guest", - joinedAt: Date.now() - }; - }, - - actions: { - sendMessage: (c, message) => { - const username = c.conn.state.userId; - c.state.messages.push({ username, message }); - c.broadcast("newMessage", { username, message }); - } - } -}); -``` + + + Define connection state as a constant value: + + ```typescript + import { actor } from "@rivetkit/actor"; + + const chatRoom = actor({ + state: { messages: [] }, + + // Define default connection state as a constant + connState: { + role: "guest", + joinedAt: 0 + }, + + onConnect: (c) => { + // Update join timestamp when a client connects + c.conn.state.joinedAt = Date.now(); + }, + + actions: { + // ... + } + }); + ``` + + + + Create connection state dynamically with a function. The data returned is used as the initial state of the connection. The connection state can be accessed through `conn.state`. + + ```typescript + import { actor } from "@rivetkit/actor"; + + const chatRoom = actor({ + state: { messages: [] }, + + // Create connection state dynamically + createConnState: (c) => { + // Validate any connection parameters + // ... + + // Return the connection state + return { + userId: generateUserId(), + role: "guest", + joinedAt: Date.now() + }; + }, + + actions: { + sendMessage: (c, message) => { + const username = c.conn.state.userId; + c.state.messages.push({ username, message }); + c.broadcast("newMessage", { username, message }); + } + } + }); + ``` + + ## Lifecycle Hooks @@ -120,7 +124,7 @@ The connection lifecycle has several hooks: - `onConnect`: Called when a client successfully connects - `onDisconnect`: Called when a client disconnects -See the documentation on [Actor Lifecycle](/concepts/lifecycle) for more details. +See the documentation on [Actor Lifecycle](/actors/lifecycle) for more details. ## Connection List @@ -131,7 +135,7 @@ This is frequently used with `conn.send(name, event)` to send messages directly For example: ```typescript -import { actor } from "rivetkit"; +import { actor } from "@rivetkit/actor"; const chatRoom = actor({ state: { users: {} }, @@ -158,7 +162,7 @@ const chatRoom = actor({ Connections can be disconnected from within an action: ```typescript -import { actor } from "rivetkit"; +import { actor } from "@rivetkit/actor"; const secureRoom = actor({ state: {}, @@ -189,4 +193,4 @@ This ensures the underlying network connections close cleanly before continuing. ## Offline & Auto-Reconnection -See [Interacting with Actors](/concepts/interacting-with-actors#offline-and-auto-reconnection) for details on reconnection behavior. +See [client documentation](/actors/communicating-with-actors) for details on reconnection behavior. diff --git a/docs/actors/events.mdx b/docs/actors/events.mdx index ba35a543b..cc89a4a69 100644 --- a/docs/actors/events.mdx +++ b/docs/actors/events.mdx @@ -1,166 +1,393 @@ --- title: Events +description: Real-time communication between actors and clients icon: tower-broadcast --- -Events are used for clients to receive realtime data from actors. +Events enable real-time communication from actors to clients. While clients use actions to send data to actors, events allow actors to push updates to connected clients instantly. -Events are used for actors to publish updates to clients. Clients call actions to communicate with the actor. + +Events work through persistent connections (WebSocket or SSE). Clients establish connections using `.connect()` and then listen for events with `.on()`. + -## Publishing from actors +## Publishing Events from Actors -Actors can publish events to clients using `c.broadcast` and `conn.send`. +### Broadcasting to All Clients -### Broadcasting events +Use `c.broadcast(eventName, data)` to send events to all connected clients: -Actors can publish events to all connected clients with `c.broadcast(name, data)`. For example: - - - -```typescript chat_room.ts -import { actor } from "rivetkit"; +```typescript +import { actor } from "@rivetkit/actor"; const chatRoom = actor({ - state: {}, + state: { + messages: [] as Array<{id: string, userId: string, text: string, timestamp: number}> + }, + actions: { - sendMessage: (c, message) => { - c.broadcast('newMessage', { message }); + sendMessage: (c, userId: string, text: string) => { + const message = { + id: crypto.randomUUID(), + userId, + text, + timestamp: Date.now() + }; + + c.state.messages.push(message); + + // Broadcast to all connected clients + c.broadcast('messageReceived', message); + + return message; + }, + + deleteMessage: (c, messageId: string) => { + const messageIndex = c.state.messages.findIndex(m => m.id === messageId); + if (messageIndex !== -1) { + c.state.messages.splice(messageIndex, 1); + + // Notify all clients about deletion + c.broadcast('messageDeleted', { messageId }); + } } } }); ``` -```typescript client.ts -import { createClient } from "rivetkit/client"; -import type { App } from "./src/index"; +### Sending to Specific Connections -const client = createClient("http://localhost:8080"); -const chatRoom = await client.chatRoom.get(); -await chatRoom.sendMessage('Hello, world!'); -``` +Send events to individual connections using `conn.send(eventName, data)`: - +```typescript +import { actor } from "@rivetkit/actor"; -### Sending events to specific connections +const gameRoom = actor({ + state: { + players: {} as Record + }, + + createConnState: (c, { params }) => ({ + playerId: params.playerId, + role: params.role || "player" + }), + + actions: { + updatePlayerPosition: (c, position: {x: number, y: number}) => { + const playerId = c.conn.state.playerId; + + if (c.state.players[playerId]) { + c.state.players[playerId].position = position; + + // Send position update to all OTHER players + for (const conn of c.conns) { + if (conn.state.playerId !== playerId) { + conn.send('playerMoved', { playerId, position }); + } + } + } + }, + + sendPrivateMessage: (c, targetPlayerId: string, message: string) => { + // Find the target player's connection + const targetConn = c.conns.find(conn => + conn.state.playerId === targetPlayerId + ); + + if (targetConn) { + targetConn.send('privateMessage', { + from: c.conn.state.playerId, + message, + timestamp: Date.now() + }); + } else { + throw new Error("Player not found or not connected"); + } + } + } +}); +``` -Actors can send messages to specific client connections. All connections are available through the context object. For example: +### Event Filtering by Connection State - +Filter events based on connection properties: -```typescript chat_room.ts -import { actor } from "rivetkit"; +```typescript +import { actor } from "@rivetkit/actor"; -const chatRoom = actor({ - state: {}, +const newsRoom = actor({ + state: { + articles: [] as Array<{id: string, category: string, content: string, level: 'public' | 'premium'}> + }, + + createConnState: (c, { params }) => ({ + userId: params.userId, + subscription: params.subscription || 'free' // 'free', 'premium' + }), + actions: { - sendPrivateMessage: (c, connId, message) => { - const conn = c.conns.find(conn => conn.id === connId); - if (conn) { - conn.send('newMessage', { message }); + publishArticle: (c, article: {category: string, content: string, level: 'public' | 'premium'}) => { + const newArticle = { + id: crypto.randomUUID(), + ...article, + timestamp: Date.now() + }; + + c.state.articles.push(newArticle); + + // Send to appropriate subscribers only + for (const conn of c.conns) { + const canAccess = article.level === 'public' || + conn.state.subscription === 'premium'; + + if (canAccess) { + conn.send('newArticle', newArticle); + } } + + return newArticle; } } }); ``` -```typescript client.ts -import { createClient } from "rivetkit/client"; -import type { App } from "./src/index"; +## Subscribing to Events from Clients -const client = createClient("http://localhost:8080"); -const chatRoom = await client.chatRoom.get(); -await chatRoom.sendPrivateMessage(123, 'Hello, world!'); -``` +Clients must establish a connection to receive events from actors. Use `.connect()` to create a persistent connection, then listen for events. + +### Basic Event Subscription + +Use `connection.on(eventName, callback)` to listen for events: - +```typescript +import { createClient } from "@rivetkit/actor/client"; +import type { registry } from "./registry"; -## Subscribing from clients +const client = createClient("http://localhost:8080"); -Clients can subscribe to events from actors using `on` and `once`. +// Get actor handle and establish connection +const chatRoom = client.chatRoom.getOrCreate(["general"]); +const connection = chatRoom.connect(); + +// Listen for events +connection.on('messageReceived', (message) => { + console.log(`${message.userId}: ${message.text}`); + displayMessage(message); +}); + +connection.on('messageDeleted', ({ messageId }) => { + console.log(`Message ${messageId} was deleted`); + removeMessageFromUI(messageId); +}); -### `on(eventName, callback)` +// Call actions through the connection +await connection.sendMessage("user-123", "Hello everyone!"); +``` -{/* [Documentation](https://jsr.io/@rivet-gg/actor-client/doc/~/ActorHandleRaw.prototype.on.html) */} +### One-time Event Listeners -Clients can subscribe to events that will happen repeatedly using `actor.on(name, callback)`. For example: +Use `connection.once(eventName, callback)` for events that should only trigger once: - +```typescript +const gameRoom = client.gameRoom.getOrCreate(["room-456"]); +const connection = gameRoom.connect({ + playerId: "player-789", + role: "player" +}); -```typescript client.ts -import { createClient } from "rivetkit/client"; -import type { App } from "./src/index"; +// Listen for game start (only once) +connection.once('gameStarted', () => { + console.log('Game has started!'); + showGameInterface(); +}); -const client = createClient("http://localhost:8080"); -const chatRoom = await client.chatRoom.get(); +// Listen for game events continuously +connection.on('playerMoved', ({ playerId, position }) => { + updatePlayerPosition(playerId, position); +}); -chatRoom.on('newMessage', ({ message }) => { - console.log('Message', message); +connection.on('privateMessage', ({ from, message }) => { + showPrivateMessage(from, message); }); ``` -```typescript chat_room.ts -import { actor } from "rivetkit"; +### Removing Event Listeners -const chatRoom = actor({ - state: {}, - actions: { - sendMessage: (c, message) => { - c.broadcast('newMessage', { message }); - } - } +Use `connection.off()` to remove event listeners: + +```typescript +const messageHandler = (message) => { + console.log("Received:", message); +}; + +// Add listener +connection.on('messageReceived', messageHandler); + +// Remove specific listener +connection.off('messageReceived', messageHandler); + +// Remove all listeners for an event +connection.off('messageReceived'); + +// Remove all listeners +connection.off(); +``` + +### React Integration + +RivetKit's React hooks provide a convenient way to handle events in React components: + +```tsx +import { useActor } from "./rivetkit"; +import { useState } from "react"; + +function ChatRoom() { + const [messages, setMessages] = useState([]); + + const chatRoom = useActor({ + name: "chatRoom", + key: ["general"] + }); + + // Listen for new messages + chatRoom.useEvent("messageReceived", (message) => { + setMessages(prev => [...prev, message]); + }); + + // Listen for deleted messages + chatRoom.useEvent("messageDeleted", ({ messageId }) => { + setMessages(prev => prev.filter(m => m.id !== messageId)); + }); + + const sendMessage = async (text: string) => { + await chatRoom.connection?.sendMessage("user-123", text); + }; + + return ( +
+ {messages.map(message => ( +
+ {message.userId}: {message.text} +
+ ))} + +
+ ); +} +``` + +## Connection Lifecycle Events + +Connections emit lifecycle events you can listen to: + +```typescript +const connection = actor.connect(); + +connection.on('connected', () => { + console.log('Connected to actor'); + enableUI(); +}); + +connection.on('disconnected', () => { + console.log('Lost connection to actor'); + showReconnectingIndicator(); +}); + +connection.on('reconnected', () => { + console.log('Reconnected to actor'); + hideReconnectingIndicator(); +}); + +connection.on('error', (error) => { + console.error('Connection error:', error); + showErrorMessage(error.message); }); ``` -
+## Advanced Event Patterns -### `once(eventName, callback)` +### Event Buffering -{/* [Documentation](https://jsr.io/@rivet-gg/actor-client/doc/~/ActorHandleRaw.prototype.once.html) */} +Events are automatically buffered during disconnections and replayed on reconnection: -Clients can listen for an event only one time with `actor.once(name, callback)`. For example: +```typescript +const connection = actor.connect(); - +// Events sent while disconnected are queued +connection.on('importantUpdate', (data) => { + // This will still be called for events sent during disconnection + // once the connection is reestablished + handleImportantUpdate(data); +}); +``` -```typescript client.ts -import { createClient } from "rivetkit/client"; -import type { App } from "./src/index"; +### Connection Parameters -const client = createClient("http://localhost:8080"); -const chatRoom = await client.chatRoom.get(); +Pass parameters when connecting to provide context to the actor: -chatRoom.once('joinRequestApproved', () => { - // This will only be called once - console.log('Join request accepted'); +```typescript +const gameRoom = client.gameRoom.getOrCreate(["competitive-room"]); +const connection = gameRoom.connect({ + playerId: getCurrentPlayerId(), + skillLevel: getUserSkillLevel(), + authToken: getAuthToken() }); -await chatRoom.requestJoin(); +// The actor can use these parameters in its onBeforeConnect hook +// or access them via c.conn.params in actions ``` -```typescript chat_room.ts -import { actor } from "rivetkit"; +### Conditional Event Handling -const chatRoom = actor({ - state: { - pendingJoinRequests: [] - }, - actions: { - requestJoin: (c) => { - // ...add to pending requests... - }, - approveJoinRequest: (c, connId) => { - const conn = c.conns.find(conn => conn.id === connId); - if (conn) { - conn.send('joinRequestApproved'); - } - } +Handle events conditionally based on connection state: + +```typescript +connection.on('playerMoved', ({ playerId, position }) => { + // Only update if it's not our own player + if (playerId !== getCurrentPlayerId()) { + updatePlayerPosition(playerId, position); + } +}); + +connection.on('newArticle', (article) => { + // Handle based on article level and user subscription + if (article.level === 'premium' && !hasSubscription()) { + showUpgradePrompt(); + } else { + displayArticle(article); } }); ``` - +## Error Handling + +Handle event-related errors gracefully: + +```typescript +try { + const connection = actor.connect(); + + connection.on('messageReceived', (message) => { + try { + validateMessage(message); + displayMessage(message); + } catch (error) { + console.error('Invalid message format:', error); + // Handle malformed event data + } + }); + +} catch (error) { + console.error('Failed to establish connection:', error); + showConnectionError(); +} +``` -## Connections +## Best Practices -Connections are used to communicate with clients from the actor. +1. **Always use connections for events**: Events only work through `.connect()`, not direct action calls +2. **Handle connection lifecycle**: Listen for connection, disconnection, and error events +3. **Clean up listeners**: Remove event listeners when components unmount +4. **Validate event data**: Don't assume event payloads are always correctly formatted +5. **Use React hooks**: For React apps, use `useActor` and `actor.useEvent` for automatic cleanup +6. **Buffer critical events**: Design actors to resend important events on reconnection if needed -Read more about connections [here](/concepts/connections). diff --git a/docs/concepts/external-sql.mdx b/docs/actors/external-sql.mdx similarity index 98% rename from docs/concepts/external-sql.mdx rename to docs/actors/external-sql.mdx index d30e88848..fa512e08d 100644 --- a/docs/concepts/external-sql.mdx +++ b/docs/actors/external-sql.mdx @@ -36,7 +36,7 @@ There are several options for places to host your SQL database: Here's a basic example of how you might set up a connection to a PostgreSQL database using the `pg` library: ```typescript actor.ts -import { actor } from "rivetkit"; +import { actor } from "@rivetkit/actor"; import { Pool } from "pg"; // Create a database connection pool @@ -106,7 +106,7 @@ export default databaseActor; Here's an example using Drizzle ORM for more type-safe database operations: ```typescript actor.ts -import { actor } from "rivetkit"; +import { actor } from "@rivetkit/actor"; import { drizzle } from "drizzle-orm/node-postgres"; import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core"; import { Pool } from "pg"; diff --git a/docs/actors/types.mdx b/docs/actors/helper-types.mdx similarity index 92% rename from docs/actors/types.mdx rename to docs/actors/helper-types.mdx index 95793a691..16764f4b3 100644 --- a/docs/actors/types.mdx +++ b/docs/actors/helper-types.mdx @@ -1,5 +1,6 @@ --- title: Helper Types +icon: code --- RivetKit provides several TypeScript helper types to make it easier to work with actors in a type-safe way. @@ -13,7 +14,7 @@ When working with actors, you often need to access the context object. RivetKit Extracts the full actor context type from a actor definition. This is the type of the context object (`c`) available in lifecycle hooks such as `onCreate`, `onStart`, etc. ```typescript -import { actor, ActorContextOf } from "rivetkit"; +import { actor, ActorContextOf } from "@rivetkit/actor"; const chatRoom = actor({ state: { messages: [] }, @@ -40,7 +41,7 @@ function processChatRoomContext(context: ChatRoomContext) { Extracts the action context type from a actor definition. This is the type of the context object (`c`) available in action handlers. ```typescript -import { actor, ActionContextOf } from "rivetkit"; +import { actor, ActionContextOf } from "@rivetkit/actor"; const counter = actor({ state: { count: 0 }, diff --git a/docs/actors/lifecycle.mdx b/docs/actors/lifecycle.mdx index 6ae357535..41fe75944 100644 --- a/docs/actors/lifecycle.mdx +++ b/docs/actors/lifecycle.mdx @@ -1,8 +1,97 @@ --- title: Lifecycle +description: Understand actor lifecycle hooks and initialization patterns icon: rotate --- +Actors follow a well-defined lifecycle with hooks at each stage. Understanding these hooks is essential for proper initialization, state management, and cleanup. + +## Input Parameters + +Actors can receive input parameters when created, allowing for flexible initialization: + +### Defining Input Schema + +Use Zod to define a schema for input validation: + +```typescript +import { actor } from "@rivetkit/actor"; +import { z } from "zod"; + +const gameActor = actor({ + createState: (c, opts) => ({ + mode: opts.input?.gameMode ?? "classic", + maxPlayers: opts.input?.maxPlayers ?? 4, + difficulty: opts.input?.difficulty ?? "medium", + players: [], + status: "waiting", + }), + + actions: { + // Actions can access input via context + getGameConfig: (c) => ({ + mode: c.state.mode, + maxPlayers: c.state.maxPlayers, + difficulty: c.state.difficulty, + }), + }, +}); +``` + +### Passing Input to Actors + +Input is provided when creating actor instances: + +```typescript +// Client side - create with input +const game = await client.game.create(["game-123"], { + gameMode: "tournament", + maxPlayers: 8, + difficulty: "hard", +}); + +// getOrCreate can also accept input (used only if creating) +const gameHandle = client.game.getOrCreate(["game-456"], { + gameMode: "casual", + maxPlayers: 4, +}); +``` + +### Input in Lifecycle Hooks + +Input is available in lifecycle hooks via the `opts` parameter: + +```typescript +const chatRoom = actor({ + createState: (c, opts) => ({ + name: opts.input?.roomName ?? "Unnamed Room", + isPrivate: opts.input?.isPrivate ?? false, + maxUsers: opts.input?.maxUsers ?? 50, + users: {}, + messages: [], + }), + + onCreate: (c, opts) => { + console.log(`Creating room: ${opts.input?.roomName}`); + + // Setup external services based on input + if (opts.input?.isPrivate) { + setupPrivateRoomLogging(opts.input.roomName); + } + }, + + actions: { + // Input remains accessible in actions + getRoomInfo: (c) => ({ + name: c.state.name, + isPrivate: c.state.isPrivate, + maxUsers: c.state.maxUsers, + currentUsers: Object.keys(c.state.users).length, + }), + }, +}); +``` + ## Lifecycle Hooks Actor lifecycle hooks are defined as functions in the actor configuration. @@ -11,16 +100,16 @@ Actor lifecycle hooks are defined as functions in the actor configuration. {/* [Documentation](https://jsr.io/@rivet-gg/actor/doc/~/Actor.prototype._onInitialize) */} -The `createState` function or `state` constant defines the initial state of the actor (see [state documentation](/concepts/state)). The `createState` function is called only once when the actor is first created. +The `createState` function or `state` constant defines the initial state of the actor (see [state documentation](/actors/state)). The `createState` function is called only once when the actor is first created. ### `createVars` and `vars` -The `createVars` function or `vars` constant defines ephemeral variables for the actor (see [state documentation](/concepts/state)). These variables are not persisted and are useful for storing runtime-only objects or temporary data. +The `createVars` function or `vars` constant defines ephemeral variables for the actor (see [state documentation](/actors/state)). These variables are not persisted and are useful for storing runtime-only objects or temporary data. The `createVars` function can also receive driver-specific context as its second parameter, allowing access to driver capabilities like Rivet KV or Cloudflare Durable Object storage. ```typescript -import { actor } from "rivetkit"; +import { actor } from "@rivetkit/actor"; // Using vars constant const counter1 = actor({ @@ -36,7 +125,7 @@ const counter2 = actor({ // Initialize with non-serializable objects return { lastAccessTime: Date.now(), - emitter: createNanoEvents() + emitter: new EventTarget() }; }, actions: { /* ... */ } @@ -46,13 +135,13 @@ const counter2 = actor({ const exampleActor = actor({ state: { count: 0 }, // Access driver context in createVars - createVars: (c, rivet) => ({ - ctx: rivet.ctx, + createVars: (c, driverCtx) => ({ + driverCtx, }), actions: { doSomething: (c) => { // Use driver-specific context - console.log(`Region: ${c.vars.rivet.metadata.region.name}`); + console.log("Driver context:", c.vars.driverCtx); } } }); @@ -63,7 +152,7 @@ const exampleActor = actor({ The `onCreate` hook is called at the same time as `createState`, but unlike `createState`, it doesn't return any value. Use this hook for initialization logic that doesn't affect the initial state. ```typescript -import { actor } from "rivetkit"; +import { actor } from "@rivetkit/actor"; // Using state constant const counter1 = actor({ @@ -85,8 +174,10 @@ const counter3 = actor({ state: { count: 0 }, // Run initialization logic (logging, external service setup, etc.) - onCreate: (c) => { + onCreate: (c, opts) => { console.log("Counter actor initialized"); + // Access input parameters if provided + console.log("Input:", opts.input); // Can perform async operations or setup // No need to return anything }, @@ -106,10 +197,11 @@ This is called after the actor has been initialized but before any connections a Use this hook to set up any resources or start any background tasks, such as `setInterval`. ```typescript -import { actor } from "rivetkit"; +import { actor } from "@rivetkit/actor"; const counter = actor({ state: { count: 0 }, + vars: { intervalId: null as NodeJS.Timeout | null }, onStart: (c) => { console.log('Actor started with count:', c.state.count); @@ -117,14 +209,22 @@ const counter = actor({ // Set up interval for automatic counting const intervalId = setInterval(() => { c.state.count++; + c.broadcast("countChanged", c.state.count); console.log('Auto-increment:', c.state.count); }, 10000); - // Store interval ID to clean up later if needed - c.custom.intervalId = intervalId; + // Store interval ID in vars to clean up later if needed + c.vars.intervalId = intervalId; }, - actions: { /* ... */ } + actions: { + stop: (c) => { + if (c.vars.intervalId) { + clearInterval(c.vars.intervalId); + c.vars.intervalId = null; + } + } + } }); ``` @@ -135,7 +235,7 @@ const counter = actor({ Called whenever the actor's state changes. This is often used to broadcast state updates. ```typescript -import { actor } from "rivetkit"; +import { actor } from "@rivetkit/actor"; const counter = actor({ state: { count: 0 }, @@ -173,7 +273,7 @@ The `onBeforeConnect` hook is called whenever a new client connects to the actor The `onBeforeConnect` hook does NOT return connection state - it's used solely for validation. ```typescript -import { actor } from "rivetkit"; +import { actor } from "@rivetkit/actor"; const chatRoom = actor({ state: { messages: [] }, @@ -209,7 +309,7 @@ const chatRoom = actor({ }); ``` -Connections cannot interact with the actor until this method completes successfully. Throwing an error will abort the connection. This can be used for authentication - see [Authentication](/concepts/authentication) for details. +Connections cannot interact with the actor until this method completes successfully. Throwing an error will abort the connection. This can be used for authentication - see [Authentication](/general/authentication) for details. ### `onConnect` @@ -218,7 +318,7 @@ Connections cannot interact with the actor until this method completes successfu Executed after the client has successfully connected. ```typescript -import { actor } from "rivetkit"; +import { actor } from "@rivetkit/actor"; const chatRoom = actor({ state: { users: {}, messages: [] }, @@ -250,7 +350,7 @@ Messages will not be processed for this actor until this hook succeeds. Errors t Called when a client disconnects from the actor. Use this to clean up any connection-specific resources. ```typescript -import { actor } from "rivetkit"; +import { actor } from "@rivetkit/actor"; const chatRoom = actor({ state: { users: {}, messages: [] }, @@ -278,7 +378,7 @@ const chatRoom = actor({ Actors can be shut down gracefully with `c.shutdown()`. Clients will be gracefully disconnected. ```typescript -import { actor } from "rivetkit"; +import { actor } from "@rivetkit/actor"; const temporaryRoom = actor({ state: { @@ -328,7 +428,7 @@ This action is permanent and cannot be reverted. When extracting logic from lifecycle hooks or actions into external functions, you'll often need to define the type of the context parameter. RivetKit provides helper types that make it easy to extract and pass these context types to external functions. ```typescript -import { actor, ActorContextOf } from "rivetkit"; +import { actor, ActorContextOf } from "@rivetkit/actor"; const myActor = actor({ state: { count: 0 }, @@ -343,23 +443,26 @@ function logActorStarted(c: ActorContextOf) { } ``` -See [Helper Types](/concepts/types) for more details on using `ActorContextOf`. +See [Helper Types](/actors/helper-types) for more details on using `ActorContextOf`. ## Full Example ```typescript -import { actor } from "rivetkit"; +import { actor } from "@rivetkit/actor"; +import { z } from "zod"; const counter = actor({ - // Initialize state - createState: () => ({ - count: 0 + // Initialize state with input + createState: (c, opts) => ({ + count: opts.input?.initialCount ?? 0, + stepSize: opts.input?.stepSize ?? 1, + name: opts.input?.name ?? "Unnamed Counter", }), // Initialize actor (run setup that doesn't affect initial state) - onCreate: (c) => { - console.log('Counter actor initialized'); - // Set up external resources, etc. + onCreate: (c, opts) => { + console.log(`Counter "${opts.input?.name}" initialized`); + // Set up external resources, logging, etc. }, // Define default connection state @@ -381,11 +484,14 @@ const counter = actor({ // Lifecycle hooks onStart: (c) => { - console.log('Counter started with count:', c.state.count); + console.log(`Counter "${c.state.name}" started with count:`, c.state.count); }, onStateChange: (c, newState) => { - c.broadcast('countUpdated', { count: newState.count }); + c.broadcast('countUpdated', { + count: newState.count, + name: newState.name + }); }, onBeforeConnect: (c, { params }) => { @@ -406,29 +512,36 @@ const counter = actor({ }, onConnect: (c) => { - console.log(`User ${c.conn.state.userId} connected`); + console.log(`User ${c.conn.state.userId} connected to "${c.state.name}"`); }, onDisconnect: (c) => { - console.log(`User ${c.conn.state.userId} disconnected`); + console.log(`User ${c.conn.state.userId} disconnected from "${c.state.name}"`); }, // Define actions actions: { - increment: (c) => { - c.state.count++; + increment: (c, amount?: number) => { + const step = amount ?? c.state.stepSize; + c.state.count += step; return c.state.count; }, reset: (c) => { // Check if user has admin role - if (c.conns.state.role !== "admin") { + if (c.conn.state.role !== "admin") { throw new Error("Unauthorized: requires admin role"); } c.state.count = 0; return c.state.count; - } + }, + + getInfo: (c) => ({ + name: c.state.name, + count: c.state.count, + stepSize: c.state.stepSize, + }), } }); diff --git a/docs/actors/metadata.mdx b/docs/actors/metadata.mdx index 71342f51e..a687e1258 100644 --- a/docs/actors/metadata.mdx +++ b/docs/actors/metadata.mdx @@ -20,7 +20,7 @@ For example: ```typescript chat_room.ts -import { actor } from "rivetkit"; +import { actor } from "@rivetkit/actor"; const chatRoom = actor({ state: { diff --git a/docs/actors/overview.mdx b/docs/actors/overview.mdx index a3a09eeda..82b37c3cb 100644 --- a/docs/actors/overview.mdx +++ b/docs/actors/overview.mdx @@ -1,285 +1,213 @@ --- -title: Rivet Actors +title: Actors Overview icon: square-info sidebarTitle: "Overview" -description: A library for building stateful, scalable, realtime backend applications. +description: Stateful serverless functions with persistence, real-time communication, and automatic scaling --- -import CreateActorCli from "/snippets/create-actor-cli.mdx"; +Actors are lightweight, stateful serverless functions that maintain persistent state and provide real-time communication. They're the core building blocks of RivetKit applications. -Actors combine compute and storage into unified entities for simplified architecture. Actors seamlessly integrate with your existing infrastructure or can serve as a complete standalone solution. +## Getting Started -## Quickstart - - - - + + + Set up actors with Node.js, Bun, and web frameworks + + + Build real-time React applications with actors + -## Concepts - -The core concepts that power Rivet Actor applications: - -- **State Is Automatically Persisted**: State automatically persists between restarts, upgrades, & crashes -- **State Is Stored In-Memory**: State is stored in memory for high-performance reads/writes while also automatically persisted -- **Isolated State Ownership**: Actors only manage their own state, which can only be modified by the actor itself -- **Communicates via Actions**: How clients and other actors interact with a actor -- **Actions Are Low-Latency**: Actions provide WebSocket-like performance for time-sensitive operations -- **Broadcast Updates With Events**: Actors can publish real-time updates to connected clients - -## Quickstart - -Run this to get started: - - - -## Code Example - -Here's a complete chat room actor that maintains state and handles messages. We'll explore each component in depth throughout this document: - -```typescript chat_room.ts -import { actor } from "rivetkit"; +## Key Features -// Define a chat room actor -const chatRoom = actor({ - // Initialize state when the actor is first created - createState: () => ({ - messages: [] - }), - - // Define actions clients can call - actions: { - // Action to send a message - sendMessage: (c, sender, text) => { - // Update state - c.state.messages.push({ sender, text }); - - // Broadcast to all connected clients - c.broadcast("newMessage", { sender, text }); - }, - - // Action to get chat history - getHistory: (c) => { - return c.state.messages; - } - } -}); - -export default chatRoom; -``` + + + Each unit of compute is like a tiny server that remembers things between requests – no need to reload data or worry about timeouts. Like AWS Lambda, but with memory and no timeouts. + + + State is stored on the same machine as your compute, so reads and writes are ultra-fast. No database round trips, no latency spikes. + + + Update state and broadcast changes in realtime. No external pub/sub systems, no polling – just built-in low-latency events. + + + Your state lives close to your users on the edge – not in a faraway data center – so every interaction feels instant. + + -## Using the App -To start using your actor, create an app and serve it: +## Use Cases -```typescript app.ts -import { setup, serve } from "rivetkit"; -import chatRoom from "./chat_room"; +Actors are perfect for applications that need persistent state and real-time updates: -// Create the application -const registry = setup({ - use: { chatRoom } -}); - -// Start serving on default port -serve(registry); -``` +### Real-time Communication +- **Chat rooms**: Real-time messaging with message history and user presence +- **Collaborative documents**: Multiple users editing documents simultaneously +- **Live events**: Broadcasting updates to many participants -## Key Actor Components +### AI & Automation +- **AI agents**: Stateful AI assistants with conversation history +- **Workflow automation**: Long-running business processes with state persistence +- **Stream processing**: Real-time data processing with persistent state -### State +### Data & Synchronization +- **Local-first sync**: Offline-first applications with server synchronization +- **Per-user databases**: Isolated data stores for each user or tenant +- **Per-tenant SaaS**: Multi-tenant applications with isolated state +- **CRDT collaboration**: Conflict-free replicated data types for real-time editing -Actors maintain state that's stored in memory and automatically persisted. State is defined either as a constant or via a `createState` function: +### Gaming & Interactive Applications +- **Multiplayer games**: Game state management with real-time updates +- **Rate limiting**: Distributed rate limiting with persistent counters -```typescript -import { actor } from "rivetkit"; +## State Management -// Method 1: State constant -const counter1 = actor({ - state: { count: 0 }, - actions: { - // ... - } -}); - -// Method 2: CreateState function -const counter2 = actor({ - createState: () => ({ count: 0 }), - actions: { - // ... - } -}); -``` - -Update state by modifying `c.state` in your actions: +Actors maintain persistent state that survives restarts, crashes, and deployments. State can be defined as a constant or created dynamically: ```typescript -import { actor } from "rivetkit"; +import { actor } from "@rivetkit/actor"; const counter = actor({ state: { count: 0 }, + actions: { - // Example of state update in an action increment: (c) => { - c.state.count += 1; + c.state.count++; return c.state.count; - } + }, + + getCount: (c) => c.state.count, } }); ``` -These changes are durable and are automatically persisted across updates, restarts, and crashes. - -Learn more about [state management](/actor/state). +Learn more about [state management](/actors/state). -### Actions +## Actions -Actions are functions defined in your actor configuration that clients & other actors can call: +Actions are the primary way to interact with actors. They're type-safe functions that can modify state and communicate with clients: ```typescript -import { actor } from "rivetkit"; +import { actor } from "@rivetkit/actor"; -const mathUtils = actor({ - state: {}, +const chatRoom = actor({ + state: { messages: [] as Array<{text: string, userId: string}> }, + actions: { - multiplyByTwo: (c, x) => { - return x * 2; - } + sendMessage: (c, userId: string, text: string) => { + const message = { text, userId }; + c.state.messages.push(message); + c.broadcast("newMessage", message); + return message; + }, + + getMessages: (c) => c.state.messages } }); ``` -Each action receives a context object (commonly named `c`) as its first parameter, which provides access to state, connections, and other utilities. +Actions can be called from your backend, your clients, or other actors: + +```typescript +const room = client.chatRoom.getOrCreate(["general"]); +const message = await room.sendMessage("user-123", "Hello everyone!"); +``` + +Learn more about [actions](/actors/actions) and [communicating with actors](/actors/communicating-with-actors). -Learn more about [actions](/actor/actions). +## Real-time Communication -### Events +Actors support real-time bidirectional communication through WebSocket and SSE connections. Clients can establish persistent connections to receive live updates. -Actors can broadcast events to connected clients: +For example, to send events to all connected clients: ```typescript -import { actor } from "rivetkit"; +import { actor } from "@rivetkit/actor"; -const inventory = actor({ - createState: () => ({ - items: [] - }), +const liveAuction = actor({ + state: { currentBid: 0 }, actions: { - addItem: (c, item) => { - // Add to state - c.state.items.push(item); - - // Notify all clients about the new item - c.broadcast("itemAdded", { item }); + placeBid: (c, amount: number) => { + c.state.currentBid = amount; + c.broadcast("newBid", { amount }); + return amount; } } }); ``` -You can also send events to specific clients: +Clients connect and listen for real-time updates: ```typescript -import { actor } from "rivetkit"; +const auction = client.liveAuction.getOrCreate(["auction-123"]); +const connection = auction.connect(); -const messageService = actor({ - state: {}, - actions: { - sendPrivateMessage: (c, userId, text) => { - // Send to a specific connection - const conn = c.conns.find(conn => conn.params.userId === userId); - if (conn) { - conn.send("privateMessage", { text }); - } - } - } +connection.on("newBid", (data) => { + console.log(`New bid: $${data.amount}`); }); -``` - -Learn more about [events](/actor/events). - -## Actor Tags - -Tags are key-value pairs attached to actors that serve two purposes: - -1. **Actor Discovery**: Find specific actors using `client.get(tags)` -2. **Organization**: Group related actors for management purposes - -For example, you can query chat rooms by tag like: -```typescript client.ts -await client.chatRoom.get({ channel: "random" }); +await auction.placeBid(150); ``` -### Common Tag Patterns +Learn more about [events](/actors/events) and [client communication](/actors/communicating-with-actors). -```typescript -import { createClient } from "rivetkit/client"; -import type { App } from "./src/index"; - -const client = createClient("http://localhost:8080"); +## Scheduling & Lifecycle -// Game room with ID parameter -const gameRoom = await client.gameRoom.get({ roomId: "ABC123" }); +Actors support scheduled tasks and lifecycle management: -// User profile with ID -const userProfile = await client.userProfile.get({ profileId: "1234" }); +```typescript +import { actor } from "@rivetkit/actor"; -// Document with multiple parameters -const document = await client.document.get({ - workspaceId: "team-alpha", - documentId: "budget-2024" +const reminder = actor({ + state: { message: "" }, + + actions: { + setReminder: (c, message: string, delayMs: number) => { + c.state.message = message; + c.schedule.after(delayMs, "sendReminder"); + }, + + sendReminder: (c) => { + c.broadcast("reminder", { message: c.state.message }); + } + } }); ``` -## Actor Lifecycle +Learn more about [actor lifecycle](/actors/lifecycle). -Actors are created automatically when needed and persist until explicitly shutdown. +## Type Safety -To shut down a actor, use `c.shutdown()` from within an action: +RivetKit provides end-to-end TypeScript safety between clients and actors: -```typescript -import { actor } from "rivetkit"; + -const chatRoom = actor({ - createState: () => ({ - messages: [] - }), +```typescript Actor +const userManager = actor({ + state: { users: {} as Record }, + actions: { - closeRoom: (c) => { - // Do any cleanup needed - c.broadcast("roomClosed"); - - // Shutdown the actor - c.shutdown(); - } + createUser: (c, name: string) => { + const userId = crypto.randomUUID(); + c.state.users[userId] = { name }; + return { userId, name }; + }, + + getUser: (c, userId: string) => c.state.users[userId] } }); ``` -Learn more about the [actor lifecycle](/actor/lifecycle). +```typescript Client +const manager = client.userManager.getOrCreate(["default"]); -## Documentation +const user = await manager.createUser("Alice"); +// Type: {userId: string, name: string} -Learn more about Rivet Actors: +const foundUser = await manager.getUser(user.userId); +// Type: {name: string} | undefined +``` + + - - - Get started with Rivet Actors in minutes - - - Understand how actor state is managed, persisted, and accessed. - - - Define and implement actor actions (RPCs) for client interaction. - - - Real-time communication with events and broadcasts. - - - Managing the creation, execution, and termination of actors. - - - Schedule tasks and alarms with actors for time-based operations. - - diff --git a/docs/actors/quickstart-backend.mdx b/docs/actors/quickstart-backend.mdx new file mode 100644 index 000000000..273ebb887 --- /dev/null +++ b/docs/actors/quickstart-backend.mdx @@ -0,0 +1,434 @@ +--- +title: Node.js & Bun Quickstart +sidebarTitle: Node.js & Bun +description: Get started with Rivet Actors in Node.js and Bun +icon: node-js +--- + + + + +```sh +npm install @rivetkit/actor +``` + + + + + +Create a simple counter actor: + +```ts registry.ts +import { actor, setup } from "@rivetkit/actor"; + +export const counter = actor({ + state: { count: 0 }, + actions: { + increment: (c, amount: number = 1) => { + c.state.count += amount; + c.broadcast("countChanged", c.state.count); + return c.state.count; + }, + getCount: (c) => c.state.count, + }, +}); + +export const registry = setup({ + use: { counter }, +}); +``` + + + + + +Choose your preferred web framework: + + + +```ts Hono +import { registry } from "./registry"; +import { Hono } from "hono"; + +// Start RivetKit with memory driver (for development) +const { client, serve } = registry.createServer(); + +// Setup Hono app +const app = new Hono(); + +// Example API endpoint +app.post("/increment/:name", async (c) => { + const name = c.req.param("name"); + + // Get or create actor and call action + const counter = client.counter.getOrCreate(name); + const newCount = await counter.increment(1); + + return c.json({ count: newCount }); +}); + +// Start server with RivetKit +serve(app); +``` + +```ts Express.js +import { registry } from "./registry"; +import express from "express"; + +// Start RivetKit +const { client, handler } = registry.createServer(); + +// Setup Express app +const app = express(); +app.use(express.json()); + +// Mount RivetKit handler +app.use("/registry", handler); + +// Example API endpoints +app.post("/increment/:name", async (req, res) => { + const { name } = req.params; + + const counter = client.counter.getOrCreate(name); + const newCount = await counter.increment(1); + + res.json({ count: newCount }); +}); + +app.listen(8080, () => { + console.log("Server running at http://localhost:8080"); +}); +``` + +```ts Elysia +import { registry } from "./registry"; +import { Elysia } from "elysia"; + +// Start RivetKit +const { client, handler } = registry.createServer(); + +// Setup Elysia app +const app = new Elysia() + .mount("/registry", handler) + .post("/increment/:name", async ({ params, body }) => { + const { name } = params; + + const counter = client.counter.getOrCreate(name); + const newCount = await counter.increment(1); + + return { count: newCount }; + }) + .listen(8080); + +console.log("Server running at http://localhost:8080"); +``` + + + + +The `/registry` endpoint is automatically mounted by RivetKit and is required for client communication. When using `serve()` with Hono, this is handled automatically. + + + + + + + + +```sh Node.js +npx tsx --watch server.ts +``` + +```sh Bun +bun --watch server.ts +``` + + + +Your server is now running at `http://localhost:8080` + + + + + +Test your counter actor using HTTP requests: + + + +```ts JavaScript +// Increment counter +const response = await fetch("http://localhost:8080/increment/my-counter", { + method: "POST" +}); + +const result = await response.json(); +console.log("Count:", result.count); // 1 +``` + +```sh curl +# Increment counter +curl -X POST http://localhost:8080/increment/my-counter +``` + + + + + + + +By default, RivetKit stores actor state on the local file system and will not scale in production. + +The following providers let you deploy & scale RivetKit: + + + + + +[Rivet](https://rivet.gg) provides open-source infrastructure to deploy & scale RivetKit. To deploy to Rivet, provide this config: + +```json rivet.json +{ + "rivetkit": { + "registry": "src/registry.ts", + "server": "src/server.ts" + } +} +``` + +And deploy with: + +```sh +npx rivet-cli deploy +``` + +Your endpoint will be available at your Rivet project URL. + + + + + +Deploy to Cloudflare Workers, install the Cloudflare Workers driver: + +```sh +npm install @rivetkit/cloudflare-workers +``` + +Update your `server.ts` to support Cloudflare Workers: + + + + ```ts Hono + import { createServer } from "@rivetkit/cloudflare-workers"; + import { Hono } from "hono"; + import { registry } from "./registry"; + + const { client, createHandler } = createServer(registry); + + // Setup router + const app = new Hono(); + + // ... etc ... + + const { handler, ActorHandler } = createHandler(app); + + export { handler as default, ActorHandler }; + ``` + + ```ts No Router + import { createServerHandler } from "@rivetkit/cloudflare-workers"; + import { registry } from "./registry"; + + const { handler, ActorHandler } = createServerHandler(registry); + export { handler as default, ActorHandler }; + ``` + + + +Update your configuration file to support `ACTOR_DO` and `ACTOR_KV` bindings: + +```json wrangler.json +{ + "name": "my-rivetkit-app", + "main": "src/index.ts", + "compatibility_date": "2025-01-20", + "compatibility_flags": ["nodejs_compat"], + "migrations": [ + { + "tag": "v1", + "new_classes": ["ActorHandler"] + } + ], + "durable_objects": { + "bindings": [ + { + "name": "ACTOR_DO", + "class_name": "ActorHandler" + } + ] + }, + "kv_namespaces": [ + { + "binding": "ACTOR_KV", + "id": "your_namespace_id" + } + ] +} +``` + +Finally, deploy: + +```sh +wrangler deploy +``` + + + + + +For production with Redis storage, install the Redis driver: + +```sh +npm install @rivetkit/redis +``` + +Then configure the driver: + +```ts server.ts +import { registry } from "./registry"; + +const { client, serve } = registry.createServer({ + driver: createRedisDriver() +}); + +// ... rest of server setup ... +``` + +Your backend can now be deployed to your cloud provider of choice. + + + + + + + + + +## Configuration Options + +### Connect Frontend To The Rivet Actor + +Create a type-safe client to connect from your frontend: + + + + + +```ts +import { createClient } from "@rivetkit/actor/client"; +import type { registry } from "./registry"; + +// Create typed client +const client = createClient("http://localhost:8080"); + +// Use the counter actor directly +const counter = client.counter.getOrCreate(["my-counter"]); + +// Call actions +const count = await counter.increment(3); +console.log("New count:", count); + +// Get current state +const currentCount = await counter.getCount(); +console.log("Current count:", currentCount); + +// Listen to real-time events +const connection = counter.connect(); +connection.on("countChanged", (newCount) => { + console.log("Count changed:", newCount); +}); + +// Increment through connection +await connection.increment(1); +``` + +See the [JavaScript client documentation](/clients/javascript) for more information. + + + + + +```tsx +import { useState } from "react"; +import { createClient, createRivetKit } from "@rivetkit/react"; +import type { registry } from "./registry"; + +const client = createClient("http://localhost:8080"); +const { useActor } = createRivetKit(client); + +function Counter() { + const [count, setCount] = useState(0); + + const counter = useActor({ + name: "counter", + key: ["my-counter"] + }); + + counter.useEvent("countChanged", (newCount: number) => { + setCount(newCount); + }); + + const increment = async () => { + await counter.connection?.increment(1); + }; + + return ( +
+

Count: {count}

+ +
+ ); +} +``` + +See the [React documentation](/clients/react) for more information. + +
+ + + +```rust +use rivetkit_client::{Client, EncodingKind, GetOrCreateOptions, TransportKind}; +use serde_json::json; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new( + "http://localhost:8080", + TransportKind::Sse, + EncodingKind::Json + ); + + // Get or create counter actor + let options = GetOrCreateOptions::default(); + let counter = client.get("counter", ["my-counter"].into(), options)? + .connect(); + + // Subscribe to events + counter.on_event("countChanged", |args| { + let count = args[0].as_i64().unwrap(); + println!("Count updated: {}", count); + }).await; + + // Call increment action + let result = counter.action("increment", vec![json!(1)]).await?; + println!("New count: {}", result); + + Ok(()) +} +``` + +See the [Rust client documentation](/clients/rust) for more information. + + + +
diff --git a/docs/actors/quickstart-frontend.mdx b/docs/actors/quickstart-frontend.mdx deleted file mode 100644 index 1794e7ea2..000000000 --- a/docs/actors/quickstart-frontend.mdx +++ /dev/null @@ -1,140 +0,0 @@ ---- -title: React -icon: react ---- - - - - -```sh -npm install rivetkit -``` - - - - -```ts registry.ts -import { actor, setup } from "rivetkit"; - -export const counter = actor({ - state: { count: 0 }, - actions: { - increment: (c, x: number) => { - c.state.count += x; - return c.state.count; - }, - }, -}); - -export const registry = setup({ - use: { counter }, -}); -``` - -TODO: important registry name - - - - - -```sh -npx rivet-cli dev src/registry.ts -``` - -TODO: If you want to set up a custom backend server, see other quickstart - - - - - -```tsx -import { useState } from "react"; -import { createClient, createRivetKit } from "@rivetkit/react"; -import type { Registry } from "../backend/registry"; - -const client = createClient(`http://localhost:8080/registry`); -const { useActor } = createRivetKit(client); - -function App() { - const [count, setCount] = useState(0); - const [counterName, setCounterName] = useState("test-counter"); - - const counter = useActor({ - name: "counter", - key: [counterName], - }); - - counter.useEvent("newCount", (x: number) => setCount(x)); - - const increment = async () => { - await counter.connection?.increment(1); - }; - - return ( -
-

Counter: {count}

- setCounterName(e.target.value)} - placeholder="Counter name" - /> - -
- ); -} - -export default App; -``` - -
- - - - - - - -```sh -npx rivet-cli deploy src/registry.ts --frontend -``` - -Your endpoint is now TODO - -Test it with TODO - - - - -TODO - - - - - - -
- -## Configuration options - -### Configure storage driver - -TODO: See backend - -### Add your own endpoints - -TODO: See backend & integrate with frontend - -### Run standalone server (no dev CLI) - -TODO: See backend - -## Next Steps - - - - - - - - diff --git a/docs/actors/quickstart-react.mdx b/docs/actors/quickstart-react.mdx new file mode 100644 index 000000000..16cb595a8 --- /dev/null +++ b/docs/actors/quickstart-react.mdx @@ -0,0 +1,296 @@ +--- +title: React Quickstart +sidebarTitle: React +description: Build real-time React applications with Rivet Actors +icon: react +--- + + + + +```sh +npm install @rivetkit/actor @rivetkit/react +``` + + + + + +Create your actor registry on the backend: + +```ts backend/registry.ts +import { actor, setup } from "@rivetkit/actor"; + +export const counter = actor({ + state: { count: 0 }, + actions: { + increment: (c, amount: number = 1) => { + c.state.count += amount; + c.broadcast("countChanged", c.state.count); + return c.state.count; + }, + getCount: (c) => c.state.count, + }, +}); + +export const registry = setup({ + use: { counter }, +}); +``` + + + + + +Start a server to run your actors: + +```ts backend/server.ts +import { registry } from "./registry"; +registry.runServer(); +``` + + + + + +Set up your React application: + +```tsx frontend/App.tsx +import { useState } from "react"; +import { createClient, createRivetKit } from "@rivetkit/react"; +import type { registry } from "../backend/registry"; + +// Create typed client +const client = createClient("http://localhost:8080"); +const { useActor } = createRivetKit(client); + +function App() { + const [count, setCount] = useState(0); + const [counterName, setCounterName] = useState("my-counter"); + + // Connect to the counter actor + const counter = useActor({ + name: "counter", + key: [counterName], + }); + + // Listen for real-time count updates + counter.useEvent("countChanged", (newCount: number) => { + setCount(newCount); + }); + + const increment = async () => { + // Call actor action through the connection + await counter.connection?.increment(1); + }; + + const incrementBy = async (amount: number) => { + await counter.connection?.increment(amount); + }; + + return ( +
+

RivetKit Counter

+

Count: {count}

+ +
+ +
+ +
+ + + +
+ +
+

Connection Status: {counter.isConnected ? "Connected" : "Disconnected"}

+

Try opening multiple tabs to see real-time sync.

+
+
+ ); +} + +export default App; +``` + +
+ + + +Configure Vite for development: + +```ts vite.config.ts +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + }, +}) +``` + + + + + +Start both the backend and frontend: + +**Terminal 1**: Start the backend + +```sh Backend +npx tsx --watch backend/server.ts +``` + +**Terminal 2**: Start the frontend + +```sh Frontend +npx vite +``` + +Open `http://localhost:5173` in your browser. Try opening multiple tabs to see real-time sync in action. + + + + + +By default, RivetKit stores actor state on the local file system and will not scale in production. + +The following providers let you deploy & scale RivetKit: + + + + + +[Rivet](https://rivet.gg) provides open-source infrastructure to deploy & scale RivetKit. To deploy to Rivet, provide this config: + +```json rivet.json +{ + "rivetkit": { + "registry": "src/registry.ts", + "server": "src/server.ts" + } +} +``` + +And deploy with: + +```sh +npx rivet-cli deploy +``` + +Your endpoint will be available at your Rivet project URL. + + + + + +Deploy to Cloudflare Workers, install the Cloudflare Workers driver: + +```sh +npm install @rivetkit/cloudflare-workers +``` + +Update your `server.ts` to support Cloudflare Workers: + + ```ts server.ts + import { createServerHandler } from "@rivetkit/cloudflare-workers"; + import { registry } from "./registry"; + + const { handler, ActorHandler } = createServerHandler(registry); + export { handler as default, ActorHandler }; + ``` + +Update your configuration file to support `ACTOR_DO` and `ACTOR_KV` bindings: + +```json wrangler.json +{ + "name": "my-rivetkit-app", + "main": "src/index.ts", + "compatibility_date": "2025-01-20", + "compatibility_flags": ["nodejs_compat"], + "migrations": [ + { + "tag": "v1", + "new_classes": ["ActorHandler"] + } + ], + "durable_objects": { + "bindings": [ + { + "name": "ACTOR_DO", + "class_name": "ActorHandler" + } + ] + }, + "kv_namespaces": [ + { + "binding": "ACTOR_KV", + "id": "your_namespace_id" + } + ] +} +``` + +Finally, deploy: + +```sh +wrangler deploy +``` + + + + + +For production with Redis storage, install the Redis driver: + +```sh +npm install @rivetkit/redis +``` + +Then configure the driver: + +```ts server.ts +import { registry } from "./registry"; + +const { client, serve } = registry.createServer({ + driver: createRedisDriver() +}); + +// ... rest of server setup ... +``` + +Your backend can now be deployed to your cloud provider of choice. + + + + + + + +
+ +## Configuration Options + +### Add Your Own Backend Endpoints + +Add custom HTTP endpoints alongside your actors to handle additional business logic, authentication, and integrations with external services. + +See [backend quickstart](/actors/quickstart-backend) for more information. + diff --git a/docs/actors/quickstart.mdx b/docs/actors/quickstart.mdx deleted file mode 100644 index 08a4c003a..000000000 --- a/docs/actors/quickstart.mdx +++ /dev/null @@ -1,308 +0,0 @@ ---- -title: Node.js & Bun -icon: node-js ---- - - - - -```sh -npm install @rivetkit/actor -``` - - - - -```ts registry.ts -import { actor, setup } from "@rivetkit/actor"; - -export const counter = actor({ - state: { count: 0 }, - actions: { - increment: (c, x: number) => { - c.state.count += x; - return c.state.count; - }, - }, -}); - -export const registry = setup({ - use: { counter }, -}); -``` - - - - - - - - -```ts Hono -import { registry } from "./registry"; -import { Hono } from "hono"; - -// Start RivetKit -// -// State is stored in memory, this can be configured later -const { client, serve } = registry.server(); - -// Setup server -const app = new Hono(); - -// Example endpoint -app.post("/increment/:name", async (c) => { - const name = c.req.param("name"); - - // Communicate with actor - const counter = client.counter.getOrCreate(name); - const newCount = await counter.increment(1); - - return c.text(`New Count: ${newCount}`); -}); - -// Start server -serve(app); -``` - - -```ts Express.js -TODO -``` - -```ts Elysia.js -TODO -``` - -TODO: How to serve without registry helper - -TODO: Why we need to use our own custom serve fn - - - - - TODO: Exporting `registry` and `app.fetch` (as `default`) is important. - - -_If you want to run without export fetch (i.e. standalone Node.js), see below._ - - - - - - - -```sh Node.js -npx tsx --watch src/server.ts -``` - -```sh Bun -bun run --watch src/server.ts -``` - - - - - - - - - -```ts fetch -const res = await fetch("http://localhost:8080/increment/foo", { - method: "POST" -}); -console.log("Output:", await res.text()); -``` - -```sh curl -curl -X POST localhost:8080/increment/foo -``` - - - - - - - - - - - - -```json rivet.json -{ - "rivetkit": { - "registry": "src/registry.ts", - "server": "src/server.ts" - } -} -``` - -```sh -npx rivet-cli deploy -``` - -Your endpoint is now TODO - -Test it with TODO - - - - -TODO - - - - -```sh -npm install @rivetkit/redis -``` - -```ts server.ts -import { registry } from "./registry"; -import { createRedisDriver } from "@rivetkit/redis"; - -// Start RivetKit -const { client, hono } = registry.run({ - driver: createRedisDriver({ - host: "127.0.0.1", - port: 6379, - }), -}); - -// ...rest of code... -``` - - - - - -```sh -npm install @rivetkit/file-system -``` - -```ts server.ts -import { registry } from "./registry"; -import { createFileSystemDriver } from "@rivetkit/file-system"; - -// Start RivetKit -const { client, hono } = registry.run({ - driver: createFileSystemDriver(), -}); - -// ...rest of code... -``` - - - - - - - - - -## Configuration Options - -### Connect your frontend to the Rivet Actor - -TODO: Quick summary of why you would want to connect your frontend - -Connect your frontend: - - - -```ts JavaScript -import { createClient } from "@rivetkit/actor/client"; -import type { registry } from "./registry.js"; - -const client = createClient("http://localhost:8080/registry"); - -const result = await client.myActor.getOrCreate().myAction("Hello, world!"); -``` - -```ts React -import { useState } from "react"; -import { createClient, createRivetKit } from "@@rivetkit/actor/react"; -import type { registry } from "./registry"; - -const client = createClient(`http://localhost:8080/registry`); -const { useActor } = createRivetKit(client); - -function App() { - const [count, setCount] = useState(0); - const [counterName, setCounterName] = useState("test-counter"); - - const counter = useActor({ - name: "counter", - key: [counterName], - }); - - counter.useEvent("newCount", (x: number) => setCount(x)); - - const increment = async () => { - await counter.connection?.increment(1); - }; - - return ( -
-

Counter: {count}

- setCounterName(e.target.value)} - placeholder="Counter name" - /> - -
- ); -} -``` - -
- -TODO: Learn more under the XXXX docs - - - TODO: Link to onAuth docs - - - - TODO: Note that `/registry` must be exposed - - -### Run as standalone server (no fetch handler) - -TODO: Intro - -```ts -import { registry } from "./registry"; -import { Hono } from "hono"; -import { serve } from "@hono/node-server"; - -// Start RivetKit -const { client, hono } = registry.run(); - -// Setup server -const app = new Hono(); - -// ...setup routes... - -serve({ fetch: app.fetch, port: 8080 }, (x) => - console.log("Listening at http://localhost:8080"), -); -``` - -IMPORTANT: You'll need to do special stuff to support deploying to Rivet or Cloudflare Workers - -## Next Steps - -TODO - - - - - - - - diff --git a/docs/concepts/scaling.mdx b/docs/actors/scaling.mdx similarity index 91% rename from docs/concepts/scaling.mdx rename to docs/actors/scaling.mdx index 9922803d1..28bdea081 100644 --- a/docs/concepts/scaling.mdx +++ b/docs/actors/scaling.mdx @@ -12,7 +12,7 @@ Actors scale by design through these key properties: | Property | Description | | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Independent State** | Each actor manages its own private data separately from other actors, so they never conflict with each other when running at the same time (i.e. using locking mechanisms). | -| **Action- & Event-Based Communication** | Actors communicate through asynchronous [actions](/concepts/actions) or [events](/concepts/events), making it easy to distribute them across different machines. | +| **Action- & Event-Based Communication** | Actors communicate through asynchronous [actions](/actors/actions) or [events](/actors/events), making it easy to distribute them across different machines. | | **Location Transparency** | Unlike traditional servers, actors don't need to know which machine other actors are running on in order to communicate with each other. They can run on the same machine, across a network, and across the world. Actors handle the network routing for you under the hood. | | **Horizontal Scaling** | Actors distribute workload by splitting responsibilities into small, focused units. Since each actor handles a limited scope (like a single user, document, or chat room), the system automatically spreads load across many independent actors rather than concentrating it in a single place. | @@ -28,7 +28,7 @@ Here are key principles for architecting your actor system: **State Management** - Each actor owns and manages only its own state -- Use [actions](/concepts/actions) to request data from other actors +- Use [actions](/actors/actions) to request data from other actors - Keep state minimal and relevant to the actor's core responsibility **Granularity Guidelines** diff --git a/docs/actors/schedule.mdx b/docs/actors/schedule.mdx index 2f8c5ad60..026037308 100644 --- a/docs/actors/schedule.mdx +++ b/docs/actors/schedule.mdx @@ -38,7 +38,7 @@ Currently, scheduling can only trigger public actions. If the scheduled action i ## Full Example ```typescript -import { actor } from "rivetkit"; +import { actor } from "@rivetkit/actor"; const reminderService = actor({ state: { diff --git a/docs/actors/state.mdx b/docs/actors/state.mdx index 7462c4764..bb32111b7 100644 --- a/docs/actors/state.mdx +++ b/docs/actors/state.mdx @@ -9,17 +9,19 @@ Actor state provides the best of both worlds: it's stored in-memory and persiste **Using External SQL Databases** Actors can also be used with external SQL databases. This can be useful to integrate actors with existing -applications or for storing relational data. Read more [here](/concepts/external-sql). +applications or for storing relational data. Read more [here](/actors/external-sql). ## Initializing State There are two ways to define a actor's initial state: -**Method 1: Static Initial State** + + + ```typescript -import { actor } from "rivetkit"; +import { actor } from "@rivetkit/actor"; // Simple state with a constant const counter = actor({ @@ -32,10 +34,12 @@ const counter = actor({ }); ``` -**Method 2: Dynamic Initial State** + + + ```typescript -import { actor } from "rivetkit"; +import { actor } from "@rivetkit/actor"; // State with initialization logic const counter = actor({ @@ -50,14 +54,18 @@ const counter = actor({ }); ``` -The `createState` function is called once when the actor is first created. See [Lifecycle](/concepts/lifecycle) for more details. + + + + +The `createState` function is called once when the actor is first created. See [Lifecycle](/actors/lifecycle) for more details. ## Modifying State To update state, modify the `state` property on the context object (`c.state`) in your actions: ```typescript -import { actor } from "rivetkit"; +import { actor } from "@rivetkit/actor"; const counter = actor({ state: { count: 0 }, @@ -89,7 +97,7 @@ Actors automatically handle persisting state transparently. This happens at the In the rare occasion you need to force a state change mid-action, you can use `c.saveState()`. This should only be used if your action makes an important state change that needs to be persisted before the action completes. ```typescript -import { actor } from "rivetkit"; +import { actor } from "@rivetkit/actor"; const criticalProcess = actor({ state: { @@ -122,14 +130,14 @@ const criticalProcess = actor({ Each actor's state is completely isolated, meaning it cannot be accessed directly by other actors or clients. This allows actors to maintain a high level of security and data integrity, ensuring that state changes are controlled and predictable. -To interact with a actor's state, you must use [Actions](/concepts/actions). Actions provide a controlled way to read from and write to the state. +To interact with a actor's state, you must use [Actions](/actors/actions). Actions provide a controlled way to read from and write to the state. ## Sharing State Between Actors If you need a shared state between multiple actors, you have two options: 1. Create a actor that holds the shared state that other actors can make action calls to -2. Use an external database, see [External SQL Databases](/concepts/external-sql) +2. Use an external database, see [External SQL Databases](/actors/external-sql) ## Ephemeral Variables @@ -141,10 +149,12 @@ In addition to persisted state, RivetKit provides a way to store ephemeral data There are two ways to define a actor's initial vars: -**Method 1: Static Initial Variables** + + + ```typescript -import { actor } from "rivetkit"; +import { actor } from "@rivetkit/actor"; // Define vars as a constant const counter = actor({ @@ -166,10 +176,12 @@ const counter = actor({ When using static `vars`, all values must be compatible with `structuredClone()`. If you need to use non-serializable objects, use `createVars` instead, which allows you to create these objects on the fly. -**Method 2: Dynamic Initial Variables** + + + ```typescript -import { actor } from "rivetkit"; +import { actor } from "@rivetkit/actor"; // Define vars with initialization logic const counter = actor({ @@ -189,12 +201,16 @@ const counter = actor({ }); ``` + + + + ### Using Variables Vars can be accessed and modified through the context object with `c.vars`: ```typescript -import { actor } from "rivetkit"; +import { actor } from "@rivetkit/actor"; import { createNanoEvents } from "nanoevents"; const counter = actor({ diff --git a/docs/clients/javascript.mdx b/docs/clients/javascript.mdx index 51efe3736..24b475d0e 100644 --- a/docs/clients/javascript.mdx +++ b/docs/clients/javascript.mdx @@ -54,19 +54,19 @@ The RivetKit JavaScript client allows you to connect to and interact with actors ```sh npm - npm install rivetkit + npm install @rivetkit/actor ``` ```sh pnpm - pnpm add rivetkit + pnpm add @rivetkit/actor ``` ```sh yarn - yarn add rivetkit + yarn add @rivetkit/actor ``` ```sh bun - bun add rivetkit + bun add @rivetkit/actor ``` @@ -77,7 +77,7 @@ The RivetKit JavaScript client allows you to connect to and interact with actors Create a file `src/client.ts` in your project to connect to your actor: ```typescript src/client.ts - import { createClient } from "rivetkit/client"; + import { createClient } from "@rivetkit/actor/client"; import type { App } from "../actors/app"; async function main() { @@ -139,5 +139,5 @@ The RivetKit JavaScript client allows you to connect to and interact with actors ## Next Steps -See the [Interacting with Actors](/concepts/interacting-with-actors) documentation for information on how to use the client. +For more information on communicating with actors, including event handling and RPC calls, see [Communicating with Actors](/actors/communicating-with-actors). diff --git a/docs/clients/python.mdx b/docs/clients/python.mdx deleted file mode 100644 index 829ee1560..000000000 --- a/docs/clients/python.mdx +++ /dev/null @@ -1,131 +0,0 @@ ---- -title: Python -icon: python ---- - -import MvpWarning from "/snippets/mvp-warning.mdx"; -import StepDefineActor from "/snippets/step-define-actor.mdx"; -import StepRunStudio from "/snippets/step-run-studio.mdx"; -import StepDeploy from "/snippets/step-deploy.mdx"; -import SetupNextSteps from "/snippets/setup-next-steps.mdx"; - -The RivetKit Python client provides a way to connect to and interact with actors from Python applications. - - - -## Quickstart - - - - Create a new directory for your project: - - ```sh - mkdir my-app - cd my-app - ``` - - It's recommended to create a virtual environment: - ```sh - python -m venv venv - source venv/bin/activate # On Windows use: venv\Scripts\activate - ``` - - - - Install the RivetKit client package: - - ```sh - pip install rivetkit-client - ``` - - - - - - Create a new file `main.py`: - - - ```python Async - import asyncio - from rivetkit_client import AsyncClient - - async def main(): - # Replace with your endpoint URL after deployment - client = AsyncClient("http://localhost:8080") - - # Get or create a actor instance - counter = await client.get("counter") - - # Subscribe to events using callback - def on_new_count(msg): - print(f"Event: {msg}") - - counter.on_event("newCount", on_new_count) - - # Call an action - result = await counter.action("increment", 5) - print(f"Action result: {result}") - - # Wait to receive events - await asyncio.sleep(1) - - # Clean up - await counter.disconnect() - - if __name__ == "__main__": - asyncio.run(main()) - ``` - - ```python Sync - from rivetkit_client import Client - - # Replace with your endpoint URL after deployment - client = Client("http://localhost:8080") - - # Get or create a actor instance - counter = client.get("counter") - - # Subscribe to events using callback - def on_new_count(msg): - print(f"Event: {msg}") - - # Clean up once we receive our event - counter.disconnect() - - counter.on_event("newCount", on_new_count) - - # Call an action - result = counter.action("increment", 5) - print(f"Action result: {result}") - - # Clean is handled on by on_new_count - ``` - - - In the code above, subscription is done with `on_event` callbacks, but you can also - subscribe directly with `receive()` calls, using the `SimpleClient` (and `AsyncSimpleClient`) - interfaces. See our [sample usage](https://github.com/rivet-gg/rivetkit/tree/main/clients/python/tests/test_e2e_simple_async.py) for more details. - - - - - - In a separate terminal, run your Python code: - - ```sh - python main.py - ``` - - You should see output like: - ``` - Event: 5 - Action result: 5 - ``` - - Run it again to see the state update. - - - - - - diff --git a/docs/clients/react.mdx b/docs/clients/react.mdx new file mode 100644 index 000000000..42ae30f8f --- /dev/null +++ b/docs/clients/react.mdx @@ -0,0 +1,644 @@ +--- +title: React +description: Build real-time React applications with Rivet Actors +icon: react +--- + +Learn how to create real-time, stateful React applications with RivetKit's actor model. The React integration provides intuitive hooks for managing actor connections and real-time updates. + +## Installation + +Install the RivetKit React package: + +```bash +npm install @rivetkit/actor @rivetkit/react +``` + +## Basic Usage + + + +First, set up your actor registry (typically in your backend): + +```typescript +// backend/registry.ts +import { actor, setup } from "@rivetkit/actor"; + +export const counter = actor({ + state: { count: 0 }, + actions: { + increment: (c, amount: number = 1) => { + c.state.count += amount; + c.broadcast("countChanged", c.state.count); + return c.state.count; + }, + getCount: (c) => c.state.count, + }, +}); + +export const registry = setup({ + use: { counter }, +}); +``` + + + +Create a typed client and RivetKit hooks: + +```tsx +// src/rivetkit.ts +import { createClient, createRivetKit } from "@rivetkit/react"; +import type { registry } from "../backend/registry"; + +export const client = createClient("http://localhost:8080"); +export const { useActor } = createRivetKit(client); +``` + + + +Connect to actors and listen for real-time updates: + +```tsx +// src/App.tsx +import { useState } from "react"; +import { useActor } from "./rivetkit"; + +function App() { + const [count, setCount] = useState(0); + const [counterName, setCounterName] = useState("my-counter"); + + // Connect to the counter actor + const counter = useActor({ + name: "counter", + key: [counterName], + }); + + // Listen for real-time count updates + counter.useEvent("countChanged", (newCount: number) => { + setCount(newCount); + }); + + const increment = async () => { + await counter.connection?.increment(1); + }; + + return ( +
+

RivetKit Counter

+

Count: {count}

+ +
+ +
+ + + +
+

Status: {counter.isConnected ? "Connected" : "Disconnected"}

+
+
+ ); +} + +export default App; +``` +
+
+ +## API Reference + +### `createRivetKit(client, options?)` + +Creates the RivetKit hooks for React integration. + +```tsx +import { createClient, createRivetKit } from "@rivetkit/react"; + +const client = createClient("http://localhost:8080"); +const { useActor } = createRivetKit(client); +``` + +#### Parameters + +- `client`: The RivetKit client created with `createClient` +- `options`: Optional configuration object + +#### Returns + +An object containing: +- `useActor`: Hook for connecting to actors + +### `useActor(options)` + +Hook that connects to an actor and manages the connection lifecycle. + +```tsx +const actor = useActor({ + name: "actorName", + key: ["actor-id"], + params: { userId: "123" }, + enabled: true +}); +``` + +#### Parameters + +- `options`: Object containing: + - `name`: The name of the actor type (string) + - `key`: Array of strings identifying the specific actor instance + - `params`: Optional parameters passed to the actor connection + - `enabled`: Optional boolean to conditionally enable/disable the connection (default: true) + +#### Returns + +Actor object with the following properties: +- `connection`: The actor connection for calling actions, or `null` if not connected +- `isConnected`: Boolean indicating if the actor is connected +- `state`: Current actor state (if available) +- `useEvent(eventName, handler)`: Method to subscribe to actor events + +### `actor.useEvent(eventName, handler)` + +Subscribe to events emitted by the actor. + +```tsx +const actor = useActor({ name: "counter", key: ["my-counter"] }); + +actor.useEvent("countChanged", (newCount: number) => { + console.log("Count changed:", newCount); +}); +``` + +#### Parameters + +- `eventName`: The name of the event to listen for (string) +- `handler`: Function called when the event is emitted + +#### Lifecycle + +The event subscription is automatically managed: +- Subscribes when the actor connects +- Cleans up when the component unmounts or actor disconnects +- Re-subscribes on reconnection + +## Advanced Patterns + +### Multiple Actors + +Connect to multiple actors in a single component: + +```tsx +function Dashboard() { + const userProfile = useActor({ + name: "userProfile", + key: ["user-123"] + }); + + const notifications = useActor({ + name: "notifications", + key: ["user-123"] + }); + + userProfile.useEvent("profileUpdated", (profile) => { + console.log("Profile updated:", profile); + }); + + notifications.useEvent("newNotification", (notification) => { + console.log("New notification:", notification); + }); + + return ( +
+ + +
+ ); +} +``` + +### Conditional Connections + +Control when actors connect using the `enabled` option: + +```tsx +function ConditionalActor() { + const [enabled, setEnabled] = useState(false); + + const counter = useActor({ + name: "counter", + key: ["conditional"], + enabled: enabled // Only connect when enabled + }); + + return ( +
+ + {enabled && counter.isConnected && ( +

Count: {counter.state?.count}

+ )} +
+ ); +} +``` + +### Authentication + +Pass authentication parameters to actors: + +```tsx +function AuthenticatedChat() { + const [authToken] = useAuthToken(); // Your auth hook + + const chatRoom = useActor({ + name: "chatRoom", + key: ["general"], + params: { + authToken, + userId: getCurrentUserId() + } + }); + + chatRoom.useEvent("messageReceived", (message) => { + console.log("New message:", message); + }); + + const sendMessage = async (text: string) => { + await chatRoom.connection?.sendMessage(text); + }; + + return ( +
+ {/* Chat UI */} +
+ ); +} +``` + +### Error Handling + +Handle connection errors gracefully: + +```tsx +function ResilientCounter() { + const [error, setError] = useState(null); + + const counter = useActor({ + name: "counter", + key: ["resilient"] + }); + + counter.useEvent("error", (err) => { + setError(err.message); + // Clear error after 5 seconds + setTimeout(() => setError(null), 5000); + }); + + counter.useEvent("connected", () => { + setError(null); + }); + + return ( +
+ {error && ( +
+ Error: {error} +
+ )} +
+ Status: {counter.isConnected ? "Connected" : "Disconnected"} +
+ {/* Rest of component */} +
+ ); +} +``` + +### Custom Hooks + +Create reusable custom hooks for common patterns: + +```tsx +// Custom hook for a counter with persistent state +function useCounter(counterId: string) { + const [count, setCount] = useState(0); + + const counter = useActor({ + name: "counter", + key: [counterId] + }); + + counter.useEvent("countChanged", setCount); + + const increment = useCallback(async (amount = 1) => { + await counter.connection?.increment(amount); + }, [counter.connection]); + + const reset = useCallback(async () => { + await counter.connection?.reset(); + }, [counter.connection]); + + return { + count, + increment, + reset, + isConnected: counter.isConnected + }; +} + +// Usage +function App() { + const { count, increment, reset, isConnected } = useCounter("my-counter"); + + return ( +
+

Count: {count}

+ + +
+ ); +} +``` + +### Real-time Collaboration + +Build collaborative features with multiple event listeners: + +```tsx +function CollaborativeEditor() { + const [content, setContent] = useState(""); + const [cursors, setCursors] = useState>({}); + + const document = useActor({ + name: "document", + key: ["doc-123"], + params: { userId: getCurrentUserId() } + }); + + // Listen for content changes + document.useEvent("contentChanged", (newContent) => { + setContent(newContent); + }); + + // Listen for cursor movements + document.useEvent("cursorMoved", ({ userId, position }) => { + setCursors(prev => ({ ...prev, [userId]: position })); + }); + + // Listen for user join/leave + document.useEvent("userJoined", ({ userId }) => { + console.log(`${userId} joined the document`); + }); + + document.useEvent("userLeft", ({ userId }) => { + setCursors(prev => { + const { [userId]: _, ...rest } = prev; + return rest; + }); + }); + + const updateContent = async (newContent: string) => { + await document.connection?.updateContent(newContent); + }; + + return ( +
+ +
+ ); +} +``` + +## Client Connection Options + +### Basic Client Setup + +Create a type-safe client to connect to your backend: + +```ts client.ts +import { createClient } from "@rivetkit/actor/client"; +import type { registry } from "./registry"; + +// Create typed client +const client = createClient("http://localhost:8080"); + +// Use the counter actor directly +const counter = client.counter.getOrCreate(["my-counter"]); + +// Call actions +const count = await counter.increment(3); +console.log("New count:", count); + +// Get current state +const currentCount = await counter.getCount(); +console.log("Current count:", currentCount); + +// Listen to real-time events +const connection = counter.connect(); +connection.on("countChanged", (newCount) => { + console.log("Count changed:", newCount); +}); + +// Increment through connection +await connection.increment(1); +``` + +### React Integration + +Use the React hooks for seamless integration: + +```tsx +import { useState } from "react"; +import { createClient, createRivetKit } from "@rivetkit/react"; +import type { registry } from "./registry"; + +const client = createClient("http://localhost:8080"); +const { useActor } = createRivetKit(client); + +function App() { + const [count, setCount] = useState(0); + const [counterName, setCounterName] = useState("test-counter"); + + const counter = useActor({ + name: "counter", + key: [counterName], + }); + + counter.useEvent("countChanged", (newCount: number) => setCount(newCount)); + + const increment = async () => { + await counter.connection?.increment(1); + }; + + return ( +
+

Counter: {count}

+ setCounterName(e.target.value)} + placeholder="Counter name" + /> + +
+ ); +} +``` + +## Environment Configuration + +### Development vs Production + +Create environment-specific configurations: + +```ts config.ts +const isDev = process.env.NODE_ENV !== "production"; + +export const config = { + port: parseInt(process.env.PORT || "8080"), + rivetkit: { + driver: isDev + ? { + topology: "standalone" as const, + actor: { type: "memory" as const }, + manager: { type: "memory" as const }, + } + : { + topology: "partition" as const, + actor: { type: "redis" as const, url: process.env.REDIS_URL! }, + manager: { type: "redis" as const, url: process.env.REDIS_URL! }, + }, + }, +}; +``` + +### Backend Configuration + +Update your server to use environment-based configuration: + +```ts server.ts +import { registry } from "./registry"; +import { config } from "./config"; + +const { client, serve } = registry.createServer(config.rivetkit); + +// ... rest of server setup +``` + +### Frontend Environment Variables + +Configure your frontend for different environments: + +```ts .env.local +VITE_API_URL=http://localhost:8080 +VITE_WS_URL=ws://localhost:8080 +``` + +```ts config/client.ts +const API_URL = import.meta.env.VITE_API_URL || "http://localhost:8080"; + +export const client = createClient(API_URL); +``` + +## Authentication Integration + +### Protected Actors + +Add authentication to secure your actors: + +```ts registry.ts +import { actor, setup } from "@rivetkit/actor"; + +export const protectedCounter = actor({ + onAuth: async (opts) => { + const token = opts.params.authToken || opts.req.headers.get("Authorization"); + + if (!token) { + throw new Error("Authentication required"); + } + + // Validate token and return user data + const user = await validateJWT(token); + return { userId: user.id, role: user.role }; + }, + + state: { count: 0 }, + + actions: { + increment: (c, amount: number = 1) => { + // Access auth data via c.conn.auth + const { userId } = c.conn.auth; + + c.state.count += amount; + c.broadcast("countChanged", { count: c.state.count, userId }); + return c.state.count; + }, + }, +}); +``` + +### React Authentication + +Connect authenticated actors in React: + +```tsx +function AuthenticatedApp() { + const [authToken, setAuthToken] = useState(null); + + const counter = useActor({ + name: "protectedCounter", + key: ["user-counter"], + params: { + authToken: authToken + }, + enabled: !!authToken // Only connect when authenticated + }); + + const login = async () => { + const token = await authenticateUser(); + setAuthToken(token); + }; + + if (!authToken) { + return ; + } + + return ( +
+

Authenticated Counter

+ {/* ... rest of authenticated UI */} +
+ ); +} +``` + +Learn more about [authentication](/general/authentication). + +## Best Practices + +1. **Use Custom Hooks**: Extract actor logic into reusable custom hooks +2. **Handle Loading States**: Always account for the initial loading state +3. **Error Boundaries**: Implement error boundaries around actor components +4. **Conditional Connections**: Use the `enabled` prop to control when actors connect +5. **Event Cleanup**: Event listeners are automatically cleaned up, but be mindful of heavy operations in handlers +6. **State Management**: Combine with React state for local UI state that doesn't need to be shared + diff --git a/docs/concepts/cors.mdx b/docs/concepts/cors.mdx deleted file mode 100644 index 4b4f0b3e4..000000000 --- a/docs/concepts/cors.mdx +++ /dev/null @@ -1,70 +0,0 @@ ---- -title: Cross-Origin Resource Sharing -sidebarTitle: CORS -icon: share-nodes ---- - -Cross-Origin Resource Sharing (CORS) is a security mechanism that allows a web application running at one origin to access resources from a different origin. Without CORS, browsers block cross-origin HTTP requests by default as a security measure. - -You'll need to configure CORS when: - -- **Local Development**: You're developing locally and your client runs on a different port than your actor service -- **Different Domain**: Your frontend application is hosted on a different domain than your actor service - -## Example - -```ts -import { setup } from "rivetkit"; -import counter from "./counter"; - -const registry = setup({ - use: { counter }, - // Change this to match your frontend's origin - cors: { origin: "https://yourdomain.com" } -}); -``` - -## Options - -### `origin` - -`string | string[] | (origin:string, c:Context) => string` (optional) - -Specifies which domains can access your resources. Options: -- Single domain: `"https://example.com"` -- Multiple domains: `["https://app.com", "https://admin.com"]` -- Dynamic validation: `(origin) => origin.endsWith('.example.com') ? origin : null` -- All domains (not recommended for production): `"*"` (default) - -### `allowMethods` - -`string[]` (optional) - -HTTP methods clients are allowed to use when accessing your resources. Default includes all standard methods: `['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH']`. - -### `allowHeaders` - -`string[]` (optional) - -Custom HTTP headers clients can send in requests. Empty by default `[]`. Common examples: -- `["Content-Type", "Authorization", "X-API-Key"]` - -### `maxAge` - -`number` (optional) - -How long browsers should cache CORS response (in seconds). Higher values improve performance by reducing preflight requests. - -### `credentials` - -`boolean` (optional) - -Whether requests can include user credentials like cookies or HTTP authentication. Must specify exact origins when enabled (cannot use with `origin: "*"`). - -### `exposeHeaders` - -`string[]` (optional) - -Server headers that browsers are allowed to access. Empty by default `[]`. Example: -- `["Content-Length", "X-Request-Id"]` - diff --git a/docs/concepts/interacting-with-workers.mdx b/docs/concepts/interacting-with-workers.mdx deleted file mode 100644 index ab6809eb5..000000000 --- a/docs/concepts/interacting-with-workers.mdx +++ /dev/null @@ -1,614 +0,0 @@ ---- -title: Interacting with Actors -icon: square-code ---- - -This guide covers how to connect to and interact with actors from client applications. - -## Setting Up the Client - -The first step is to create a client that will connect to your actor service: - - -```typescript TypeScript -import { createClient } from "rivetkit/client"; -import type { App } from "../src/index"; - -// Create a client with the connection address and app type -const client = createClient(/* CONNECTION ADDRESS */); -``` - -```rust Rust -use rivetkit_client::{Client, EncodingKind, GetOrCreateOptions, TransportKind}; - -// Create a client with connection address and configuration -let client = Client::new( - "http://localhost:8080", // Connection address - TransportKind::WebSocket, // Transport (WebSocket or SSE) - EncodingKind::Cbor, // Encoding (Json or Cbor) -); -``` - -```python Python -from rivetkit_client import AsyncClient as ActorClient - -# Create a client with the connection address -client = ActorClient("http://localhost:8080") -``` - - -See the setup guide for your platform for details on how to get the connection address. - -## Finding & Connecting to Actors - -RivetKit provides several methods to connect to actors: - -### `get(tags, opts)` - Find or Create - -The most common way to connect is with `get()`, which finds an existing actor matching the provided tags or creates a new one: - - -```typescript TypeScript -// Connect to a chat room for the "general" channel -const room = await client.chatRoom.get({ - name: "chat_room", - channel: "general" -}); - -// Now you can call methods on the actor -await room.sendMessage("Alice", "Hello everyone!"); -``` - -```rust Rust -use rivetkit_client::GetOrCreateOptions; -use serde_json::json; - -// Connect to a chat room for the "general" channel -let tags = vec![ - ("name".to_string(), "chat_room".to_string()), - ("channel".to_string(), "general".to_string()), -]; - -let mut options = GetOrCreateOptions { - tags: Some(tags), - ..Default::default() -}; - -let room = client.get("chatRoom", options)? - .connect(); - -// Now you can call methods on the actor -room.action("sendMessage", vec![json!("Alice"), json!("Hello everyone!")]) - .await - .expect("Failed to send message"); -``` - -```python Python -# Connect to a chat room for the "general" channel -room = await client.get("chatRoom", tags=[ - ("name", "chat_room"), - ("channel", "general") -]) - -# Now you can call methods on the actor -await room.action("sendMessage", ["Alice", "Hello everyone!"]) -``` - - -### `create(opts)` - Explicitly Create New - -When you specifically want to create a new actor instance: - - -```typescript TypeScript -// Create a new document actor -const doc = await client.myDocument.create({ - create: { - tags: { - name: "my_document", - docId: "123" - } - } -}); - -await doc.initializeDocument("My New Document"); -``` - -```rust Rust -use rivetkit_client::CreateOptions; -use rivetkit_client::client::CreateRequestMetadata; -use serde_json::json; - -// Create a new document actor -let tags = vec![ - ("name".to_string(), "my_document".to_string()), - ("docId".to_string(), "123".to_string()), -]; - -let create_options = CreateOptions::default(); - -let doc = client.create("myDocument", ["tags-or-keys"].into(), create_options) - .expect("Failed to create document") - .connect(); - -// Initialize the document -doc.action("initializeDocument", vec![json!("My New Document")]) - .await - .expect("Failed to initialize document"); -``` - -```python Python -# Create a new document actor -doc = await client.get("myDocument", tags=[ - ("name", "my_document"), - ("docId", "123") -]) - -await doc.action("initializeDocument", ["My New Document"]) -``` - - -### `getWithId(id, opts)` - Connect by ID - -Connect to an actor using its internal ID: - - -```typescript TypeScript -// Connect to a specific actor by its ID -const myActorId = "55425f42-82f8-451f-82c1-6227c83c9372"; -const doc = await client.myDocument.getWithId(myActorId); - -await doc.updateContent("Updated content"); -``` - -```rust Rust -use rivetkit_client::GetWithIdOptions; - -// Connect to a specific actor by its ID -let my_actor_id = "55425f42-82f8-451f-82c1-6227c83c9372"; -let options = GetWithIdOptions::default(); -let doc = client.get_with_id(my_actor_id, [].into(), options) - .expect("Failed to get document") - .connect(); - -// Update content -doc.action("updateContent", vec![json!("Updated content")]) - .await - .expect("Failed to update document"); -``` - -```python Python -# Connect to a specific actor by its ID -my_actor_id = "55425f42-82f8-451f-82c1-6227c83c9372" -doc = await client.get_with_id(my_actor_id) - -await doc.action("updateContent", ["Updated content"]) -``` - - - -It's usually better to use tags for discovery rather than directly using actor IDs. - - -## Calling Actions - -Once connected, calling actor actions are straightforward: - - -```typescript TypeScript -// Call an action -const result = await mathUtils.multiplyByTwo(5); -console.log(result); // 10 - -// Call an action with multiple parameters -await chatRoom.sendMessage("Alice", "Hello everyone!"); - -// Call an action with an object parameter -await gameRoom.updateSettings({ - maxPlayers: 10, - timeLimit: 300, - gameMode: "capture-the-flag" -}); -``` - -```rust Rust -use serde_json::json; - -// Call an action -let result = math_utils.action("multiplyByTwo", vec![json!(5)]) - .await - .expect("Failed to call multiplyByTwo"); -println!("Result: {}", result.as_i64().unwrap()); // 10 - -// Call an action with multiple parameters -chat_room.action("sendMessage", vec![json!("Alice"), json!("Hello everyone!")]) - .await - .expect("Failed to send message"); - -// Call an action with an object parameter -let settings = json!({ - "maxPlayers": 10, - "timeLimit": 300, - "gameMode": "capture-the-flag" -}); -game_room.action("updateSettings", vec![settings]) - .await - .expect("Failed to update settings"); -``` - -```python Python -# Call an action -result = await math_utils.action("multiplyByTwo", [5]) -print(result) # 10 - -# Call an action with multiple parameters -await chat_room.action("sendMessage", ["Alice", "Hello everyone!"]) - -# Call an action with an object parameter -await game_room.action("updateSettings", [{ - "maxPlayers": 10, - "timeLimit": 300, - "gameMode": "capture-the-flag" -}]) -``` - - - -All actor action calls are asynchronous and require `await`, even if the actor's action is not async. - - -## Listening for Events - -Actors can send realtime updates to clients using events: - -### `on(eventName, callback)` - Continuous Listening - -To listen for events that will happen repeatedly: - - -```typescript TypeScript -// Listen for new chat messages -chatRoom.on("newMessage", ({ sender, text, timestamp }) => { - console.log(`${sender}: ${text}`); - updateChatUI(sender, text, timestamp); -}); - -// Listen for game state updates -gameRoom.on("stateUpdate", (gameState) => { - updateGameUI(gameState); -}); -``` - -```rust Rust -use std::sync::Arc; - -// Listen for new chat messages -chat_room.on_event("newMessage", move |args| { - let message = &args[0]; - let sender = message["sender"].as_str().unwrap(); - let text = message["text"].as_str().unwrap(); - println!("{}: {}", sender, text); - // Update UI with message data -}).await; - -// Listen for game state updates -let game_ui = Arc::new(GameUI::new()); -let game_ui_clone = game_ui.clone(); -game_room.on_event("stateUpdate", move |args| { - let game_state = &args[0]; - game_ui_clone.update(game_state); -}).await; -``` - -```python Python -# Listen for new chat messages -def handle_message(message): - sender = message["sender"] - text = message["text"] - print(f"{sender}: {text}") - # Update UI with message data - -chat_room.on_event("newMessage", handle_message) - -# Listen for game state updates -def handle_state_update(game_state): - # Update UI with new game state - update_game_ui(game_state) - -game_room.on_event("stateUpdate", handle_state_update) -``` - - -### `once(eventName, callback)` - One-time Listening - -For events you only need to hear once: - - -```typescript TypeScript -// Listen for when a request is approved -actor.once("requestApproved", () => { - showApprovalNotification(); - unlockFeatures(); -}); -``` - -```rust Rust -// `once` is not implemented in Rust -``` - -```python Python -# Listen for when a request is approved -def handle_approval(): - show_approval_notification() - unlock_features() - -actor.on_event("requestApproved", handle_approval) -``` - - -## Connection Options - -### Authentication and Connection Parameters - -Pass information with your connection using the `params` option: - - -```typescript TypeScript -const chatRoom = await client.chatRoom.get({ channel: "super-secret" }, { - params: { - userId: "1234", - authToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", - displayName: "Alice" - } -}); -``` - -```rust Rust -use serde_json::json; -use rivetkit_client::GetOptions; - -let key = vec![ - "super-secret-channel".to_string(), -]; - -let params = json!({ - "userId": "1234", - "authToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", - "displayName": "Alice" -}); - -let options = GetOptions { - params: Some(params), -}; - -let chat_room = client.get("chatRoom", key, options) - .expect("Failed to get chat room") - .connect(); -``` - -```python Python -chat_room = await client.get( - "chatRoom", - tags=[("channel", "super-secret")], - params={ - "userId": "1234", - "authToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", - "displayName": "Alice" - } -) -``` - - -The actor can access these parameters in the `onBeforeConnect` or `createConnState` hook: - -```typescript -import { actor } from "rivetkit"; - -const chatRoom = actor({ - state: { messages: [] }, - - createConnState: (c, { params }) => { - // Validate authentication token - const { userId, authToken, displayName } = params; - if (!validateToken(userId, authToken)) { - throw new Error("Invalid authentication"); - } - - // Return connection state - return { userId, displayName }; - }, - - actions: { - // ... - } -}); -``` - -Read more about [connection parameters](/concepts/connections). - -### Additional Options - -#### `opts.noCreate` - -Connect only if an actor exists, without creating a new one: - -```typescript -try { - const doc = await client.document.get({ documentId: "doc-123" }, { noCreate: true }); - await doc.doSomething(); -} catch (error) { - console.log("Document doesn't exist"); -} -``` - -## Client Options - - -```typescript TypeScript -// Example with all client options -const client = createClient( - "https://actors.example.com", - { - // Data serialization format - encoding: "cbor", // or "json" - - // Network transports in order of preference - supportedTransports: ["websocket", "sse"] - } -); -``` - -```rust Rust -use rivetkit_client::{Client, EncodingKind, GetOrCreateOptions, TransportKind}; - -// Create client with specific options -let client = Client::new( - "https://actors.example.com", - TransportKind::WebSocket, // or TransportKind::Sse - EncodingKind::Cbor, // or EncodingKind::Json -); - -// Rust does not support accepting multiple transports -``` - -```python Python -from rivetkit_client import AsyncClient as ActorClient - -# Example with all client options -client = ActorClient( - "https://actors.example.com", - "websocket" # or "sse" - "cbor", # or "json" -) - -# Python does not support accepting multiple transports -``` - - -### `encoding` - -`"cbor" | "json"` (optional) - -Specifies the data encoding format used for communication: - -- `"cbor"` (default): Binary format that's more efficient for data transfer -- `"json"`: Text-based format with wider compatibility across environments - -### `supportedTransports` - -`("websocket" | "sse")[]` (optional) - -Configures which network transport mechanisms the client will use to communicate with actors, sorted by priority: - -- `"websocket"`: Real-time bidirectional communication, best for most applications -- `"sse"` (Server-Sent Events): Works in more restricted environments where WebSockets may be blocked - -Default is `["websocket", "sse"]`, which automatically negotiates the best available option. - -## Error Handling - -RivetKit provides specific error types to help you handle different failure scenarios: - -### Action Errors - -When an action fails, it throws an error with details about the failure: - -```typescript -try { - await actor.someAction(); -} catch (error) { - console.error(`Action failed: ${error.code} - ${error.message}`); - // Handle specific error codes - if (error.code === "permission_denied") { - // Handle permission errors - } -} -``` - -These errors can be thrown from within the actor with `UserError`: - -```typescript -import { actor, UserError } from "rivetkit"; - -const documentActor = actor({ - state: { content: "" }, - - actions: { - editDocument: (c, userId, newContent) => { - // Check if user has permission to edit - if (!hasPermission(userId, "edit")) { - throw new UserError("You don't have permission to edit this document", { - code: "permission_denied", - meta: { userId } - }); - } - - c.state.content = newContent; - } - } -}); -``` - -RivetKit doesn't expose internal errors to clients for security, helping to prevent the exposure of sensitive information or internal implementation details. - -### Other Errors - -Other common errors you might encounter: - -- `InternalError`: Error from your actor that's not a subclass of `UserError` -- `ManagerError`: Issues when connecting to or communicating with the actor manager - -## Disconnecting and Cleanup - -The client connection is automatically cleaned up when it goes out of scope. - -If you need to explicitly disconnect: - - -```typescript TypeScript -// Disconnect from the actor -await actor.dispose(); - -// Disconnect the entire client -await client.dispose(); -``` - -```rust Rust -// Disconnect from the actor -actor.disconnect().await; - -// The client will be cleaned up automatically when it goes out of scope -// Or explicitly drop it with: -drop(client); -``` - -```python Python -# Disconnect from the actor -await actor.disconnect() -``` - - -## Offline and Auto-Reconnection - -Clients automatically attempt to reconnect (with [exponential backoff](https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/retry-backoff.html)) when disconnected. Actions made while disconnected are queued. - -On reconnection, event subscriptions are reestablished & queued actions are executed. - -This makes your applications resilient to temporary network failures without any extra code. - -## Next Steps - - - - Learn how state works in actors - - - Learn more about actor events - - - Add security to your actors - - - Manage client connections - - diff --git a/docs/concepts/overview.mdx b/docs/concepts/overview.mdx deleted file mode 100644 index cc6e746a4..000000000 --- a/docs/concepts/overview.mdx +++ /dev/null @@ -1,257 +0,0 @@ ---- -title: Introduction -icon: square-info ---- - -import CreateActorCli from "/snippets/create-actor-cli.mdx"; - -Actors combine compute and storage into unified entities for simplified architecture. Actors seamlessly integrate with your existing infrastructure or can serve as a complete standalone solution. - -## Quickstart - -Run this to get started: - - - -## Code Example - -Here's a complete chat room actor that maintains state and handles messages. We'll explore each component in depth throughout this document: - -```typescript chat_room.ts -import { actor } from "rivetkit"; - -// Define a chat room actor -const chatRoom = actor({ - // Initialize state when the actor is first created - createState: () => ({ - messages: [] - }), - - // Define actions clients can call - actions: { - // Action to send a message - sendMessage: (c, sender, text) => { - // Update state - c.state.messages.push({ sender, text }); - - // Broadcast to all connected clients - c.broadcast("newMessage", { sender, text }); - }, - - // Action to get chat history - getHistory: (c) => { - return c.state.messages; - } - } -}); - -export default chatRoom; -``` - -## Using the App - -To start using your actor, create an app and serve it: - -```typescript app.ts -import { setup, serve } from "rivetkit"; -import chatRoom from "./chat_room"; - -// Create the application -const registry = setup({ - use: { chatRoom } -}); - -// Start serving on default port -serve(registry); -``` - -## Key Actor Components - -### State - -Actors maintain state that's stored in memory and automatically persisted. State is defined either as a constant or via a `createState` function: - -```typescript -import { actor } from "rivetkit"; - -// Method 1: State constant -const counter1 = actor({ - state: { count: 0 }, - actions: { - // ... - } -}); - -// Method 2: CreateState function -const counter2 = actor({ - createState: () => ({ count: 0 }), - actions: { - // ... - } -}); -``` - -Update state by modifying `c.state` in your actions: - -```typescript -import { actor } from "rivetkit"; - -const counter = actor({ - state: { count: 0 }, - actions: { - // Example of state update in an action - increment: (c) => { - c.state.count += 1; - return c.state.count; - } - } -}); -``` - -These changes are durable and are automatically persisted across updates, restarts, and crashes. - -Learn more about [state management](/concepts/state). - -### Actions - -Actions are functions defined in your actor configuration that clients & other actors can call: - -```typescript -import { actor } from "rivetkit"; - -const mathUtils = actor({ - state: {}, - actions: { - multiplyByTwo: (c, x) => { - return x * 2; - } - } -}); -``` - -Each action receives a context object (commonly named `c`) as its first parameter, which provides access to state, connections, and other utilities. - -Learn more about [actions](/concepts/actions). - -### Events - -Actors can broadcast events to connected clients: - -```typescript -import { actor } from "rivetkit"; - -const inventory = actor({ - createState: () => ({ - items: [] - }), - - actions: { - addItem: (c, item) => { - // Add to state - c.state.items.push(item); - - // Notify all clients about the new item - c.broadcast("itemAdded", { item }); - } - } -}); -``` - -You can also send events to specific clients: - -```typescript -import { actor } from "rivetkit"; - -const messageService = actor({ - state: {}, - actions: { - sendPrivateMessage: (c, userId, text) => { - // Send to a specific connection - const conn = c.conns.find(conn => conn.params.userId === userId); - if (conn) { - conn.send("privateMessage", { text }); - } - } - } -}); -``` - -Learn more about [events](/concepts/events). - -## Actor Tags - -Tags are key-value pairs attached to actors that serve two purposes: - -1. **Actor Discovery**: Find specific actors using `client.get(tags)` -2. **Organization**: Group related actors for management purposes - -For example, you can query chat rooms by tag like: - -```typescript client.ts -await client.chatRoom.get({ channel: "random" }); -``` - -### Common Tag Patterns - -```typescript -import { createClient } from "rivetkit/client"; -import type { App } from "./src/index"; - -const client = createClient("http://localhost:8080"); - -// Game room with ID parameter -const gameRoom = await client.gameRoom.get({ roomId: "ABC123" }); - -// User profile with ID -const userProfile = await client.userProfile.get({ profileId: "1234" }); - -// Document with multiple parameters -const document = await client.document.get({ - workspaceId: "team-alpha", - documentId: "budget-2024" -}); -``` - -## Actor Lifecycle - -Actors are created automatically when needed and persist until explicitly shutdown. - -To shut down a actor, use `c.shutdown()` from within an action: - -```typescript -import { actor } from "rivetkit"; - -const chatRoom = actor({ - createState: () => ({ - messages: [] - }), - actions: { - closeRoom: (c) => { - // Do any cleanup needed - c.broadcast("roomClosed"); - - // Shutdown the actor - c.shutdown(); - } - } -}); -``` - -Learn more about the [actor lifecycle](/concepts/lifecycle). - -## Next Steps - - - - Learn how to connect to actors from clients - - - Deep dive into actor state management - - - Learn more about actor actions - - - Learn more about realtime events - - diff --git a/docs/docs.json b/docs/docs.json index 362ac3e6d..7829c3d97 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -41,136 +41,124 @@ "groups": [ { "group": "Getting Started", - "pages": [ - "introduction" - ] + "pages": ["introduction"] }, { "group": "Actors", + "icon": "shapes", "pages": [ -"actors/overview", -{ -"group": "Quickstart", -"icon": "forward", -"pages": [ -"actors/quickstart", -"actors/quickstart-frontend" -] -}, -"actors/state", -"actors/actions", -"actors/events", + "actors/overview", + { + "group": "Quickstart", + "icon": "forward", + "pages": [ + "actors/quickstart-backend", + "actors/quickstart-react" + ] + }, + "actors/state", + "actors/actions", + "actors/events", + "actors/schedule", { "group": "More", "pages": [ -"actors/schedule", - -"actors/lifecycle", -"actors/connections", -"actors/authentication", - { - "group": "Advanced", - "pages": [ -"actors/metadata", -"actors/types" - - ] - } + "actors/communicating-with-actors", + "actors/communicating-between-actors", + "actors/connections", + "actors/lifecycle", + "actors/metadata", + "actors/helper-types", + "actors/external-sql", + "actors/scaling" ] } ] }, - { - "group": "Workflows", - "pages": [ - "workflows/overview" - ] - }, { "group": "Integrations", "pages": [ - "integrations/better-auth", { - "group": "Frameworks", + "group": "Frontend & Clients", + "icon": "code", "pages": [ - "frameworks/react", "clients/javascript", - "clients/rust", - "clients/python" + "clients/react", + "clients/rust" ] }, { - "group": "Deploy", + "group": "Frontend", + "pages": [] + }, + { + "group": "Backend", + "icon": "server", "pages": [ - "platforms/rivet", - "platforms/cloudflare-workers", - "platforms/bun", - "platforms/nodejs" + "integrations/hono", + "integrations/express", + "integrations/elysia", + "integrations/trpc" ] }, { - "group": "Libraries & Services", + "group": "Auth", + "icon": "lock", "pages": [ - "integrations/hono", - "integrations/resend" + "integrations/better-auth" ] }, { - "group": "Drivers", + "group": "Misc", "pages": [ - "drivers/overview", - "drivers/file-system", - "drivers/memory", - "drivers/redis", - "drivers/rivet", - "drivers/cloudflare-workers", - "drivers/build" + "integrations/vitest" ] } ] }, + { + "group": "Reference", + "pages": [ + "general/authentication", + "general/testing", { - "group": "Reference", + "group": "More", "pages": [ - { - "group": "Common Concepts", - "pages": [ - "concepts/cors", - "concepts/external-sql", - "concepts/interacting-with-actors", - "concepts/logging", - "concepts/overview", - "concepts/scaling", - "concepts/testing", - "concepts/edge", - "concepts/topology" - ] - }, - { - "group": "LLM Prompts", - "pages": [ - "llm/cursor", - "llm/windsurf", - "llm/claude", - "llm/docs-as-markdown", - "llm/prompt", - "llm/llms", - "llm/llms-full" - ] - }, + "general/edge", + "general/cors", + "general/logging" + ] + }, { - "group": "API", + "group": "HTTP API", "openapi": { "source": "./openapi.json", - "directory": "api" + "directory": "reference/api" } - }, - "support/enterprise" - ] } + ] + } ] }, - "contextual": { + "_temp": [ + "general/architecture", + "general/registry", + "general/webhooks", + { + "group": "Drivers", + "icon": "database", + "pages": [ + "drivers/overview", + "drivers/rivet", + "drivers/cloudflare-workers", + "drivers/file-system", + "drivers/memory", + "drivers/redis", + "drivers/building-drivers" + ] + } + ], + "contextual": { "options": [ "copy", "view", diff --git a/docs/drivers/build.mdx b/docs/drivers/building-drivers.mdx similarity index 100% rename from docs/drivers/build.mdx rename to docs/drivers/building-drivers.mdx diff --git a/docs/drivers/cloudflare-workers.mdx b/docs/drivers/cloudflare-workers.mdx index c1e522fd8..e69de29bb 100644 --- a/docs/drivers/cloudflare-workers.mdx +++ b/docs/drivers/cloudflare-workers.mdx @@ -1,37 +0,0 @@ ---- -title: Cloudflare Workers (Durable Objects) -sidebarTitle: Durable Objects ---- - -import DriverNote from '/snippets/driver-note.mdx'; - -The Cloudflare Workers Driver is an implementation that uses Cloudflare's Durable Objects for actor state persistence and coordination. It leverages Cloudflare's global network for low-latency, distributed actor systems that can scale automatically. - - - -## Compatibility - -| Platforms | Topologies | -| --------- | ---------- | -| Node.js | Standalone | -| Bun | Coordinate | -| Cloudflare Workers | Partition | -| Rivet | | - -## Usage - -There's no need to explicitly configure drivers when using RivetKit with Cloudflare Workers. The platform package automatically sets up the appropriate drivers. - -See the [Cloudflare Workers Platform](/platforms/cloudflare-workers) documentation for complete setup instructions. - -## Limitations - -The Cloudflare Workers driver has several limitations to be aware of: - -- **Storage Limits**: Storage is limited to Cloudflare's Durable Objects size limits -- **Cost**: Requires Cloudflare Workers Paid plan for Durable Objects usage -- **Platform Lock-in**: Your implementation will be specific to the Cloudflare Workers platform - - - For a platform-agnostic approach with similar global distribution capabilities, consider using [Rivet](/platforms/rivet) instead. - diff --git a/docs/drivers/file-system.mdx b/docs/drivers/file-system.mdx index cd2143994..e69de29bb 100644 --- a/docs/drivers/file-system.mdx +++ b/docs/drivers/file-system.mdx @@ -1,121 +0,0 @@ ---- -title: File System ---- - -import DriverNote from '/snippets/driver-note.mdx'; - -The File System Driver is a simple file-based implementation designed for development and testing environments. It stores all actor state in local files, providing persistence between application restarts. - - - - - The File System Driver is primarily intended for development and testing. For production environments, consider more robust options like Rivet or Cloudflare Workers. - - -## Compatibility - -| Platforms | Topologies | -| --------- | ---------- | -| Node.js | Standalone | -| Bun | Partition | -| Cloudflare Workers | Coordinate | -| Rivet | | - -## Installation - - - - Install the required packages: - - - - ```bash - npm install rivetkit/file-system rivetkit/nodejs - ``` - - - ```bash - yarn add rivetkit/file-system rivetkit/nodejs - ``` - - - ```bash - pnpm add rivetkit/file-system rivetkit/nodejs - ``` - - - ```bash - bun add rivetkit/file-system rivetkit/nodejs - ``` - - - - - - Create a simple server using the File System driver: - - ```typescript src/index.ts - import { serve } from "@rivetkit/nodejs" - import { FileSystemManagerDriver, FileSystemActorDriver, FileSystemGlobalState } from "@rivetkit/file-system"; - - const fsState = new FileSystemGlobalState(); - serve(app, { - topology: "standalone", - drivers: { - manager: new FileSystemManagerDriver(app, fsState), - actor: new FileSystemActorDriver(fsState), - }, - }); - ``` - - - - Start your server: - - - - ```bash - npm run dev - ``` - - - ```bash - yarn dev - ``` - - - ```bash - pnpm dev - ``` - - - ```bash - bun dev - ``` - - - - - -## Features - -The File System driver provides several benefits for development: - -- **Persistence**: Actor state is stored in files and persists between application restarts -- **Durability**: Data is written to disk, providing protection against process crashes -- **Visibility**: State files can be inspected for debugging purposes -- **No External Dependencies**: Doesn't require additional services like Redis - -## Limitations - -The File System driver has several limitations to be aware of: - -- **Single Machine**: Only works within a single machine - not suitable for distributed environments -- **Scalability**: Limited scalability beyond a single instance -- **Coordination**: Limited support for coordinated topology -- **Performance**: File I/O operations may be slower than in-memory alternatives - - - For production environments or applications requiring distributed capabilities, consider using the [Rivet](/platforms/rivet) or [Cloudflare Workers](/platforms/cloudflare-workers) instead. - - diff --git a/docs/drivers/memory.mdx b/docs/drivers/memory.mdx index 7d15b7a80..e69de29bb 100644 --- a/docs/drivers/memory.mdx +++ b/docs/drivers/memory.mdx @@ -1,112 +0,0 @@ ---- -title: Memory ---- - -import DriverNote from '/snippets/driver-note.mdx'; - -The Memory Driver is a simple in-memory implementation designed for development and testing environments. It stores all actor state in memory, which means data is not persisted between application restarts. - - - - - The Memory Driver is not recommended for production environments as it doesn't provide persistence or distributed capabilities. - - -## Compatibility - -| Platforms | Topologies | -| --------- | ---------- | -| Node.js | Standalone | -| Bun | Partition | -| Cloudflare Workers | Coordinate | -| Rivet | | - -## Installation - - - - Install the required packages: - - - - ```bash - npm install rivetkit/memory rivetkit/nodejs - ``` - - - ```bash - yarn add rivetkit/memory rivetkit/nodejs - ``` - - - ```bash - pnpm add rivetkit/memory rivetkit/nodejs - ``` - - - ```bash - bun add rivetkit/memory rivetkit/nodejs - ``` - - - - - - Create a simple server using the Memory driver: - - ```typescript src/index.ts - import { serve } from "@rivetkit/nodejs" - import { MemoryManagerDriver, MemoryActorDriver, MemoryGlobalState } from "@rivetkit/memory"; - - const memoryState = new MemoryGlobalState(); - serve(app, { - topology: "standalone", - drivers: { - manager: new MemoryManagerDriver(app, memoryState), - actor: new MemoryActorDriver(memoryState), - }, - }); - ``` - - - - Start your server: - - - - ```bash - npm run dev - ``` - - - ```bash - yarn dev - ``` - - - ```bash - pnpm dev - ``` - - - ```bash - bun dev - ``` - - - - - -## Limitations - -The Memory driver has several limitations to be aware of: - -- **No Persistence**: All data is stored in memory and lost when the application restarts -- **Single Process**: Only works within a single process - not suitable for distributed environments -- **Scalability**: Cannot scale beyond a single instance -- **Coordination**: Limited support for coordinated topology, as actors can only communicate within the same process - - - For production environments or applications requiring persistence and distributed capabilities, consider using the [Rivet](/platforms/rivet) or [Cloudflare Workers](/platforms/cloudflare-workers) instead. - - diff --git a/docs/drivers/overview.mdx b/docs/drivers/overview.mdx index 622da8e42..a9f572fd1 100644 --- a/docs/drivers/overview.mdx +++ b/docs/drivers/overview.mdx @@ -1,37 +1 @@ ---- -title: Drivers -sidebarTitle: Overview ---- - -import DriverNote from '/snippets/driver-note.mdx'; - -Drivers in RivetKit the infrastructure layer between your actor code and the underlying systems. - - - -## Accessing Driver Context - -Drivers expose a custom driver context for functionality specific to the given driver. This can be accessed as a parameter to `createVars` then later accessed with `c.vars`. Read more about the [`createVars` lifecycle hook](/concepts/lifecycle). - -## Available Drivers - -Choose a driver based on your deployment needs - [Memory](/drivers/memory) for development, [Rivet](/drivers/rivet) or [Cloudflare Workers](/drivers/cloudflare-workers) for production environments, and [Redis](/drivers/redis) for specialized self-hosted scenarios. - - - - Local file system driver for development and testing with persistent storage. Stores actor state in files for durability between restarts. - - - In-memory driver for development and testing. Simple and lightweight with no external dependencies. - - - Self-hosted option for custom deployments where you need fine-grained control over your infrastructure. Suitable for specialized deployment scenarios. - - - Fully-managed cloud platform with built-in scaling, deployment, and monitoring. Recommended for production deployments. - - - Edge computing platform for global distribution and low latency. Recommended for production deployments. - - - +TODO: Comparison diff --git a/docs/drivers/redis.mdx b/docs/drivers/redis.mdx index 9d191d95b..e69de29bb 100644 --- a/docs/drivers/redis.mdx +++ b/docs/drivers/redis.mdx @@ -1,160 +0,0 @@ ---- -title: Redis ---- - -import DriverNote from '/snippets/driver-note.mdx'; - -The Redis Driver is a production-ready implementation that uses Redis for actor state persistence, coordination, and communication. It supports distributed actor systems and enables horizontal scaling across multiple instances. - - - - - RivetKit requires AOF (Append Only File) persistence to be enabled on your Redis server. See the [Redis Persistence Documentation](https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/#append-only-file) for setup instructions. - - -## Compatibility - -| Platforms | Topologies | -| --------- | ---------- | -| Node.js | Standalone | -| Bun | Coordinate | -| Cloudflare Workers | Partition | -| Rivet | | - -## Installation - - - - Install the required packages: - - - - ```bash - npm install rivetkit/redis rivetkit/nodejs ioredis - ``` - - - ```bash - yarn add rivetkit/redis rivetkit/nodejs ioredis - ``` - - - ```bash - pnpm add rivetkit/redis rivetkit/nodejs ioredis - ``` - - - ```bash - bun add rivetkit/redis rivetkit/nodejs ioredis - ``` - - - - - - Create a Redis connection and set up your server: - - ```typescript src/index.ts - import { serve } from "@rivetkit/nodejs" - import { RedisManagerDriver } from "@rivetkit/redis/manager"; - import { RedisActorDriver } from "@rivetkit/redis/actor"; - import { RedisCoordinateDriver } from "@rivetkit/redis/coordinate"; - import Redis from "ioredis"; - - // Create a Redis connection - const redis = new Redis(); - - serve(app, { - topology: "coordinate", // Can be "standalone" or "coordinate" - drivers: { - manager: new RedisManagerDriver(redis), - actor: new RedisActorDriver(redis), - coordinate: new RedisCoordinateDriver(redis), - }, - }); - ``` - - - - Start your server: - - - - ```bash - npm run dev - ``` - - - ```bash - yarn dev - ``` - - - ```bash - pnpm dev - ``` - - - ```bash - bun dev - ``` - - - - - -## Redis Configuration - -The Redis driver requires an [ioredis](https://github.com/redis/ioredis) connection instance when creating the drivers. Custom configuration parameters can be passed like: - -```typescript -import Redis from "ioredis"; -import { RedisManagerDriver } from "@rivetkit/redis/manager"; -import { RedisActorDriver } from "@rivetkit/redis/actor"; -import { RedisCoordinateDriver } from "@rivetkit/redis/coordinate"; - -// Create a Redis connection -const redis = new Redis({ - host: "localhost", - port: 6379, - password: "foobar", -}); - -// Create the Redis drivers -const managerDriver = new RedisManagerDriver(redis); -const actorDriver = new RedisActorDriver(redis); -const coordinateDriver = new RedisCoordinateDriver(redis); -``` - - - See the [ioredis documentation](https://github.com/redis/ioredis#connect-to-redis) for more connection configuration options. - - -## Hosted Redis Providers - -For production deployments, consider using these managed Redis providers: - -- [Amazon ElastiCache](https://aws.amazon.com/elasticache/) - AWS managed Redis service -- [Azure Cache for Redis](https://azure.microsoft.com/en-us/products/cache) - Microsoft Azure managed Redis service -- [Google Cloud Memorystore](https://cloud.google.com/memorystore) - Google Cloud managed Redis service -- [Upstash](https://upstash.com/) - Serverless Redis with pay-per-use pricing -- [Redis Cloud](https://redis.com/redis-enterprise-cloud/overview/) - Official Redis offering with enterprise features -- [Dragonfly](https://www.dragonflydb.io/) - Redis-compatible database with higher performance - - - For local development where persistence isn't required, the [Memory Driver](/drivers/memory) offers a simpler setup with no external dependencies. - - -## Limitations - -The Redis driver has several limitations to be aware of: - -- **Very Limited Storage**: Storage is limited to the available memory of your Redis server -- **Single Region Support**: Only supports deployment within a single region, not globally distributed -- **Performance Bottleneck**: All operations go through Redis, which can become a bottleneck under high load -- **Single Point of Failure**: Redis becomes a single point of failure if not configured with proper failover mechanisms - - - For multi-region support, built-in redundancy, and unlimited storage capacity, consider using [Rivet](/platforms/rivet) or [Cloudflare Workers](/platforms/cloudflare-workers) instead. - - diff --git a/docs/drivers/rivet.mdx b/docs/drivers/rivet.mdx index 84542ff77..e69de29bb 100644 --- a/docs/drivers/rivet.mdx +++ b/docs/drivers/rivet.mdx @@ -1,38 +0,0 @@ ---- -title: Rivet ---- - -import DriverNote from '/snippets/driver-note.mdx'; - -The Rivet Driver is a production-ready implementation that uses Rivet's managed infrastructure for actor state persistence, coordination, and communication. It provides a fully managed environment for RivetKit with built-in scaling, monitoring, and global distribution. - - - -## Compatibility - -| Platforms | Topologies | -| --------- | ---------- | -| Node.js | Standalone | -| Bun | Coordinate | -| Cloudflare Workers | Partition | -| Rivet | | - -## Usage - -There's no need to explicitly configure drivers when using RivetKit with Rivet. The platform package automatically sets up the appropriate drivers based on your project configuration. - -See the [Rivet Platform](/platforms/rivet) documentation for complete setup instructions. - -## Benefits - -The Rivet driver offers several advantages: - -- **Fully Managed**: No infrastructure to provision or maintain -- **Global Distribution**: Deploy actors globally with automatic region selection -- **Monitoring**: Built-in observability and performance metrics -- **Cost Efficiency**: Pay only for what you use with no upfront costs -- **Partition Topology Support**: Optimized for the Partition topology for efficient scaling - - - Rivet is a great choice for production deployments, offering enterprise-grade reliability without the operational overhead. - diff --git a/docs/frameworks/react.mdx b/docs/frameworks/react.mdx deleted file mode 100644 index df00004a6..000000000 --- a/docs/frameworks/react.mdx +++ /dev/null @@ -1,243 +0,0 @@ ---- -title: React -icon: react ---- - -import MvpWarning from "/snippets/mvp-warning.mdx"; -import StepDefineActor from "/snippets/step-define-actor.mdx"; -import StepRunStudio from "/snippets/step-run-studio.mdx"; -import StepDeploy from "/snippets/step-deploy.mdx"; -import SetupNextSteps from "/snippets/setup-next-steps.mdx"; - -Learn how to create realtime, stateful React applications with RivetKit's actor model. - - - -## Quickstart - - - - Create a new React project with TypeScript support: - - - ```sh npm - npm create vite@latest my-app -- --template react-ts - ``` - - ```sh pnpm - pnpm create vite@latest my-app --template react-ts - ``` - - ```sh yarn - yarn create vite my-app --template react-ts - ``` - - ```sh bun - bunx create-vite@latest my-app --template react-ts - ``` - - - - - Navigate to your React project and install the RivetKit client and React packages: - - - ```sh npm - cd my-app - npm install rivetkit rivetkit/react - ``` - - ```sh pnpm - cd my-app - pnpm add rivetkit rivetkit/react - ``` - - ```sh yarn - cd my-app - yarn add rivetkit rivetkit/react - ``` - - ```sh bun - cd my-app - bun add rivetkit rivetkit/react - ``` - - - - - - - Now modify your `src/App.tsx` file to connect to your RivetKit backend: - - ```tsx src/App.tsx - import { createClient } from "rivetkit/client"; - import { createReactRivetKit } from "@rivetkit/react"; - import type { App } from "../actors/app"; - import React, { useState } from "react"; - - // Replace with your endpoint URL after deployment - const client = createClient("http://localhost:8080"); - const { useActor, useActorEvent } = createReactRivetKit(client); - - function App() { - // Connect to counter actor - const [{ actor }] = useActor("counter"); - const [count, setCount] = useState(0); - - // Listen to count updates - useActorEvent({ actor, event: "newCount" }, (newCount) => { - setCount(newCount); - }); - - return ( -
-

Count: {count}

- -
- ); - } - - // For Vite + React 18 - import { createRoot } from 'react-dom/client'; - const root = createRoot(document.getElementById('root')!); - root.render(); - ``` -
- - - - - In a separate terminal, start your React app: - - - ```sh npm - cd my-app - npm run dev - ``` - - ```sh pnpm - cd my-app - pnpm run dev - ``` - - ```sh yarn - cd my-app - yarn dev - ``` - - ```sh bun - cd my-app - bun run dev - ``` - - - Your React app should now be running and connected to your RivetKit backend. Open your browser to the URL shown in the terminal (typically http://localhost:5173) to see your application. - - -
- -## API Reference - -The React integration leverages React's hooks system to provide an idiomatic way to interact with RivetKit in React applications. - -### `createReactRivetKit` - -The main function that creates React hooks for interacting with RivetKit. It takes a client instance and returns hook functions. - -```tsx -const { useActor, useActorEvent } = createReactRivetKit(client); -``` - -#### Parameters - -- `client`: The RivetKit client created with `createClient`. - -#### Returns - -An object containing React hooks: -- `useActor`: Hook for connecting to actors -- `useActorEvent`: Hook for subscribing to actor events - -### `useActor` - -Hook that connects to a actor, creating it if necessary. It manages the actor connection and returns the actor handle. - -```tsx -const [{ actor, error, isLoading, state }] = useActor(actorName, options); -``` - -#### Parameters - -- `actorName`: The name of the actor to connect to (string). -- `options`: Optional connection options (same options as `client.actorName.get()`). - - `id`: String identifier for the actor instance. - - `tags`: Key-value pairs for actor identification. - - `params`: Parameters to pass during connection. - - `noCreate`: Boolean to prevent actor creation if it doesn't exist. - -#### Returns - -Returns an array with a single object containing: -- `actor`: The actor handle if connected, or `undefined` if still connecting. -- `error`: Any error that occurred during connection. -- `isLoading`: Boolean indicating if the connection is in progress. -- `state`: String representing the internal connection state ("init", "creating", "created", or "error"). - -### `useActorEvent` - -Hook that subscribes to events from a actor. - -```tsx -useActorEvent({ actor, event }, cb); -``` - -#### Parameters - -- `opts`: Object containing: - - `actor`: The actor handle from `useActor`, or undefined. - - `event`: The name of the event to subscribe to. -- `cb`: Function called when the event is fired. The arguments passed to this function depend on the event type. - -#### Returns - -This hook doesn't return a value. The subscription is automatically managed by the hook lifecycle. - -## Example Usage - -### Simple Counter - -```tsx -import { createClient } from "rivetkit/client"; -import { createReactRivetKit } from "@rivetkit/react"; -import type { App } from "../actors/app"; -import { useState } from "react"; - -// Connect to RivetKit -const client = createClient("http://localhost:8080"); -const { useActor, useActorEvent } = createReactRivetKit(client); - -function Counter() { - // Get actor and track count - const [{ actor }] = useActor("counter"); - const [count, setCount] = useState(0); - - // Listen for count updates - useActorEvent({ actor, event: "newCount" }, setCount); - - return ( -
-

Count: {count}

- -
- ); -} -``` - - diff --git a/docs/concepts/topology.mdx b/docs/general/architecture.mdx similarity index 97% rename from docs/concepts/topology.mdx rename to docs/general/architecture.mdx index 95ce856b7..7210e41b4 100644 --- a/docs/concepts/topology.mdx +++ b/docs/general/architecture.mdx @@ -1,7 +1,4 @@ ---- -title: Topologies -icon: list-tree ---- +TODO: Architecture RivetKit supports three topologies that define how actors are distributed and scale. diff --git a/docs/general/authentication.mdx b/docs/general/authentication.mdx new file mode 100644 index 000000000..b65738e71 --- /dev/null +++ b/docs/general/authentication.mdx @@ -0,0 +1,397 @@ +--- +title: Authentication +description: Secure your actors with authentication and authorization +icon: fingerprint +--- + +RivetKit provides multiple authentication methods to secure your actors. Use `onAuth` for server-side validation or `onBeforeConnect` for actor-level authentication. + +## Authentication Methods + +### onAuth Hook (Recommended) + +The `onAuth` hook runs on the HTTP server before clients can access actors. This is the preferred method for most authentication scenarios. + +```typescript +import { actor, UserError } from "@rivetkit/actor"; + +const chatRoom = actor({ + onAuth: async (opts) => { + const { req, params, intents } = opts; + + // Extract token from params or headers + const token = params.authToken || req.headers.get("Authorization"); + + if (!token) { + throw new UserError("Authentication required"); + } + + // Validate token and return user data + const user = await validateJWT(token); + return { + userId: user.id, + role: user.role, + permissions: user.permissions + }; + }, + + state: { messages: [] }, + + actions: { + sendMessage: (c, text: string) => { + // Access auth data via c.conn.auth + const { userId, role } = c.conn.auth; + + if (role !== "member") { + throw new UserError("Insufficient permissions"); + } + + const message = { + id: crypto.randomUUID(), + userId, + text, + timestamp: Date.now(), + }; + + c.state.messages.push(message); + c.broadcast("newMessage", message); + return message; + } + } +}); +``` + +### `onBeforeConnect` Hook + +Use `onBeforeConnect` when you need access to actor state for authentication: + +```typescript +const userProfileActor = actor({ + // Empty onAuth allows all requests to reach the actor + onAuth: () => ({}), + + state: { + ownerId: null as string | null, + isPrivate: false + }, + + onBeforeConnect: async (c, opts) => { + const { params } = opts; + const userId = await validateUser(params.token); + + // Check if user can access this profile + if (c.state.isPrivate && c.state.ownerId !== userId) { + throw new UserError("Access denied to private profile"); + } + }, + + createConnState: (c, opts) => { + return { userId: opts.params.userId }; + }, + + actions: { + updateProfile: (c, data) => { + // Check ownership + if (c.state.ownerId !== c.conn.state.userId) { + throw new UserError("Only owner can update profile"); + } + + // Update profile... + } + } +}); +``` + + +Prefer `onAuth` over `onBeforeConnect` when possible, as `onAuth` runs on the HTTP server and uses fewer actor resources. + + +## Connection Parameters + +Pass authentication data when connecting: + +```typescript +// Client side +const chat = client.chatRoom.getOrCreate(["general"]); +const connection = chat.connect({ + authToken: "jwt-token-here", + userId: "user-123" +}); + +// Or with action calls +const counter = client.counter.getOrCreate(["user-counter"], { + authToken: "jwt-token-here" +}); +``` + +## Intent-Based Authentication (Experimental) + +The `onAuth` hook receives an `intents` parameter indicating what the client wants to do: + +```typescript +const secureActor = actor({ + onAuth: async (opts) => { + const { intents, params } = opts; + + // Different validation based on intent + if (intents.has("action")) { + // Requires higher privileges for actions + return await validateAdminToken(params.token); + } else if (intents.has("connect")) { + // Lower privileges for connections/events + return await validateUserToken(params.token); + } + + throw new UserError("Unknown intent"); + }, + + actions: { + adminAction: (c) => { + // Only accessible with admin token + return "Admin action performed"; + } + } +}); +``` + +## Error Handling + +### Authentication Errors + +Use specific error types for different authentication failures: + +```typescript +import { UserError, Unauthorized, Forbidden } from "@rivetkit/actor/errors"; + +const protectedActor = actor({ + onAuth: async (opts) => { + const token = opts.params.authToken; + + if (!token) { + throw new Unauthorized("Authentication token required"); + } + + try { + const user = await validateToken(token); + return user; + } catch (error) { + if (error.name === "TokenExpired") { + throw new Unauthorized("Token has expired"); + } + throw new Unauthorized("Invalid authentication token"); + } + }, + + actions: { + adminOnly: (c) => { + if (c.conn.auth.role !== "admin") { + throw new Forbidden("Admin access required"); + } + return "Admin content"; + } + } +}); +``` + +### Client Error Handling + +Handle authentication errors on the client: + +```typescript +try { + const result = await protectedActor.adminOnly(); +} catch (error) { + if (error.code === "UNAUTHORIZED") { + // Redirect to login + window.location.href = "/login"; + } else if (error.code === "FORBIDDEN") { + // Show permission denied message + showError("You don't have permission for this action"); + } +} +``` + +## Integration with Auth Providers + +### Better Auth Integration + + + + Complete integration guide for Better Auth + + + +### JWT Authentication + +```typescript +import { actor, UserError } from "@rivetkit/actor"; +import jwt from "jsonwebtoken"; + +const jwtActor = actor({ + onAuth: async (opts) => { + const token = opts.params.jwt || + opts.req.headers.get("Authorization")?.replace("Bearer ", ""); + + if (!token) { + throw new UserError("JWT token required"); + } + + try { + const payload = jwt.verify(token, process.env.JWT_SECRET!); + return { + userId: payload.sub, + role: payload.role, + permissions: payload.permissions || [] + }; + } catch (error) { + throw new UserError("Invalid or expired JWT token"); + } + }, + + actions: { + protectedAction: (c, data) => { + const { permissions } = c.conn.auth; + + if (!permissions.includes("write")) { + throw new UserError("Write permission required"); + } + + // Perform action... + return { success: true }; + } + } +}); +``` + +### API Key Authentication + +```typescript +const apiActor = actor({ + onAuth: async (opts) => { + const apiKey = opts.params.apiKey || + opts.req.headers.get("X-API-Key"); + + if (!apiKey) { + throw new UserError("API key required"); + } + + // Validate with your API service + const response = await fetch(`${process.env.AUTH_SERVICE}/validate`, { + method: "POST", + headers: { "X-API-Key": apiKey } + }); + + if (!response.ok) { + throw new UserError("Invalid API key"); + } + + const user = await response.json(); + return { + userId: user.id, + tier: user.tier, + rateLimit: user.rateLimit + }; + }, + + actions: { + premiumAction: (c) => { + if (c.conn.auth.tier !== "premium") { + throw new UserError("Premium subscription required"); + } + + return "Premium content"; + } + } +}); +``` + +## Role-Based Access Control + +Implement RBAC with helper functions: + +```typescript +// auth-helpers.ts +export function requireRole(requiredRole: string) { + return (c: any) => { + const userRole = c.conn.auth.role; + const roleHierarchy = { "user": 1, "moderator": 2, "admin": 3 }; + + if (roleHierarchy[userRole] < roleHierarchy[requiredRole]) { + throw new UserError(`${requiredRole} role required`); + } + }; +} + +export function requirePermission(permission: string) { + return (c: any) => { + const permissions = c.conn.auth.permissions || []; + if (!permissions.includes(permission)) { + throw new UserError(`Permission '${permission}' required`); + } + }; +} + +// usage in actor +const forumActor = actor({ + onAuth: async (opts) => { + // ... authenticate and return user with role/permissions + }, + + actions: { + deletePost: (c, postId: string) => { + requireRole("moderator")(c); + // Delete post logic... + }, + + editPost: (c, postId: string, content: string) => { + requirePermission("edit_posts")(c); + // Edit post logic... + } + } +}); +``` + +## Testing Authentication + +Mock authentication for testing: + +```typescript +// test helpers +export function createMockAuth(userData: any) { + return { + onAuth: async () => userData + }; +} + +// in tests +describe("Protected Actor", () => { + it("allows admin actions", async () => { + const mockActor = { + ...protectedActor, + ...createMockAuth({ role: "admin", userId: "123" }) + }; + + const result = await mockActor.adminOnly(); + expect(result).toBe("Admin content"); + }); + + it("denies non-admin actions", async () => { + const mockActor = { + ...protectedActor, + ...createMockAuth({ role: "user", userId: "123" }) + }; + + await expect(mockActor.adminOnly()).rejects.toThrow("Admin access required"); + }); +}); +``` + +## Best Practices + +1. **Use onAuth**: Prefer `onAuth` over `onBeforeConnect` for most authentication +2. **Validate Early**: Authenticate at the HTTP server level when possible +3. **Specific Errors**: Use appropriate error types (Unauthorized, Forbidden) +4. **Rate Limiting**: Consider rate limiting in your authentication logic +5. **Token Refresh**: Handle token expiration gracefully on the client +6. **Audit Logging**: Log authentication events for security monitoring +7. **Least Privilege**: Only grant the minimum permissions needed diff --git a/docs/general/cors.mdx b/docs/general/cors.mdx new file mode 100644 index 000000000..ee3899e30 --- /dev/null +++ b/docs/general/cors.mdx @@ -0,0 +1,302 @@ +--- +title: Cross-Origin Resource Sharing +sidebarTitle: CORS +icon: share-nodes +--- + +Cross-Origin Resource Sharing (CORS) is a security mechanism that allows a web application running at one origin to access resources from a different origin. Without CORS, browsers block cross-origin HTTP requests by default as a security measure. + +You'll need to configure CORS when: + +- **Local Development**: You're developing locally and your client runs on a different port than your actor service +- **Different Domain**: Your frontend application is hosted on a different domain than your actor service + +## Registry-Level CORS + +Configure CORS directly in your registry setup for simple cases: + +```typescript +import { setup } from "@rivetkit/actor"; +import counter from "./counter"; + +const registry = setup({ + use: { counter }, + cors: { + origin: "https://yourdomain.com", + credentials: true + } +}); +``` + +This approach works well for basic setups but has limitations for complex scenarios. + +## Router-Level CORS (Recommended) + +For production applications, configure CORS at the router level for maximum control: + +```typescript +import { registry } from "./registry"; +import { Hono } from "hono"; +import { cors } from "hono/cors"; +import { ALLOWED_PUBLIC_HEADERS } from "@rivetkit/actor"; + +const { serve } = registry.createServer(); +const app = new Hono(); + +app.use("*", cors({ + origin: ["http://localhost:3000", "https://myapp.com"], + allowHeaders: [ + "Authorization", + "Content-Type", + ...ALLOWED_PUBLIC_HEADERS + ], + allowMethods: ["POST", "GET", "OPTIONS"], + exposeHeaders: ["Content-Length"], + maxAge: 600, + credentials: true, +})); + +serve(app); +``` + +### Required Headers for RivetKit + +RivetKit requires specific headers for communication. Always include `ALLOWED_PUBLIC_HEADERS`: + +```typescript +import { ALLOWED_PUBLIC_HEADERS } from "@rivetkit/actor"; + +// ALLOWED_PUBLIC_HEADERS includes: +// - "Content-Type" +// - "User-Agent" +// - "X-RivetKit-Query" +// - "X-RivetKit-Encoding" +// - "X-RivetKit-Conn-Params" +// - "X-RivetKit-Actor" +// - "X-RivetKit-Conn" +// - "X-RivetKit-Conn-Token" + +const corsConfig = { + allowHeaders: [ + "Authorization", // For your auth tokens + ...ALLOWED_PUBLIC_HEADERS // Required RivetKit headers + ] +}; +``` + + +Without `ALLOWED_PUBLIC_HEADERS`, RivetKit clients won't be able to communicate with your actors from the browser. + + +## Framework-Specific Examples + +### Express.js + +```typescript +import express from "express"; +import cors from "cors"; +import { ALLOWED_PUBLIC_HEADERS } from "@rivetkit/actor"; +import { registry } from "./registry"; + +const { handler } = registry.createServer(); +const app = express(); + +app.use(cors({ + origin: ["http://localhost:3000", "https://myapp.com"], + allowedHeaders: [ + "Authorization", + "Content-Type", + ...ALLOWED_PUBLIC_HEADERS + ], + credentials: true, +})); + +app.use("/registry", handler); +app.listen(8080); +``` + +### Elysia + +```typescript +import { Elysia } from "elysia"; +import { cors } from "@elysiajs/cors"; +import { ALLOWED_PUBLIC_HEADERS } from "@rivetkit/actor"; +import { registry } from "./registry"; + +const { handler } = registry.createServer(); + +const app = new Elysia() + .use(cors({ + origin: ["http://localhost:3000", "https://myapp.com"], + allowedHeaders: [ + "Authorization", + "Content-Type", + ...ALLOWED_PUBLIC_HEADERS + ], + credentials: true, + })) + .mount("/registry", handler) + .listen(8080); +``` + +## Configuration Options + +### `origin` + +`string | string[] | (origin: string) => boolean | string` + +Specifies which domains can access your resources: + +```typescript +// Single domain +origin: "https://example.com" + +// Multiple domains +origin: ["https://app.com", "https://admin.com"] + +// Dynamic validation +origin: (origin) => { + return origin?.endsWith('.example.com') ? origin : false; +} + +// All domains (not recommended for production) +origin: "*" +``` + +### `allowMethods` + +`string[]` + +HTTP methods clients are allowed to use: + +```typescript +allowMethods: ["GET", "POST", "OPTIONS"] // Common for RivetKit +``` + +### `allowHeaders` + +`string[]` + +Headers that clients can send in requests: + +```typescript +allowHeaders: [ + "Authorization", // Your auth headers + "Content-Type", // Standard content type + "X-API-Key", // Custom API key header + ...ALLOWED_PUBLIC_HEADERS // Required RivetKit headers +] +``` + +### `credentials` + +`boolean` + +Whether to allow credentials (cookies, auth headers): + +```typescript +credentials: true // Required for authentication +``` + + +When `credentials: true`, you cannot use `origin: "*"`. Specify exact origins instead. + + +### `maxAge` + +`number` + +How long browsers cache CORS preflight responses (in seconds): + +```typescript +maxAge: 600 // Cache for 10 minutes +``` + +### `exposeHeaders` + +`string[]` + +Server headers that browsers can access: + +```typescript +exposeHeaders: ["Content-Length", "X-Request-Id"] +``` + +## Development vs Production + +### Development Setup + +For local development, allow localhost origins: + +```typescript +const isDev = process.env.NODE_ENV !== "production"; + +const corsConfig = { + origin: isDev + ? ["http://localhost:3000", "http://localhost:5173"] + : ["https://myapp.com"], + allowHeaders: ["Authorization", ...ALLOWED_PUBLIC_HEADERS], + credentials: true, +}; +``` + +### Production Setup + +For production, be restrictive with origins: + +```typescript +const corsConfig = { + origin: [ + "https://myapp.com", + "https://www.myapp.com", + "https://admin.myapp.com" + ], + allowHeaders: ["Authorization", ...ALLOWED_PUBLIC_HEADERS], + credentials: true, + maxAge: 3600, // Cache for 1 hour +}; +``` + +## Troubleshooting + +### Common CORS Errors + +**"Access to fetch blocked by CORS policy"** +- Add your frontend's origin to the `origin` list +- Ensure `ALLOWED_PUBLIC_HEADERS` are included in `allowHeaders` + +**"Request header not allowed"** +- Add the missing header to `allowHeaders` +- Include `ALLOWED_PUBLIC_HEADERS` in your configuration + +**"Credentials mode mismatch"** +- Set `credentials: true` in CORS config +- Cannot use `origin: "*"` with credentials + +### Debug CORS Issues + +Enable CORS logging to debug issues: + +```typescript +// Log CORS requests in development +if (process.env.NODE_ENV === "development") { + app.use("*", async (c, next) => { + console.log("CORS request:", { + origin: c.req.header("Origin"), + method: c.req.method, + headers: Object.fromEntries(c.req.headers.entries()) + }); + await next(); + }); +} +``` + +## Best Practices + +1. **Use Router-Level CORS**: More flexible than registry-level configuration +2. **Include ALLOWED_PUBLIC_HEADERS**: Required for RivetKit communication +3. **Specify Exact Origins**: Avoid wildcards in production +4. **Enable Credentials**: Needed for authentication +5. **Cache Preflight Requests**: Use appropriate `maxAge` values +6. **Environment-Specific Config**: Different settings for dev/prod + diff --git a/docs/concepts/edge.mdx b/docs/general/edge.mdx similarity index 78% rename from docs/concepts/edge.mdx rename to docs/general/edge.mdx index 535133a97..9efb0c71f 100644 --- a/docs/concepts/edge.mdx +++ b/docs/general/edge.mdx @@ -35,16 +35,16 @@ const actor = await client.example.get({ }); ``` -See [Create & Manage Actors](/docs/manage) for more information. +See [Create & Manage Actors](/actors/communicating-with-actors) for more information. ## Available regions -See available regions [here](/docs/regions). +See available regions [here](/general/edge.mdx). ### Fetching region list It's common to need to display a list of available regions in your application. -To fetch a full list of regions, you can use the `GET https://api.rivet.gg/regions` HTTP endpoint. See API documentation [here](/docs/api/actor/regions/list). +When deployed to Rivet, you can fetch a full list of regions with the `GET https://api.rivet.gg/regions` HTTP endpoint. See API documentation [here](https://rivet.gg/docs/api/actor/regions/list). We don't recommend hard-coding the region list. This allows you to develop your application with a local development cluster. diff --git a/docs/concepts/logging.mdx b/docs/general/logging.mdx similarity index 97% rename from docs/concepts/logging.mdx rename to docs/general/logging.mdx index ce10feb15..c31856863 100644 --- a/docs/concepts/logging.mdx +++ b/docs/general/logging.mdx @@ -46,7 +46,7 @@ Consider this example: ```typescript structured_logging.ts -import { actor } from "rivetkit"; +import { actor } from "@rivetkit/actor"; const counter = actor({ state: { count: 0 }, @@ -64,7 +64,7 @@ const counter = actor({ ``` ```typescript unstructured_logging.ts -import { actor } from "rivetkit"; +import { actor } from "@rivetkit/actor"; const counter = actor({ state: { count: 0 }, @@ -92,7 +92,7 @@ Additionally, structured logs can be parsed and queried at scale using tools lik The logger is available in all lifecycle hooks: ```typescript -import { actor } from "rivetkit"; +import { actor } from "@rivetkit/actor"; const loggingExample = actor({ state: { events: [] }, diff --git a/docs/general/registry.mdx b/docs/general/registry.mdx new file mode 100644 index 000000000..1636f8b81 --- /dev/null +++ b/docs/general/registry.mdx @@ -0,0 +1,293 @@ +--- +title: Registry +description: Configure and manage your actor registry +icon: list +--- + +The registry is the central configuration hub for your RivetKit application. It defines which actors are available and how your application runs. + +## Basic Setup + +Create a registry by importing your actors and using the `setup` function: + +```typescript +import { setup } from "@rivetkit/actor"; +import { counterActor } from "./actors/counter"; +import { chatRoomActor } from "./actors/chat-room"; + +export const registry = setup({ + use: { + counter: counterActor, + chatRoom: chatRoomActor, + }, +}); +``` + +## Creating Servers + +### Development Server + +For development, create and run a server directly: + +```typescript +import { registry } from "./registry"; + +// Start a development server +registry.runServer({ + driver: { + topology: "standalone", + actor: { type: "memory" }, + manager: { type: "memory" }, + }, +}); +``` + +### Production Setup + +For production, get the handler and integrate with your framework: + +```typescript +import { registry } from "./registry"; +import { Hono } from "hono"; + +// Create server components +const { client, hono, handler, serve } = registry.createServer({ + driver: { + topology: "partition", + actor: { type: "redis", url: "redis://localhost:6379" }, + manager: { type: "redis", url: "redis://localhost:6379" }, + }, +}); + +// Use with Hono +const app = new Hono(); +app.route("/registry", hono); + +// Or use the handler directly +app.all("/registry/*", handler); + +// Start the server +serve(app); +``` + +## Configuration Options + +### Driver Configuration + +The driver configuration determines how actors are stored and managed: + +```typescript +const { client } = registry.createServer({ + driver: { + // Topology: how actors are distributed + topology: "standalone", // "standalone" | "partition" | "coordinate" + + // Actor storage + actor: { + type: "memory", // "memory" | "file-system" | "redis" | "rivet" + // Additional driver-specific options + }, + + // Manager coordination + manager: { + type: "memory", // "memory" | "redis" | "rivet" + // Additional driver-specific options + }, + }, +}); +``` + +### Topology Options + +- **`standalone`**: Single process, good for development +- **`partition`**: Distributed actors, good for production scaling +- **`coordinate`**: Peer-to-peer coordination, good for high availability + +### Storage Drivers + +- **`memory`**: In-memory storage, data lost on restart +- **`file-system`**: Persistent file-based storage +- **`redis`**: Redis-backed persistence and coordination +- **`rivet`**: Rivet platform integration + +### CORS Configuration + +Configure CORS for browser clients: + +```typescript +registry.runServer({ + cors: { + origin: ["https://myapp.com", "https://staging.myapp.com"], + credentials: true, + }, +}); +``` + +### Request Limits + +Configure request size limits: + +```typescript +registry.runServer({ + maxIncomingMessageSize: 1024 * 1024, // 1MB limit +}); +``` + +## Worker Mode + +For distributed topologies, you can create worker instances: + +```typescript +// Manager instance (handles routing) +const { hono: managerHono } = registry.createServer({ + driver: { topology: "partition", /* ... */ }, +}); + +// Worker instance (runs actors) +const { hono: workerHono } = registry.createWorker({ + driver: { topology: "partition", /* ... */ }, +}); +``` + +## Type Safety + +The registry provides full type safety for your client: + +```typescript +// TypeScript knows about your actors +const counter = client.counter.getOrCreate(["my-counter"]); +const chatRoom = client.chatRoom.getOrCreate(["general"]); + +// Action calls are type-checked +const count: number = await counter.increment(5); +``` + +## Testing Configuration + +Use memory drivers for testing: + +```typescript +// test-registry.ts +export const testRegistry = setup({ + use: { + counter: counterActor, + chatRoom: chatRoomActor, + }, +}); + +// In your tests +const { client } = testRegistry.createServer({ + driver: { + topology: "standalone", + actor: { type: "memory" }, + manager: { type: "memory" }, + }, +}); +``` + +## Environment-Specific Configuration + +Use environment variables to configure different environments: + +```typescript +const isProd = process.env.NODE_ENV === "production"; +const redisUrl = process.env.REDIS_URL || "redis://localhost:6379"; + +export const registry = setup({ + use: { + counter: counterActor, + chatRoom: chatRoomActor, + }, +}); + +// Environment-specific server creation +export function createAppServer() { + return registry.createServer({ + driver: isProd + ? { + topology: "partition", + actor: { type: "redis", url: redisUrl }, + manager: { type: "redis", url: redisUrl }, + } + : { + topology: "standalone", + actor: { type: "memory" }, + manager: { type: "memory" }, + }, + cors: { + origin: isProd ? "https://myapp.com" : "*", + }, + }); +} +``` + +## Best Practices + +### Registry Organization + +Keep your registry clean and organized: + +```typescript +// actors/index.ts - Export all actors +export { counterActor } from "./counter"; +export { chatRoomActor } from "./chat-room"; +export { gameActor } from "./game"; + +// registry.ts - Import and configure +import { setup } from "@rivetkit/actor"; +import * as actors from "./actors"; + +export const registry = setup({ + use: actors, +}); +``` + +### Actor Naming + +Use consistent naming conventions: + +```typescript +export const registry = setup({ + use: { + // Use camelCase for actor names + counter: counterActor, + chatRoom: chatRoomActor, + userProfile: userProfileActor, + + // Group related actors with prefixes + gameSession: gameSessionActor, + gameLobby: gameLobbyActor, + }, +}); +``` + +### Configuration Management + +Separate configuration from registry definition: + +```typescript +// config.ts +export const appConfig = { + redis: { + url: process.env.REDIS_URL || "redis://localhost:6379", + }, + cors: { + origin: process.env.ALLOWED_ORIGINS?.split(",") || ["*"], + }, +}; + +// server.ts +import { registry } from "./registry"; +import { appConfig } from "./config"; + +const { serve } = registry.createServer({ + driver: { + topology: "partition", + actor: { type: "redis", url: appConfig.redis.url }, + manager: { type: "redis", url: appConfig.redis.url }, + }, + cors: appConfig.cors, +}); + +serve(); +``` diff --git a/docs/concepts/testing.mdx b/docs/general/testing.mdx similarity index 97% rename from docs/concepts/testing.mdx rename to docs/general/testing.mdx index f398d51b6..2b098df8b 100644 --- a/docs/concepts/testing.mdx +++ b/docs/general/testing.mdx @@ -43,7 +43,7 @@ test("my actor test", async (test) => { ``` ```ts src/index.ts -import { actor, setup } from "rivetkit"; +import { actor, setup } from "@rivetkit/actor"; const myActor = actor({ state: { value: "initial" }, @@ -90,7 +90,7 @@ test("actor should persist state", async (test) => { ``` ```ts src/index.ts -import { setup } from "rivetkit"; +import { setup } from "@rivetkit/actor"; const counter = actor({ state: { count: 0 }, @@ -141,7 +141,7 @@ test("actor should emit events", async (test) => { ``` ```ts src/index.ts -import { actor, setup } from "rivetkit"; +import { actor, setup } from "@rivetkit/actor"; export const chatRoom = actor({ state: { @@ -192,7 +192,7 @@ test("scheduled tasks should execute", async (test) => { ``` ```ts src/index.ts -import { actor, setup } from "rivetkit"; +import { actor, setup } from "@rivetkit/actor"; const scheduler = actor({ state: { diff --git a/docs/general/webhooks.mdx b/docs/general/webhooks.mdx new file mode 100644 index 000000000..1333ed77b --- /dev/null +++ b/docs/general/webhooks.mdx @@ -0,0 +1 @@ +TODO diff --git a/docs/images/integrations/elysia.svg b/docs/images/integrations/elysia.svg new file mode 100644 index 000000000..db27a0ebf --- /dev/null +++ b/docs/images/integrations/elysia.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/docs/images/integrations/express.svg b/docs/images/integrations/express.svg new file mode 100644 index 000000000..8e4296838 --- /dev/null +++ b/docs/images/integrations/express.svg @@ -0,0 +1 @@ + diff --git a/docs/images/integrations/trpc.svg b/docs/images/integrations/trpc.svg new file mode 100644 index 000000000..103966bc0 --- /dev/null +++ b/docs/images/integrations/trpc.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/integrations/better-auth.mdx b/docs/integrations/better-auth.mdx index 0c256867e..295190b53 100644 --- a/docs/integrations/better-auth.mdx +++ b/docs/integrations/better-auth.mdx @@ -1,303 +1,406 @@ --- title: Better Auth +description: Integrate RivetKit with Better Auth for authentication --- - - - -```sh -npm install better-auth -``` +Better Auth provides a comprehensive authentication solution that integrates seamlessly with Rivet Actors using the `onAuth` hook. - + + Check out the complete example + - +## Installation - - -```ts Hono -import { registry } from "./registry"; -import { Hono } from "hono"; +Install Better Auth alongside RivetKit: -// Start RivetKit -// -// State is stored in memory, this can be configured later -const { client, hono } = registry.run(); +```bash +npm install better-auth better-sqlite3 +npm install -D @types/better-sqlite3 -// Setup server -const app = new Hono(); - -// Expose RivetKit to the frontend (optional) -app.route("/registry", hono); +# For React integration +npm install @rivetkit/react +``` -app.post("/increment/:name", async (c) => { - const name = c.req.param("name"); + + This example uses SQLite to keep the example. In production, replace this with a database like Postgres. Read more about [configuring your database in Better Auth](https://www.better-auth.com/docs/installation#configure-database). + - // Communicate with actor - const counter = client.counter.getOrCreate(name); - const newCount = await counter.increment(1); +## Backend Setup - return c.text(`New Count: ${newCount}`); + + +Create your authentication configuration: + +```typescript auth.ts +import { betterAuth } from "better-auth"; +import Database from "better-sqlite3"; + +export const auth = betterAuth({ + database: new Database("/tmp/auth.sqlite"), + trustedOrigins: ["http://localhost:5173"], + emailAndPassword: { + enabled: true, + }, }); - -// app.fetch will be used to run the server -export { registry, default: app.fetch }; -``` - -```ts Express.js -TODO ``` + -```ts Elysia.js -TODO -``` - - - - - TODO: Exporting `registry` and `app.fetch` (as `default`) is important. - + +Create and apply the database schema: -_If you want to run without export fetch (i.e. standalone Node.js), see below._ +```bash +# Generate migration files +pnpm dlx @better-auth/cli@latest generate --config auth.ts +# Apply migrations to create the database tables +pnpm dlx @better-auth/cli@latest migrate --config auth.ts -y +``` - - - + +Use the `onAuth` hook to validate sessions: -```ts Actor +```typescript registry.ts import { actor, setup } from "@rivetkit/actor"; - -export const counter = actor({ - onAuth: () => { ... }, - state: { count: 0 }, - actions: { - increment: (c, x: number) => { - c.state.count += x; - return c.state.count; - }, - }, +import { Unauthorized } from "@rivetkit/actor/errors"; +import { auth } from "./auth"; + +export const chatRoom = actor({ + // Validate authentication before actor access + onAuth: async (opts) => { + const { req } = opts; + + // Use Better Auth to validate the session + const authResult = await auth.api.getSession({ + headers: req.headers, + }); + if (!authResult) throw new Unauthorized(); + + // Return user data to be available in actor + return { + user: authResult.user, + session: authResult.session, + }; + }, + + state: { + messages: [] as Array<{ + id: string; + userId: string; + username: string; + message: string; + timestamp: number; + }>, + }, + + actions: { + sendMessage: (c, message: string) => { + // Access authenticated user data + const { user } = c.conn.auth; + + const newMessage = { + id: crypto.randomUUID(), + userId: user.id, + username: user.name, + message, + timestamp: Date.now(), + }; + + c.state.messages.push(newMessage); + c.broadcast("newMessage", newMessage); + + return newMessage; + }, + + getMessages: (c) => c.state.messages, + }, }); export const registry = setup({ - use: { counter }, + use: { chatRoom }, }); ``` - -```ts Workflow -``` - - - - - - + +Configure your server to handle Better Auth routes and RivetKit: -```sh Node.js -npx serve-fetch src/server.ts -``` - -```sh Bun -bun serve src/server.ts -``` - - - - - - +```typescript +// server.ts +import { registry } from "./registry"; +import { auth } from "./auth"; +import { Hono } from "hono"; +import { cors } from "hono/cors"; +import { ALLOWED_PUBLIC_HEADERS } from "@rivetkit/actor"; - +const { serve } = registry.createServer(); +const app = new Hono(); -```ts fetch -const res = await fetch("http://localhost:8080/increment/foo", { - method: "POST" -}); -console.log("Output:", await res.text()); -``` +// Configure CORS for Better Auth + RivetKit +app.use("*", cors({ + // Where your frontend is running + origin: ["http://localhost:5173"], + // ALLOWED_PUBLIC_HEADERS are headers required for RivetKit to operate + allowHeaders: ["Authorization", ...ALLOWED_PUBLIC_HEADERS], + allowMethods: ["POST", "GET", "OPTIONS"], + exposeHeaders: ["Content-Length"], + maxAge: 600, + credentials: true, +})); + +// Mount Better Auth routes +app.on(["GET", "POST"], "/api/auth/**", (c) => + auth.handler(c.req.raw) +); -```sh curl -curl -X POST localhost:8080/increment/foo +// Start RivetKit server +serve(app); ``` - - - - + - - - - - - -```sh -npx rivet-cli deploy src/server.ts -``` - -Your endpoint is now TODO - -Test it with TODO - - - - -TODO - +## Frontend Integration - + + +Create a Better Auth client for your frontend: -```sh -npm install @rivetkit/redis -``` +```typescript +// auth-client.ts +import { createAuthClient } from "better-auth/react"; -```ts server.ts -import { registry } from "./registry"; -import { createRedisDriver } from "@rivetkit/redis"; - -// Start RivetKit -const { client, hono } = registry.run({ - driver: createRedisDriver({ - host: "127.0.0.1", - port: 6379, - }), +export const authClient = createAuthClient({ + baseURL: "http://localhost:8080", }); - -// ...rest of code... ``` + - - - - -```sh -npm install @rivetkit/file-system + +Create login/signup forms: + +```tsx +// AuthForm.tsx +import React, { useState } from "react"; +import { authClient } from "./auth-client"; + +export function AuthForm() { + const [isLogin, setIsLogin] = useState(true); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [name, setName] = useState(""); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + try { + if (isLogin) { + await authClient.signIn.email({ email, password }); + } else { + await authClient.signUp.email({ email, password, name }); + } + } catch (error) { + console.error("Auth error:", error); + } + }; + + return ( +
+

{isLogin ? "Sign In" : "Sign Up"}

+ + {!isLogin && ( + setName(e.target.value)} + required + /> + )} + + setEmail(e.target.value)} + required + /> + + setPassword(e.target.value)} + required + /> + + + + +
+ ); +} ``` +
-```ts server.ts -import { registry } from "./registry"; -import { createFileSystemDriver } from "@rivetkit/file-system"; - -// Start RivetKit -const { client, hono } = registry.run({ - driver: createFileSystemDriver(), -}); + +Use authenticated sessions with RivetKit: -// ...rest of code... -``` +```tsx +// ChatRoom.tsx +import React, { useState } from "react"; +import { createClient } from "@rivetkit/client"; +import { createRivetKit } from "@rivetkit/react"; +import { authClient } from "./auth-client"; +import type { registry } from "../backend/registry"; -
+const client = createClient("http://localhost:8080"); +const { useActor } = createRivetKit(client); -
+interface ChatRoomProps { + session: { user: { id: string; name: string } }; + roomId: string; +} +export function ChatRoom({ session, roomId }: ChatRoomProps) { + const [newMessage, setNewMessage] = useState(""); + + const chatRoom = useActor({ + name: "chatRoom", + key: [roomId], + }); + + const sendMessage = async () => { + if (!newMessage.trim()) return; + + await chatRoom.sendMessage(newMessage); + setNewMessage(""); + }; + + return ( +
+
+ Welcome, {session.user.name}! + +
+ +
+ {chatRoom.state.messages.map(msg => ( +
+ {msg.username}: {msg.message} +
+ ))} +
+ +
+ setNewMessage(e.target.value)} + onKeyPress={(e) => e.key === "Enter" && sendMessage()} + placeholder="Type a message..." + /> + +
+
+ ); +} +```
- -## Configuration Options - -### Connect your frontend to the Rivet Actor - -TODO: Quick summary of why you would want to connect your frontend - -Connect your frontend: - - - -```ts JavaScript -import { createClient } from "@rivetkit/actor/client"; -import type { registry } from "./registry.js"; - -const client = createClient("http://localhost:8080/registry"); - -const result = await client.myActor.getOrCreate().myAction("Hello, world!"); +## Advanced Features + +### Role-Based Access + +Add role checking to your actors: + +```typescript +export const adminActor = actor({ + onAuth: async (opts) => { + const authResult = await auth.api.getSession({ + headers: opts.req.headers, + }); + if (!authResult) throw new Unauthorized(); + + return { user: authResult.user }; + }, + + actions: { + deleteUser: (c, userId: string) => { + // Check user role (assuming you store roles in user data) + const { user } = c.conn.auth; + if (user.role !== "admin") { + throw new Unauthorized("Admin access required"); + } + + // Admin-only action + // ... implementation + }, + }, +}); ``` -```ts React -import { useState } from "react"; -import { createClient, createRivetKit } from "@@rivetkit/actor/react"; -import type { registry } from "./registry"; - -const client = createClient(`http://localhost:8080/registry`); -const { useActor } = createRivetKit(client); - -function App() { - const [count, setCount] = useState(0); - const [counterName, setCounterName] = useState("test-counter"); - - const counter = useActor({ - name: "counter", - key: [counterName], - }); - - counter.useEvent("newCount", (x: number) => setCount(x)); - - const increment = async () => { - await counter.connection?.increment(1); - }; - - return ( -
-

Counter: {count}

- setCounterName(e.target.value)} - placeholder="Counter name" - /> - -
- ); +### Session Management + +Handle session expiration gracefully: + +```tsx +// hooks/useAuth.ts +import { authClient } from "./auth-client"; +import { useEffect } from "react"; + +export function useAuthWithRefresh() { + const { data: session, error } = authClient.useSession(); + + useEffect(() => { + if (error?.message?.includes("session")) { + // Redirect to login on session expiration + window.location.href = "/login"; + } + }, [error]); + + return session; } ``` -
+## Production Deployment -TODO: Learn more under the XXXX docs +For production, you'll need a database from a provider like [Neon](https://neon.tech/), [PlanetScale](https://planetscale.com/), [AWS RDS](https://aws.amazon.com/rds/), or [Google Cloud SQL](https://cloud.google.com/sql). - - TODO: Link to onAuth docs - +Configure your production database connection: - - TODO: Note that `/registry` must be exposed - +```typescript +// auth.ts +import { betterAuth } from "better-auth"; +import { Pool } from "pg"; -### Run as standalone server (no fetch handler) - -TODO: Intro - -```ts -import { registry } from "./registry"; -import { Hono } from "hono"; -import { serve } from "@hono/node-server"; - -// Start RivetKit -const { client, hono } = registry.run(); - -// Setup server -const app = new Hono(); - -// ...setup routes... - -serve({ fetch: app.fetch, port: 8080 }, (x) => - console.log("Listening at http://localhost:8080"), -); +export const auth = betterAuth({ + database: new Pool({ + connectionString: process.env.DATABASE_URL, + }), + trustedOrigins: [process.env.FRONTEND_URL], + emailAndPassword: { enabled: true }, +}); ``` -IMPORTANT: You'll need to do special stuff to support deploying to Rivet or Cloudflare Workers +Set the following environment variables for production: -## Next Steps +```bash +DATABASE_URL=postgresql://username:password@localhost:5432/myapp +FRONTEND_URL=https://myapp.com +BETTER_AUTH_SECRET=your-secure-secret-key +BETTER_AUTH_URL=https://api.myapp.com +``` -TODO +Read more about [configuring Postgres with Better Auth](https://www.better-auth.com/docs/adapters/postgresql). - - - - - - + + Don't forget to re-generate & re-apply your database migrations if you change the database in your Better Auth config. + diff --git a/docs/integrations/elysia.mdx b/docs/integrations/elysia.mdx new file mode 100644 index 000000000..7576bc808 --- /dev/null +++ b/docs/integrations/elysia.mdx @@ -0,0 +1,87 @@ +--- +title: Elysia +description: Integrate RivetKit with Elysia for fast TypeScript web applications +--- + +Elysia is a fast and type-safe web framework for Bun. RivetKit integrates seamlessly with Elysia using the `.mount()` method. + + + Check out the complete example + + +## Installation + +Install Elysia alongside RivetKit: + +```bash +npm install elysia +# or with bun +bun add elysia +``` + +## Basic Setup + + + +Set up your Rivet Actors: + +```typescript +// registry.ts +import { actor, setup } from "@rivetkit/actor"; + +export const counter = actor({ + state: { count: 0 }, + actions: { + increment: (c, amount: number = 1) => { + c.state.count += amount; + c.broadcast("countChanged", c.state.count); + return c.state.count; + }, + getCount: (c) => c.state.count, + }, +}); + +export const registry = setup({ + use: { counter }, +}); +``` + + + +Mount RivetKit into your Elysia application: + +```typescript +// server.ts +import { registry } from "./registry"; +import { Elysia } from "elysia"; + +const { client, handler } = registry.createServer(); + +// Setup Elysia app +const app = new Elysia() + // Mount RivetKit handler + .mount("/registry", handler) + // Add your API routes + .post("/increment/:name", async ({ params }) => { + const name = params.name; + + const counter = client.counter.getOrCreate([name]); + const newCount = await counter.increment(1); + + return `New Count: ${newCount}`; + }) + .get("/count/:name", async ({ params }) => { + const name = params.name; + + const counter = client.counter.getOrCreate([name]); + const count = await counter.getCount(); + + return { count }; + }) + .listen(8080); + +console.log("Server running at http://localhost:8080"); +``` + + + diff --git a/docs/integrations/express.mdx b/docs/integrations/express.mdx new file mode 100644 index 000000000..865883e18 --- /dev/null +++ b/docs/integrations/express.mdx @@ -0,0 +1,109 @@ +--- +title: Express +description: Integrate RivetKit with Express.js for Node.js web applications +--- + +Express.js is a popular Node.js web framework. RivetKit integrates seamlessly with Express using middleware mounting. + + + Check out the complete example + + +## Installation + +Install Express alongside RivetKit: + +```bash +npm install express +npm install -D @types/express +``` + +## Basic Setup + + + +Set up your Rivet Actor: + +```typescript +// registry.ts +import { actor, setup } from "@rivetkit/actor"; + +export const counter = actor({ + state: { count: 0 }, + actions: { + increment: (c, amount: number = 1) => { + c.state.count += amount; + c.broadcast("countChanged", c.state.count); + return c.state.count; + }, + getCount: (c) => c.state.count, + }, +}); + +export const registry = setup({ + use: { counter }, +}); +``` + + + +Mount RivetKit into your Express application: + +```typescript +// server.ts +import { registry } from "./registry"; +import express from "express"; + +// Start RivetKit +const { client, handler } = registry.createServer(); + +// Setup Express app +const app = express(); + +// Enable JSON parsing +app.use(express.json()); + +// Mount RivetKit handler +app.use("/registry", handler); + +// Add your API routes +app.post("/increment/:name", async (req, res) => { + const name = req.params.name; + const { amount = 1 } = req.body; + + try { + const counter = client.counter.getOrCreate([name]); + const newCount = await counter.increment(amount); + + res.json({ success: true, count: newCount }); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +app.get("/count/:name", async (req, res) => { + const name = req.params.name; + + try { + const counter = client.counter.getOrCreate([name]); + const count = await counter.getCount(); + + res.json({ name, count }); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +app.listen(8080, () => { + console.log("Server running at http://localhost:8080"); +}); +``` + + + diff --git a/docs/integrations/hono.mdx b/docs/integrations/hono.mdx index 26c292867..9c3199622 100644 --- a/docs/integrations/hono.mdx +++ b/docs/integrations/hono.mdx @@ -1,199 +1,102 @@ --- title: Hono +description: Integrate RivetKit with Hono for ultra-fast web applications --- -[Hono](https://hono.dev/) is a lightweight web framework that works well with RivetKit across multiple deployment platforms. This guide explains how to integrate RivetKit with Hono on different platforms. +Hono is an ultra-fast web framework that works on any runtime. RivetKit integrates seamlessly with Hono through the `serve()` method. -## Mounting The RivetKit Router + + Check out the complete example + -When mounting the RivetKit router at a custom path, you **must** specify the same path in the router configuration using `basePath`: +## Installation -```typescript -// Setup the RivetKit app -const registry = setup({ - use: { counter }, - // IMPORTANT: Must specify the same basePath where your router is mounted - basePath: "/my-path" -}); - -// Create a router from the app -const { router: actorRouter } = createRouter(app); +Install Hono alongside RivetKit: -// Mount at the same path specified in basePath -honoApp.route("/my-path", actorRouter); +```bash +npm install hono ``` -This ensures that WebSocket connections and other functionality work correctly when accessing your actors through the custom path. - -## Platform-Specific Examples - -Each platform has specific requirements for integrating Hono with RivetKit. - -### Cloudflare Workers - -```typescript -import { createRouter } from "@rivetkit/cloudflare-workers"; -import { setup } from "rivetkit"; -import { Hono } from "hono"; -import counter from "./counter"; - -// Create your Hono app inside the fetch handler -const honoApp = new Hono(); - -// Add your custom routes -honoApp.get("/", (c) => c.text("Welcome to my app!")); -honoApp.get("/hello", (c) => c.text("Hello, world!")); - -// Setup the RivetKit app -const registry = setup({ - use: { counter }, - // IMPORTANT: Must specify the same basePath where your router is mounted - basePath: "/my-path" -}); +## Basic Setup -// Create a router and handler from the app -const { router: actorRouter, ActorHandler } = createRouter(app); - -// Mount the RivetKit router at /my-path -honoApp.route("/my-path", actorRouter); - -// IMPORTANT: Must export `ActorHandler` as this exact name -export { honoApp as default, ActorHandler }; -``` - -Make sure to update your client connection URL to include the custom path: + + +Set up your Rivet Actor: ```typescript -// If you mounted RivetKit at /my-path -import { createClient } from "rivetkit/client"; -import type { App } from "./src/index"; - -const client = createClient("https://your-actor.actors.dev/my-path"); -``` - -For this to work with Cloudflare Workers, your `wrangler.json` **must** include specific Durable Object and KV namespace bindings with the exact names expected by RivetKit: - -```json -{ - "name": "counter", - "main": "src/index.ts", - "compatibility_date": "2025-01-29", - "migrations": [ - { - "new_classes": ["ActorHandler"], - "tag": "v1" - } - ], - "durable_objects": { - "bindings": [ - { - "class_name": "ActorHandler", // Must match exported class - "name": "ACTOR_DO" // Must use this exact name - } - ] +// registry.ts +import { actor, setup } from "@rivetkit/actor"; + +export const counter = actor({ + state: { count: 0 }, + actions: { + increment: (c, amount: number = 1) => { + c.state.count += amount; + c.broadcast("countChanged", c.state.count); + return c.state.count; + }, + getCount: (c) => c.state.count, }, - "kv_namespaces": [ - { - "binding": "ACTOR_KV", // Must use this exact name - "id": "YOUR_KV_NAMESPACE_ID" // Replace with your KV ID - } - ] -} -``` - - -### Node.js - -```typescript -import { serve } from "@hono/node-server"; -import { Hono } from "hono"; -import { setup, createRouter } from "@rivetkit/nodejs"; -import counter from "./counter"; - -// Create your Hono app -const honoApp = new Hono(); - -// Add your custom routes -honoApp.get("/", (c) => c.text("Welcome to my app!")); -honoApp.get("/hello", (c) => c.text("Hello, world!")); - -// Setup the RivetKit app -const registry = setup({ - use: { counter }, - // IMPORTANT: Must specify the same basePath where your router is mounted - basePath: "/my-path" }); -// Create a router from the app -const { router: actorRouter, injectWebSocket } = createRouter(app); - -// Mount the RivetKit router at /my-path -honoApp.route("/my-path", actorRouter); - -// Create server with the combined app -const server = serve({ - fetch: honoApp.fetch, - port: 8080, +export const registry = setup({ + use: { counter }, }); - -// IMPORTANT: Inject the websocket handler into the server -injectWebSocket(server); - -console.log("Server running at http://localhost:8080"); ``` + -Make sure to update your client connection URL to include the custom path: - -```typescript -// If you mounted RivetKit at /my-path -import { createClient } from "rivetkit/client"; -import type { App } from "./src/index"; - -const client = createClient("http://localhost:8080/my-path"); -``` - -### Bun + +Use RivetKit's `serve()` method with your Hono app: ```typescript +// server.ts +import { registry } from "./registry"; import { Hono } from "hono"; -import { setup, createRouter } from "@rivetkit/bun"; -import counter from "./counter"; -// Create your Hono app -const honoApp = new Hono(); - -// Add your custom routes -honoApp.get("/", (c) => c.text("Welcome to my app!")); -honoApp.get("/hello", (c) => c.text("Hello, world!")); - -// Setup the RivetKit app -const registry = setup({ - use: { counter }, - // IMPORTANT: Must specify the same basePath where your router is mounted - basePath: "/my-path" +// Start RivetKit +const { client, serve } = registry.createServer(); + +// Setup Hono app +const app = new Hono(); + +// Add your API routes +app.post("/increment/:name", async (c) => { + const name = c.req.param("name"); + const body = await c.req.json().catch(() => ({})); + const amount = body.amount || 1; + + try { + const counter = client.counter.getOrCreate([name]); + const newCount = await counter.increment(amount); + + return c.json({ success: true, count: newCount }); + } catch (error) { + return c.json({ + success: false, + error: error.message + }, 500); + } }); -// Create a router from the app -const { router: actorRouter, webSocketHandler } = createRouter(app); - -// Mount the RivetKit router at /my-path -honoApp.route("/my-path", actorRouter); +app.get("/count/:name", async (c) => { + const name = c.req.param("name"); + + try { + const counter = client.counter.getOrCreate([name]); + const count = await counter.getCount(); + + return c.json({ name, count }); + } catch (error) { + return c.json({ + success: false, + error: error.message + }, 500); + } +}); -// Create and start the server -export default { - port: 8080, - fetch: honoApp.fetch, - // IMPORTANT: Pass the webSocketHandler to Bun - websocket: webSocketHandler, -}; +// Start server with RivetKit integration +serve(app); ``` + + -Make sure to update your client connection URL to include the custom path: - -```typescript -// If you mounted RivetKit at /my-path -import { createClient } from "rivetkit/client"; -import type { App } from "./src/index"; - -const client = createClient("http://localhost:8080/my-path"); -``` diff --git a/docs/integrations/resend.mdx b/docs/integrations/resend.mdx deleted file mode 100644 index 49fdb6cbc..000000000 --- a/docs/integrations/resend.mdx +++ /dev/null @@ -1,268 +0,0 @@ ---- -title: Resend ---- - -[Resend](https://resend.com) is an email API service that works seamlessly with RivetKit for handling emails and notifications. - -## Example - -See how RivetKit and Resend can power engagement with daily streak notifications. - -View on GitHub - -## Quickstart - - - -```bash -npm install resend -``` - - - -```typescript actors.ts -import { actor, setup } from "rivetkit"; -import { Resend } from "resend"; - -const resend = new Resend(process.env.RESEND_API_KEY); - -const user = actor({ - state: { - email: null as string | null, - }, - - actions: { - // Example: Somehow acquire the user's email - register: async (c, email: string) => { - c.state.email = email; - }, - - // Example: Send an email - sendExampleEmail: async (c) => { - if (!c.state.email) throw new Error("No email registered"); - - await resend.emails.send({ - from: "updates@yourdomain.com", - to: c.state.email, - subject: "Hello, world!", - html: "

Lorem ipsum

", - }); - }, - }, -}); - -export const registry = setup({ use: { user } }); -``` -
- - -```typescript client.ts -import { createClient } from "rivetkit"; -import { App } from "./actors/app.ts"; - -const client = createClient("http://localhost:8787"); -const userActor = await client.user.get({ tags: { user: "user123" } }); - -await userActor.register("user@example.com"); -await userActor.sendExampleEmail(); -``` - -
- -## Use Cases - -### Scheduling Emails - -RivetKit's scheduling capabilities with Resend make it easy to send emails at specific times: - - -```typescript actors.ts -const emailScheduler = actor({ - state: { - email: null as string | null, - }, - - actions: { - scheduleEmail: async (c, email: string, delayMs: number = 86400000) => { - c.state.email = email; - await c.schedule.at(Date.now() + delayMs, "sendEmail"); - }, - - sendEmail: async (c) => { - if (!c.state.email) return; - - await resend.emails.send({ - from: "updates@yourdomain.com", - to: c.state.email, - subject: "Your scheduled message", - html: "

This email was scheduled earlier!

", - }); - }, - }, -}); -``` - -```typescript client.ts -const client = createClient({ url: "http://localhost:3000" }); -const scheduler = await client.emailScheduler.get({ id: "user123" }); -await scheduler.scheduleEmail("user@example.com", 60000); // 1 minute -``` -
- -### Daily Reminders - -Send daily reminders to users based on their activity: - - -```typescript actors.ts -const reminder = actor({ - state: { - email: null as string | null, - lastActive: null as number | null, - }, - - actions: { - trackActivity: async (c, email: string) => { - c.state.email = email; - c.state.lastActive = Date.now(); - - // Schedule check for tomorrow - await c.schedule.at(Date.now() + 24 * 60 * 60 * 1000, "checkActivity"); - }, - - checkActivity: async (c) => { - if (!c.state.email) return; - - // If inactive for 24+ hours, send reminder - if (Date.now() - (c.state.lastActive || 0) >= 24 * 60 * 60 * 1000) { - await resend.emails.send({ - from: "reminders@yourdomain.com", - to: c.state.email, - subject: "We miss you!", - html: "

Don't forget to check in today.

", - }); - } - - // Reschedule for tomorrow - await c.schedule.at(Date.now() + 24 * 60 * 60 * 1000, "checkActivity"); - }, - }, -}); -``` - -```typescript client.ts -const client = createClient({ url: "http://localhost:3000" }); -const userReminder = await client.reminder.get({ id: "user123" }); -await userReminder.trackActivity("user@example.com"); -``` -
- -### Alerting Systems - -Monitor your systems and send alerts when issues are detected: - - -```typescript actors.ts -const monitor = actor({ - state: { - alertEmail: null as string | null, - isHealthy: true, - }, - - actions: { - configure: async (c, email: string) => { - c.state.alertEmail = email; - await c.schedule.at(Date.now() + 60000, "checkHealth"); - }, - - checkHealth: async (c) => { - // Simple mock health check - const wasHealthy = c.state.isHealthy; - c.state.isHealthy = await mockHealthCheck(); - - // Alert on status change to unhealthy - if (wasHealthy && !c.state.isHealthy && c.state.alertEmail) { - await resend.emails.send({ - from: "alerts@yourdomain.com", - to: c.state.alertEmail, - subject: "⚠️ System Alert", - html: "

The system is experiencing issues.

", - }); - } - - // Reschedule next check - await c.schedule.at(Date.now() + 60000, "checkHealth"); - }, - }, -}); - -// Mock function -async function mockHealthCheck() { - return Math.random() > 0.1; // 90% chance of being healthy -} -``` - -```typescript client.ts -const client = createClient({ url: "http://localhost:3000" }); -const systemMonitor = await client.monitor.get({ id: "api-service" }); -await systemMonitor.configure("admin@example.com"); -``` -
- -## Testing - -When testing actors that use Resend, you should mock the Resend API to avoid sending real emails during tests. RivetKit's testing utilities combined with Vitest make this straightforward: - -```typescript -import { test, expect, vi, beforeEach } from "vitest"; -import { setupTest } from "rivetkit/test"; -import { app } from "../actors/app"; - -// Create mock for send method -const mockSendEmail = vi.fn().mockResolvedValue({ success: true }); - -beforeEach(() => { - process.env.RESEND_API_KEY = "test_mock_api_key_12345"; - - vi.mock("resend", () => { - return { - Resend: vi.fn().mockImplementation(() => { - return { - emails: { - send: mockSendEmail - } - }; - }) - }; - }); - - mockSendEmail.mockClear(); -}); - -test("email is sent when action is called", async (t) => { - const { client } = await setupTest(t, app); - const actor = await client.user.get(); - - // Call the action that should send an email - await actor.someActionThatSendsEmail("user@example.com"); - - // Verify the email was sent with the right parameters - expect(mockSendEmail).toHaveBeenCalledWith( - expect.objectContaining({ - to: "user@example.com", - subject: "Expected Subject", - }), - ); -}); -``` - -Using `vi.advanceTimersByTimeAsync()` is particularly useful for testing scheduled emails: - -```typescript -// Fast forward time to test scheduled emails -await vi.advanceTimersByTimeAsync(24 * 60 * 60 * 1000); // Advance 24 hours - -// Test that the scheduled email was sent -expect(mockSendEmail).toHaveBeenCalledTimes(2); -``` - diff --git a/docs/integrations/trpc.mdx b/docs/integrations/trpc.mdx new file mode 100644 index 000000000..0c8242bc7 --- /dev/null +++ b/docs/integrations/trpc.mdx @@ -0,0 +1,152 @@ +--- +title: tRPC +description: Integrate RivetKit with tRPC for end-to-end type-safe APIs +--- + +tRPC provides end-to-end type safety for your APIs. RivetKit integrates seamlessly with tRPC, allowing you to create type-safe procedures that call Rivet Actors. + + + Check out the complete example + + +## Installation + +Install tRPC alongside RivetKit: + +```bash +npm install @trpc/server @trpc/client zod +npm install -D @trpc/next # if using Next.js +``` + +## Basic Setup + + + +Set up your Rivet Actors: + +```typescript +// registry.ts +import { actor, setup } from "@rivetkit/actor"; + +export const counter = actor({ + state: { count: 0 }, + actions: { + increment: (c, amount: number = 1) => { + c.state.count += amount; + c.broadcast("countChanged", c.state.count); + return c.state.count; + }, + getCount: (c) => c.state.count, + reset: (c) => { + c.state.count = 0; + c.broadcast("countChanged", 0); + return 0; + }, + }, +}); + +export const registry = setup({ + use: { counter }, +}); +``` + + + +Create your tRPC router that uses RivetKit: + +```typescript +// server.ts +import { registry } from "./registry"; +import { initTRPC } from "@trpc/server"; +import { createHTTPServer } from "@trpc/server/adapters/standalone"; +import { z } from "zod"; + +// Start RivetKit +const { client } = registry.createServer(); + +// Initialize tRPC +const t = initTRPC.create(); + +// Create tRPC router with RivetKit integration +const appRouter = t.router({ + // Counter procedures + counter: t.router({ + increment: t.procedure + .input(z.object({ + name: z.string(), + amount: z.number().optional().default(1) + })) + .mutation(async ({ input }) => { + const counter = client.counter.getOrCreate([input.name]); + const newCount = await counter.increment(input.amount); + return { name: input.name, count: newCount }; + }), + + get: t.procedure + .input(z.object({ name: z.string() })) + .query(async ({ input }) => { + const counter = client.counter.getOrCreate([input.name]); + const count = await counter.getCount(); + return { name: input.name, count }; + }), + + reset: t.procedure + .input(z.object({ name: z.string() })) + .mutation(async ({ input }) => { + const counter = client.counter.getOrCreate([input.name]); + const count = await counter.reset(); + return { name: input.name, count }; + }), + }), +}); + +// Export type for client +export type AppRouter = typeof appRouter; + +// Create HTTP server +const server = createHTTPServer({ + router: appRouter, +}); + +server.listen(3001); +console.log("tRPC server listening at http://localhost:3001"); +``` + + + +Create a type-safe tRPC client: + +```typescript +// client.ts +import { createTRPCProxyClient, httpBatchLink } from "@trpc/client"; +import type { AppRouter } from "./server"; + +export const trpc = createTRPCProxyClient({ + links: [ + httpBatchLink({ + url: "http://localhost:3001", + }), + ], +}); + +// Usage examples +async function examples() { + // Increment counter + const result = await trpc.counter.increment.mutate({ + name: "my-counter", + amount: 5 + }); + console.log(result); // { name: "my-counter", count: 5 } + + // Get counter value + const value = await trpc.counter.get.query({ name: "my-counter" }); + console.log(value); // { name: "my-counter", count: 5 } + + // Reset counter + const reset = await trpc.counter.reset.mutate({ name: "my-counter" }); + console.log(reset); // { name: "my-counter", count: 0 } +} +``` + + + diff --git a/docs/integrations/vitest.mdx b/docs/integrations/vitest.mdx new file mode 100644 index 000000000..6637fb6f7 --- /dev/null +++ b/docs/integrations/vitest.mdx @@ -0,0 +1,6 @@ +--- +title: Vitest +--- + +See [Testing](/general/testing) documentation. + diff --git a/docs/introduction.mdx b/docs/introduction.mdx index 7a251d3bc..7ae36d7ad 100644 --- a/docs/introduction.mdx +++ b/docs/introduction.mdx @@ -5,11 +5,9 @@ sidebarTitle: Welcome mode: custom --- -import ComparisonTable from "/snippets/landing-comparison-table.mdx"; import Snippets from "/snippets/landing-snippets.mdx"; import Tech from "/snippets/landing-tech.mdx"; import Quotes from "/snippets/landing-quotes.mdx"; -import Manifesto from "/snippets/landing-manifesto.mdx"; import FAQ from "/snippets/landing-faq.mdx"; {/*
*/} @@ -55,11 +53,11 @@ import FAQ from "/snippets/landing-faq.mdx"; */} -
- +
+
-
+

Actors

@@ -69,7 +67,7 @@ import FAQ from "/snippets/landing-faq.mdx";
- +
@@ -82,7 +80,7 @@ import FAQ from "/snippets/landing-faq.mdx";
- +
@@ -95,7 +93,7 @@ import FAQ from "/snippets/landing-faq.mdx";
- {/* + {/*
@@ -165,21 +163,21 @@ import FAQ from "/snippets/landing-faq.mdx";
Supports
{/* Platforms (all-in-one) */} - + Rivet Platform
Rivet
- + Cloudflare Workers
Cloudflare Workers
{/* Compute */} - + Node.js
Node.js
- + Bun
Bun
@@ -203,15 +201,11 @@ import FAQ from "/snippets/landing-faq.mdx"; TypeScript
TypeScript
- - Python -
Python (Client)
-
Rust
Rust (Client)
- + React
React
@@ -221,7 +215,19 @@ import FAQ from "/snippets/landing-faq.mdx"; Hono
Hono
- + + Express +
Express
+
+ + Express +
Elysia
+
+ + tRPC +
tRPC
+
+ Vitest
Vitest
@@ -238,19 +244,6 @@ import FAQ from "/snippets/landing-faq.mdx";
- {/* -
- -
-

Less Complexity, More Functionality

-

RivetKit provides a solid foundation with the features you'd expect for modern apps.

- -
- - -
- */} -
@@ -328,12 +321,6 @@ import FAQ from "/snippets/landing-faq.mdx";
*/} - {/*
- -
- -
*/} -
diff --git a/docs/llm/claude.mdx b/docs/llm/claude.mdx deleted file mode 100644 index 7e80ed611..000000000 --- a/docs/llm/claude.mdx +++ /dev/null @@ -1,89 +0,0 @@ ---- -title: "Using Claude Code with RivetKit" -sidebarTitle: "Claude Code" ---- - -[Claude Code](https://claude.ai/code) is a powerful CLI tool from Anthropic that integrates Claude directly into your development workflow. When working with RivetKit, you can enhance Claude's understanding of the codebase by providing project-specific context. - -## Setting Up CLAUDE.md for RivetKit - -The `CLAUDE.md` file serves as a memory file for Claude Code, containing important information about your project that persists between sessions. - -To set up an effective `CLAUDE.md` file for RivetKit: - - - - Copy the RivetKit [prompt.txt](/llm/prompt) - - - Create a `CLAUDE.md` file in your project root - - - Paste the template into `CLAUDE.md` - - - Run Claude Code and ask a simple question like `What lifecycle hook is used when a actor is first created?` to confirm it's reading your CLAUDE.md file correctly - - - -## Example Commands for RivetKit - -Here are some useful ways to leverage Claude Code with RivetKit: - -### Understand the Codebase - -```bash -# Get an overview of RivetKit's architecture -claude "explain the architecture of RivetKit and how the different topologies work" - -# Understand how actors communicate -claude "explain how actors communicate with each other in the coordinate topology" - -# Learn about specific concepts -claude "explain the lifecycle hooks for actors and when each one is called" -``` - -### Find Relevant Code - -```bash -# Find implementations of specific components -claude "find the files that implement the Redis driver" - -# Locate examples of specific patterns -claude "show me examples of RPC methods in the codebase" - -# Understand actor state management -claude "explain how actor state is persisted between restarts" -``` - -### Add New Features - -```bash -# Create a new actor implementation -claude "help me create a new actor for managing user sessions" - -# Add authentication to a actor -claude "show me how to add authentication to my actor's _onBeforeConnect method" - -# Implement error handling -claude "help me implement proper error handling for my actor's RPC methods" -``` - -### Debug Issues - -```bash -# Diagnose connection problems -claude "my actor connections are dropping, help me debug why" - -# Fix state persistence issues -claude "my actor state isn't persisting between restarts, what could be wrong?" - -# Address scaling problems -claude "my actors are using too much memory, how can I optimize them?" -``` - -## Additional Resources - -- [Claude Code Documentation](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview) -- [Claude Code Tutorials](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/tutorials) - diff --git a/docs/llm/cursor.mdx b/docs/llm/cursor.mdx deleted file mode 100644 index 7779627ea..000000000 --- a/docs/llm/cursor.mdx +++ /dev/null @@ -1,104 +0,0 @@ ---- -title: "Using Cursor with RivetKit" -sidebarTitle: "Cursor" ---- - -This guide shows how to integrate RivetKit with Cursor to enhance your developer experience through AI-assisted coding. - -## Setting Up Cursor Rules for RivetKit - -Cursor rules allow you to customize how the AI assistant behaves when working with RivetKit code. By providing project-specific context, you'll get more accurate and relevant suggestions. - -### Project Rules (Recommended) - -Project rules are stored in the `.cursor/rules` directory and apply to specific files or folders in your project. - - -Open your RivetKit project in Cursor - - Press `Cmd + Shift + P` (Mac) or `Ctrl + Shift + P` (Windows/Linux). - - Select `New Cursor Rule` and name it `RivetKit-development-guide`. - - - Set `Auto Attach` to `**/*.ts`. - - Paste [prompt.txt](/llm/prompt) into the rule. - - - -### Global Rules - -If you frequently work with RivetKit across multiple projects, you can add RivetKit-specific rules globally: - - - - Open Cursor and go to `Cursor Settings` > `Rules` > `User Rules` - - - Paste [prompt.txt](/llm/prompt) into the textbox - - - -Global rules will apply to all of your projects when using Cursor. - -## Example Queries for RivetKit - -Here are some useful prompts to try with Cursor when working with RivetKit: - -### Understand the Codebase - -``` -# Get an overview of RivetKit's architecture -Explain the architecture of RivetKit and how the different topologies work - -# Understand how actors communicate -Explain how actors communicate with each other in the coordinate topology - -# Learn about specific concepts -Explain the lifecycle hooks for actors and when each one is called -``` - -### Find Relevant Code - -``` -# Find implementations of specific components -Find the files that implement the Redis driver - -# Locate examples of specific patterns -Show me examples of RPC methods in the codebase - -# Understand actor state management -Explain how actor state is persisted between restarts -``` - -### Add New Features - -``` -# Create a new actor implementation -Help me create a new actor for managing user sessions - -# Add authentication to a actor -Show me how to add authentication to my actor's _onBeforeConnect method - -# Implement error handling -Help me implement proper error handling for my actor's RPC methods -``` - -### Debug Issues - -``` -# Diagnose connection problems -My actor connections are dropping, help me debug why - -# Fix state persistence issues -My actor state isn't persisting between restarts, what could be wrong? - -# Address scaling problems -My actors are using too much memory, how can I optimize them? -``` - -## Additional Resources - -- [Cursor Rules Documentation](https://docs.cursor.com/context/rules-for-ai) - diff --git a/docs/llm/docs-as-markdown.mdx b/docs/llm/docs-as-markdown.mdx deleted file mode 100644 index 02bd9ed3b..000000000 --- a/docs/llm/docs-as-markdown.mdx +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: Docs As Markdown for LLMs -sidebarTitle: Docs as Markdown ---- - -RivetKit documentation is designed to be easily accessible for Large Language Models (LLMs). All documentation pages are automatically available as plain Markdown files by simply appending `.md` to the URL. - -## How to Access - -To access any RivetKit documentation page as Markdown: - -1. Navigate to the regular documentation URL (e.g., `https://RivetKit.org/concepts/state`) -2. Append `.md` to the URL (e.g., `https://RivetKit.org/concepts/state.md`) - -This will return the raw Markdown content of the documentation page. - -## Use Cases - -This feature enables seamless integration with AI tools by providing documentation in clean Markdown format. Key uses include: - -- **Context for LLM chats**: Importing relevant docs into LLM chat sessions for specific context -- **Web crawling**: Enabling efficient crawling of documentation content -- **AI agents**: Building AI agents with access to RivetKit documentation - -## Additional Resources - -- [prompt.txt](/llm/prompt) -- [llms.txt](/llm/llms) -- [llms-full.txt](/llm/llms-full) - diff --git a/docs/llm/llms-full.mdx b/docs/llm/llms-full.mdx deleted file mode 100644 index d524fc4ef..000000000 --- a/docs/llm/llms-full.mdx +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: "llms-full.txt" -url: "/llms-full.txt" ---- - diff --git a/docs/llm/llms.mdx b/docs/llm/llms.mdx deleted file mode 100644 index c7390ea94..000000000 --- a/docs/llm/llms.mdx +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: "llms.txt" -url: "/llms.txt" ---- - diff --git a/docs/llm/prompt.mdx b/docs/llm/prompt.mdx deleted file mode 100644 index 6a7a1aabd..000000000 --- a/docs/llm/prompt.mdx +++ /dev/null @@ -1,170 +0,0 @@ ---- -title: "prompt.txt" ---- - -## Using `prompt.txt` for RivetKit - -The `prompt.txt` file provides LLMs with comprehensive information about RivetKit's conventions, terminology, and best practices. To use it: - -1. Copy the contents below to your clipboard -2. Paste it into your preferred AI assistant (Claude, ChatGPT, Cursor rules, Windsurf Rules, etc.) -3. Ask your RivetKit development questions after the prompt - -This structured information helps AI tools provide more accurate and contextually relevant guidance for your RivetKit development tasks. - -## AI Editor Guides - -Read the integration guide for your editor of choice: - - - - - - - -## `prompt.txt` - -````markdown prompt.txt -# RivetKit Development Guide - -This guide contains essential information for working with the RivetKit project. - -## Project Naming and Terminology - -- Use `RivetKit` when referring to the project in documentation and plain English -- Use `rivetkit` (kebab-case) when referring to the project in code, package names, and imports - -### Core Concepts - -- **Actor**: A stateful, long-lived entity that processes messages and maintains state -- **Manager**: Component responsible for creating, routing, and managing actor instances -- **Action**: Method for a actor to expose callable functions to clients -- **Event**: Asynchronous message sent from a actor to connected clients -- **Alarm**: Scheduled callback that triggers at a specific time - -## Build Commands - -- **Type Check:** `yarn check-types` - Verify TypeScript types -- **Check specific package:** `yarn check-types -F rivetkit` - Check only specified package -- **Build:** `yarn build` - Production build using Turbopack -- **Build specific package:** `yarn build -F rivetkit` - Build only specified package -- **Format:** `yarn fmt` - Format code with Biome - -## Driver Implementations - -Available driver implementations: - -- **Memory**: In-memory implementation for development and testing -- **Redis**: Production-ready implementation using Redis for persistence and pub/sub -- **Cloudflare Workers**: Uses Durable Objects for actor state persistence -- **Rivet**: Fully-managed cloud platform with built-in scaling and monitoring - -## Platform Support - -RivetKit supports multiple runtime environments: - -- **NodeJS**: Standard Node.js server environment -- **Cloudflare Workers**: Edge computing environment -- **Bun**: Fast JavaScript runtime alternative to Node.js -- **Rivet**: Cloud platform with built-in scaling and management - -## Package Import Resolution - -When importing from workspace packages, always check the package's `package.json` file under the `exports` field to determine the correct import paths: - -1. Locate the package's `package.json` file -2. Find the `exports` object which maps subpath patterns to their file locations -3. Use these defined subpaths in your imports rather than direct file paths - -## Code Style Guidelines - -- **Formatting:** Uses Biome for consistent formatting -- **Imports:** Organized imports enforced, unused imports warned -- **TypeScript:** Strict mode enabled, target ESNext -- **Naming:** - - camelCase for variables, functions - - PascalCase for classes, interfaces, types - - UPPER_CASE for constants -- **Error Handling:** - - Use `UserError` for client-safe errors - - Use `InternalError` for internal errors - -## Project Structure - -- Monorepo with Yarn workspaces and Turborepo -- Core code in `packages/rivetkit/` -- Platform implementations in `packages/platforms/` -- Driver implementations in `packages/drivers/` - -## State Management - -- Each actor owns and manages its own isolated state via `c.state` -- State is automatically persisted between action calls -- State is initialized via `createState` function or `state` constant -- Only JSON-serializable types can be stored in state -- Use `onStateChange` to react to state changes - -## Authentication and Security - -- Authentication is handled through the `onBeforeConnect` lifecycle hook -- Connection state is accessed with `c.conn.state` -- Access control should be implemented for each action -- Throwing an error in `onBeforeConnect` will abort the connection -- Use `UserError` for safe error messages to clients -- Use data validation libraries like zod for input validation - -## Actions and Events - -- **Actions**: Used for clients to call actor functions -- **Events**: For actors to publish updates to clients -- Actions are defined in the `actions` object of the actor configuration -- Helper functions outside the `actions` object are not callable by clients -- Broadcasting is done via `c.broadcast(name, data)` -- Specific client messaging uses `conn.send(name, data)` -- Clients subscribe to events with `actor.on(eventName, callback)` - -## Lifecycle Hooks - -- `createState()`: Function that returns initial actor state -- `onStart(c)`: Called any time actor is started (after restart/upgrade) -- `onStateChange(c, newState)`: Called when actor state changes -- `onBeforeConnect(c)`: Called when new client connects -- `onConnect(c)`: Executed after client connection succeeds -- `onDisconnect(c)`: Called when client disconnects - -## Actor Management - -- App is configured with actors using `setup({ use: { actorName }})` followed by `serve(registry)` -- Actors are accessed by client using `client.actorName.get()` -- Actors can pass an ID parameter or object with `client.actorName.get(id)` or `client.actorName.get({key: value})` -- Actors can be shut down with `c.shutdown()` from within the actor - -## Scaling and Architecture Guidelines - -- Each actor should have a single responsibility -- Keep state minimal and relevant to the actor's core function -- Use separate actors for different entity types (users, rooms, documents) -- Avoid too many cross-actor communications -- Use appropriate topology based on your scaling needs - -## Scheduling - -- Schedule future events with `c.after(duration, fn, ...args)` -- Schedule events for specific time with `c.at(timestamp, fn, ...args)` -- Scheduled events persist across actor restarts - -## CORS Configuration - -- Configure CORS to allow cross-origin requests in production -- Set allowed origins, methods, headers, and credentials -- For development, use `cors: { origin: "http://localhost:3000" }` - -## Development Best Practices - -- Prefer functional actor pattern with `actor({ ... })` syntax -- Use zod for runtime type validation -- Use `assertUnreachable(x: never)` for exhaustive type checking -- Add proper JSDoc comments for public APIs -- Run `yarn check-types` regularly during development -- Use `tsx` CLI to execute TypeScript scripts directly -```` diff --git a/docs/llm/windsurf.mdx b/docs/llm/windsurf.mdx deleted file mode 100644 index fe234e624..000000000 --- a/docs/llm/windsurf.mdx +++ /dev/null @@ -1,102 +0,0 @@ ---- -title: "Using Windsurf with RivetKit" -sidebarTitle: "Windsurf" ---- - -This guide shows how to integrate RivetKit with Windsurf to enhance your developer experience through AI-assisted coding. - -## Setting Up Windsurf Rules for RivetKit - -Windsurf rules allow you to customize how the AI assistant (Cascade) behaves when working with RivetKit code. By providing project-specific context, you'll get more accurate and relevant suggestions. - -### Workspace Rules (Recommended) - -Workspace rules are stored in the `.windsurfrules` file in your project root and apply to your local workspace. - - -Open your RivetKit project in Windsurf - - Navigate to "Windsurf - Settings" (at the bottom right) > "Settings" > "Cascade" > "Set Workspace AI Rules" and click "Edit Rules." - - This will create a `.windsurfrules` file in your project root. - - - Paste [prompt.txt](/llm/prompt) into the `.windsurfrules` file. - - - -### Global Rules - -If you frequently work with RivetKit across multiple projects, you can add RivetKit-specific rules globally: - - - - Navigate to "Windsurf - Settings" (at the bottom right) > "Settings" > "Cascade" > "Set Global AI Rules" and click "Edit Rules." - - - Paste [prompt.txt](/llm/prompt) into the `global_rules.md` file. - - - -Global rules will apply to all of your projects when using Windsurf. - -## Example Queries for RivetKit - -Here are some useful prompts to try with Windsurf when working with RivetKit: - -### Understand the Codebase - -``` -# Get an overview of RivetKit's architecture -Explain the architecture of RivetKit and how the different topologies work - -# Understand how actors communicate -Explain how actors communicate with each other in the coordinate topology - -# Learn about specific concepts -Explain the lifecycle hooks for actors and when each one is called -``` - -### Find Relevant Code - -``` -# Find implementations of specific components -Find the files that implement the Redis driver - -# Locate examples of specific patterns -Show me examples of RPC methods in the codebase - -# Understand actor state management -Explain how actor state is persisted between restarts -``` - -### Add New Features - -``` -# Create a new actor implementation -Help me create a new actor for managing user sessions - -# Add authentication to a actor -Show me how to add authentication to my actor's _onBeforeConnect method - -# Implement error handling -Help me implement proper error handling for my actor's RPC methods -``` - -### Debug Issues - -``` -# Diagnose connection problems -My actor connections are dropping, help me debug why - -# Fix state persistence issues -My actor state isn't persisting between restarts, what could be wrong? - -# Address scaling problems -My actors are using too much memory, how can I optimize them? -``` - -## Additional Resources - -- [Windsurf Rules Documentation](https://docs.codeium.com/windsurf/memories#windsurfrules) - diff --git a/docs/media/.DS_Store b/docs/media/.DS_Store deleted file mode 100644 index 84026c6cb..000000000 Binary files a/docs/media/.DS_Store and /dev/null differ diff --git a/docs/media/platforms/rivet/dash.png b/docs/media/platforms/rivet/dash.png deleted file mode 100644 index e54d1479f..000000000 Binary files a/docs/media/platforms/rivet/dash.png and /dev/null differ diff --git a/docs/media/platforms/rivet/service-token.png b/docs/media/platforms/rivet/service-token.png deleted file mode 100644 index b516b7b0d..000000000 Binary files a/docs/media/platforms/rivet/service-token.png and /dev/null differ diff --git a/docs/platforms/bun.mdx b/docs/platforms/bun.mdx deleted file mode 100644 index 3f940ed8f..000000000 --- a/docs/platforms/bun.mdx +++ /dev/null @@ -1,77 +0,0 @@ ---- -title: Bun ---- - -import SetupNextSteps from '/snippets/setup-next-steps.mdx'; -import MvpWarning from '/snippets/mvp-warning.mdx'; -import ExtraNotes from "/snippets/platform-extra-notes.mdx"; -import StepStartFramework from "/snippets/step-start-framework.mdx"; -import StepUpdateClient from "/snippets/step-update-client.mdx"; - -Bun provides a fast runtime environment for running RivetKit, with excellent performance for both development and production. - - - -## Deploy - - - - - - Install the Bun platform package: - - ```sh - bun add rivetkit/bun - ``` - - - - Create a file `src/index.ts` to start your RivetKit server: - - ```typescript src/index.ts - import { serve } from "@rivetkit/bun"; - import { app } from "../actors/app"; - - // Start the server with file-system driver (default) - serve(registry); - ``` - - - - Build and run your production server: - - ```sh - bun build ./src/index.ts --outdir ./dist - bun ./dist/index.js - ``` - - Deploy to any cloud provider of your choice. - - - - - - - -## Using Different Drivers - -By default, RivetKit for Bun uses the file-system driver, which persists state between restarts. - -For simple deployments, you can switch between drivers with: - -```typescript -serve(app, { - mode: "in-memory", // Switch to in-memory (file-system is default) -}); -``` - -For production, consider using an alternative driver like [Redis](/drivers/redis). - -## Available Regions - -Bun typically runs in a single region at a time. For multi-region deployments, consider: - -- [Rivet](/platforms/rivet) - Managed cloud service with global deployment -- [Cloudflare Workers](/platforms/cloudflare-workers) - Edge computing with global distribution - - diff --git a/docs/platforms/cloudflare-workers.mdx b/docs/platforms/cloudflare-workers.mdx deleted file mode 100644 index 760d4954a..000000000 --- a/docs/platforms/cloudflare-workers.mdx +++ /dev/null @@ -1,150 +0,0 @@ ---- -title: Cloudflare Workers (Durable Objects) -sidebarTitle: Durable Objects ---- - -import SetupNextSteps from '/snippets/setup-next-steps.mdx'; -import MvpWarning from '/snippets/mvp-warning.mdx'; -import ExtraNotes from "/snippets/platform-extra-notes.mdx"; -import StepStartFramework from "/snippets/step-start-framework.mdx"; -import StepUpdateClient from "/snippets/step-update-client.mdx"; - -The Cloudflare Workers platform with Durable Objects provides a robust environment for running RivetKit at the edge. - - - -## Deploy - - - - - - Install the Cloudflare Workers platform package: - - - ```sh npm - npm install rivetkit/cloudflare-workers - ``` - - ```sh pnpm - pnpm add rivetkit/cloudflare-workers - ``` - - ```sh yarn - yarn add rivetkit/cloudflare-workers - ``` - - ```sh bun - bun add rivetkit/cloudflare-workers - ``` - - - - - Create a file `src/index.ts` to handle Cloudflare Workers integration: - - ```typescript src/index.ts - import { createHandler } from "@rivetkit/cloudflare-workers"; - import { app } from "../actors/app"; - - // Create handlers for Cloudflare Workers - const { handler, ActorHandler } = createHandler(app); - - // Export the handlers for Cloudflare - export { handler as default, ActorHandler }; - ``` - - - - Create a KV namespace for your actors: - - ```sh - npx wrangler kv:namespace create ACTOR_KV - ``` - - Take note of the KV namespace ID from the output, as you'll need it in the next step. - - - - Create a `wrangler.jsonc` configuration and add your KV namespace ID from the previous step: - - ```json wrangler.jsonc - { - "$schema": "node_modules/wrangler/config-schema.json", - "name": "my-app", - "main": "src/index.ts", - "compatibility_date": "2025-04-04", - "observability": { - "enabled": true - }, - "migrations": [ - { - "tag": "v1", - "new_classes": ["ActorHandler"] - } - ], - "durable_objects": { - "bindings": [ - { - "name": "ACTOR_DO", - "class_name": "ActorHandler" - } - ] - }, - "kv_namespaces": [ - { - "binding": "ACTOR_KV", - "id": "your-namespace-id-here" // Replace with your actual ID - } - ] - } - ``` - - - - Deploy your project to Cloudflare Workers: - - ```sh - npx wrangler deploy - ``` - - Your RivetKit application will be available at your Cloudflare Workers URL. - - - - - - - -## Accessing Cloudflare Context And Bindings - -You can access Cloudflare-specific features like the [DurableObjectState](https://developers.cloudflare.com/durable-objects/api/state/) and environment bindings from your actor: - -```typescript -import { actor } from "rivetkit"; - -const myActor = actor({ - // Load Cloudflare-specific variables - createVars: (c, cloudflare) => ({ - ctx: cloudflare.ctx, - env: cloudflare.env, - }), - actions: { - foo: async (c) => { - // Access DurableObjectState - await c.vars.ctx.storage.get("foo"); - - // Access bindings - await c.vars.env.MY_BUCKET.put(key, "foo"); - }, - } -}); -``` - -## Available Regions - -See available regions [here](https://developers.cloudflare.com/durable-objects/reference/data-location/#supported-locations-1). - -Cloudflare does not guarantee your code will run in the requested region. - - diff --git a/docs/platforms/nodejs.mdx b/docs/platforms/nodejs.mdx deleted file mode 100644 index 9dc10a784..000000000 --- a/docs/platforms/nodejs.mdx +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: Node.js ---- - -import SetupNextSteps from "/snippets/setup-next-steps.mdx"; -import MvpWarning from "/snippets/mvp-warning.mdx"; -import ExtraNotes from "/snippets/platform-extra-notes.mdx"; -import StepStartFramework from "/snippets/step-start-framework.mdx"; -import StepUpdateClient from "/snippets/step-update-client.mdx"; - -Node.js provides a robust environment for running RivetKit, ideal for development and production deployments. - - - -## Deploy - - - - - - Install the Node.js platform package: - - - ```sh npm - npm install rivetkit/nodejs - ``` - - ```sh pnpm - pnpm add rivetkit/nodejs - ``` - - ```sh yarn - yarn add rivetkit/nodejs - ``` - - ```sh bun - bun add rivetkit/nodejs - ``` - - - - - Create a file `src/index.ts` to start your RivetKit server: - - ```typescript src/index.ts - import { serve } from "@rivetkit/nodejs"; - import { app } from "../actors/app"; - - // Start the server with file-system driver (default) - serve(registry); - ``` - - - - Compile your TypeScript code and run the server: - - ```sh - npx -p typescript tsc - node dist/index.js - ``` - - Deploy to any cloud provider of your choice. - - - - - - - - -## Using Different Drivers - -By default, RivetKit for Node.js uses the file-system driver, which persists state between restarts. - -For simple deployments, you can switch between drivers with: - -```typescript -serve(app, { - mode: "in-memory", // Switch to in-memory (file-system is default) -}); -``` - -For production, consider using an alternative driver like [Redis](/drivers/redis). - -## Available Regions - -Node.js typically runs in a single region at a time. For multi-region deployments, consider: - -- [Rivet](/platforms/rivet) - Managed cloud service with global deployment -- [Cloudflare Workers](/platforms/cloudflare-workers) - Edge computing with global distribution - - diff --git a/docs/platforms/rivet.mdx b/docs/platforms/rivet.mdx deleted file mode 100644 index 47afaeaed..000000000 --- a/docs/platforms/rivet.mdx +++ /dev/null @@ -1,105 +0,0 @@ ---- -title: Rivet ---- - -import MvpWarning from "/snippets/mvp-warning.mdx"; -import StepStartFramework from "/snippets/step-start-framework.mdx"; -import StepUpdateClient from "/snippets/step-update-client.mdx"; -import ExtraNotes from "/snippets/platform-extra-notes.mdx"; -import SetupNextSteps from "/snippets/setup-next-steps.mdx"; - -Rivet provides a fully managed cloud service for running RivetKit, with automatic scaling, global deployment, and built-in monitoring. - - - -## Deploy - - - - - - Install the Rivet platform package: - - - ```sh npm - npm install rivetkit/rivet - ``` - - ```sh pnpm - pnpm add rivetkit/rivet - ``` - - ```sh yarn - yarn add rivetkit/rivet - ``` - - ```sh bun - bun add rivetkit/rivet - ``` - - - - - Deploy your RivetKit application to Rivet: - - - ```sh npm - npx rivetkit/cli@latest deploy rivet actors/app.ts - ``` - - ```sh pnpm - pnpm exec rivetkit/cli@latest deploy rivet actors/app.ts - ``` - - ```sh yarn - yarn rivetkit/cli@latest deploy rivet actors/app.ts - ``` - - ```sh bun - bunx rivetkit/cli@latest deploy rivet actors/app.ts - ``` - - - This will: - 1. Prompt you to log in to Rivet if needed - 2. Ask you to select or create a Rivet project - 3. Build and deploy your application - 4. Provide you with your new endpoint - - Update your client code with the new endpoint URL and test your deployed application. - - - - - - - -## Accessing Rivet Context - -[Rivet's `ActorContext`](https://rivet.gg/docs/javascript-runtime#the-actor-context-object) can be accessed from `createVars`. - -```typescript -import { actor } from "rivetkit"; - -const myActor = actor({ - // Load Rivet-specific variables - createVars: (c, rivet) => ({ - rivet: rivet.ctx, - }), - actions: { - foo: async (c) => { - // Access ActorContext - c.log.info(`Region: ${c.vars.rivet.metadata.region.name}`); - await c.vars.rivet.kv.get("foo"); - }, - } -}); -``` - -## Available Regions - -Rivet supports deploying your actors to multiple regions automatically. You can specify region preferences in your Rivet project settings in the Rivet Hub. - -See available regions [here](https://rivet.gg/docs/regions). - - diff --git a/docs/scripts/faq.js b/docs/scripts/faq.js deleted file mode 100644 index 3e86410f6..000000000 --- a/docs/scripts/faq.js +++ /dev/null @@ -1,50 +0,0 @@ -function initializeFAQ() { - // Find all FAQ sections that need initialization - document.querySelectorAll('.faq-section:not([data-faq-initialized])').forEach(faqSection => { - // Skip if already initialized - if (faqSection.hasAttribute('data-faq-initialized')) return; - - console.log("[Initialize] FAQ", faqSection?.id || "all"); - - // Mark as initialized - faqSection.setAttribute('data-faq-initialized', 'true'); - - // Find all accordions in this section - const accordions = faqSection.querySelectorAll('.faq-accordion'); - if (!accordions.length) return; - - accordions.forEach(accordion => { - // Skip if already initialized - if (accordion.hasAttribute('data-initialized')) return; - - const button = accordion.querySelector('.faq-question'); - const answer = accordion.querySelector('.faq-answer'); - - if (!button || !answer) return; - - // Mark as initialized to prevent duplicate listeners - accordion.setAttribute('data-initialized', 'true'); - - button.addEventListener('click', () => { - const isOpen = accordion.getAttribute('data-state') === 'open'; - - // Close all other accordions in this section - accordions.forEach(otherAccordion => { - if (otherAccordion !== accordion) { - otherAccordion.setAttribute('data-state', 'closed'); - } - }); - - // Toggle current accordion - accordion.setAttribute('data-state', isOpen ? 'closed' : 'open'); - }); - }); - }); -} - -// Initial run on DOM ready -if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", initializeFAQ); -} else { - initializeFAQ(); -} \ No newline at end of file diff --git a/docs/snippets/cloudflare-deploy.mdx b/docs/snippets/cloudflare-deploy.mdx deleted file mode 100644 index 04820035e..000000000 --- a/docs/snippets/cloudflare-deploy.mdx +++ /dev/null @@ -1,36 +0,0 @@ -1. Create a new KV namespace with: - - ```sh - npx wrangler kv namespace create ACTOR_KV - ``` - -2. After creating the KV namespace, you will receive an ID. Update your `wrangler.json` file by replacing the placeholder in the `kv_namespaces` section with this ID. It should look like this: - - ```json - { - "kv_namespaces": [ - { - "binding": "ACTOR_KV", - "id": "your-namespace-id-here" // Replace with your actual ID - } - ] - // ...etc... - } - ``` - -3. Deploy your project to Cloudflare Workers by running: - - ```sh - npx wrangler deploy - ``` - -4. Update `tests/client.ts` (or wherever your client lives) to use the deployed endpoint. Replace the local endpoint in `client.ts` with your Cloudflare Workers URL: - - ```typescript - import { createClient } from "rivetkit/client"; - import type { App } from "../src/index"; - - const client = createClient("https://your-actor-subdomain.actors.dev"); - ``` - - Ensure you replace `your-actor-subdomain` with the actual subdomain assigned to your actor. diff --git a/docs/snippets/create-actor-cli.mdx b/docs/snippets/create-actor-cli.mdx deleted file mode 100644 index 65366ec96..000000000 --- a/docs/snippets/create-actor-cli.mdx +++ /dev/null @@ -1,21 +0,0 @@ - -```sh npx -npx create-actor@latest -``` - -```sh npm -npm create actor@latest -``` - -```sh pnpm -pnpm create actor@latest -``` - -```sh yarn -yarn create actor@latest -``` - -```sh bun -bun create actor@latest -``` - diff --git a/docs/snippets/driver-note.mdx b/docs/snippets/driver-note.mdx deleted file mode 100644 index 210b212f8..000000000 --- a/docs/snippets/driver-note.mdx +++ /dev/null @@ -1,3 +0,0 @@ - - You rarely need to manually configure drivers. The platform package you're using will configure the appropriate drivers for you. See the documentation for your platform for details. - diff --git a/docs/snippets/integration-existing-projects.mdx b/docs/snippets/integration-existing-projects.mdx deleted file mode 100644 index c8baf0b03..000000000 --- a/docs/snippets/integration-existing-projects.mdx +++ /dev/null @@ -1,3 +0,0 @@ - - If you already have an existing application and want to mount RivetKit on a subpath, see our [Hono integration guide](/integrations/hono). Remember to specify the same path in `config.basePath` as where you mount the router. - diff --git a/docs/snippets/landing-comparison-table.mdx b/docs/snippets/landing-comparison-table.mdx deleted file mode 100644 index 4cac8baf8..000000000 --- a/docs/snippets/landing-comparison-table.mdx +++ /dev/null @@ -1,561 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Feature - RivetKit - RivetKit - - Durable Objects - Durable Objects - - Socket.io - Socket.io - - Redis - Redis - - AWS Lambda - AWS Lambda -
-
-
-
- - In-Memory State -
-
-
- Fast access to in-memory data without complex caching -
-
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
-
-
-
-
- -
- RivetKit -
-
-
- -
- Durable Objects -
-
-
- -
- Socket.io -
-
-
- -
- Redis -
-
-
- AWS Lambda -
-
-
-
-
-
- - Persisted State -
-
-
- Built-in persistence that survives crashes and restarts -
-
-
-
- -
-
-
- -
-
-
-
-
-
-
-
-
-
-
- -
- RivetKit -
-
-
- -
- Durable Objects -
-
-
- Socket.io -
-
-
- Redis -
-
-
- AWS Lambda -
-
-
-
-
-
- - Actions -
-
-
- Define and call functions that interact with your actors -
-
-
-
- -
-
-
- -
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
- RivetKit -
-
-
- -
- Durable Objects -
-
-
- -
- Socket.io -
-
-
- Redis -
-
-
- -
- AWS Lambda -
-
-
-
-
-
- - Events (Pub/Sub) -
-
-
- Real-time messaging with publish/subscribe patterns -
-
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
-
-
-
-
- -
- RivetKit -
-
-
- -
- Durable Objects -
-
-
- -
- Socket.io -
-
-
- -
- Redis -
-
-
- AWS Lambda -
-
-
-
-
-
- - Scheduling -
-
-
- Run tasks in the future without external schedulers -
-
-
-
- -
-
-
- -
-
-
-
-
-
-
- -
-
-
-
-
- -
- RivetKit -
-
-
- -
- Durable Objects -
-
-
- Socket.io -
-
-
- Redis -
-
-
- -
- AWS Lambda -
-
-
-
-
-
- - Edge Computing -
-
-
- Deploy globally for low-latency access from anywhere -
-
-
-
- -
- ¹ -
-
- -
-
-
-
-
-
-
- -
-
-
-
-
- -
- RivetKit ¹ -
-
-
- -
- Durable Objects -
-
-
- Socket.io -
-
-
- Redis -
-
-
- -
- AWS Lambda -
-
-
-
-
-
- - No Vendor Lock -
-
-
- Run on multiple platforms without rewriting your app -
-
-
-
- -
-
-
-
-
- -
-
-
- -
-
-
-
-
-
-
- -
- RivetKit -
-
-
- Durable Objects -
-
-
- -
- Socket.io -
-
-
- -
- Redis -
-
-
- AWS Lambda -
-
-
- -
-

- = - requires significant boilerplate code or external service -

-

¹ = on supported platforms

-
diff --git a/docs/snippets/landing-manifesto.mdx b/docs/snippets/landing-manifesto.mdx deleted file mode 100644 index 68ba9d51b..000000000 --- a/docs/snippets/landing-manifesto.mdx +++ /dev/null @@ -1,21 +0,0 @@ -
-

Why We Created RivetKit

-

- Stateful serverless is the future of how applications will be architected. -

-

- Startups increasingly build on stateful serverless to ship faster, achieve better performance, and outscale databases like Postgres. The actor model – closely related to stateful serverless – has an established history in frameworks like Elixir, Orleans, and Akka, though these typically involve steep learning curves and complex infrastructure. Cloudflare demonstrates the power of this approach, having built their entire infrastructure – including R2, Workflows, and Queues – on their stateful serverless engine called Durable Objects. -

-

- With years of experience in gaming infrastructure, we've seen firsthand how the stateful serverless model excels. After building numerous systems like matchmaking, chat, presence, and social networks using stateful serverless, we're convinced it's hands down the best way to build applications. However, the ecosystem lacks accessibility and resources. -

-

- To popularize stateful serverless, we decided to build something that works for everyone. No vendor lock-in, no steep learning curve, and a community-driven approach that brings the best ideas from different ecosystems together. -

-

- At Rivet, we maintain an open-source runtime to run stateful serverless workloads – including RivetKit. We see maintaining RivetKit as a rising tide: more people will build applications this way, and we hope to provide the best deployment, monitoring, and collaboration solution for this architecture. -

-
- Nathan Flurry, Nicholas Kissel, and the Rivet Team -
-
\ No newline at end of file diff --git a/docs/snippets/landing-tech.mdx b/docs/snippets/landing-tech.mdx index cdf1da552..0a6e6be99 100644 --- a/docs/snippets/landing-tech.mdx +++ b/docs/snippets/landing-tech.mdx @@ -3,18 +3,18 @@

Runs Anywhere

Deploy RivetKit anywhere - from serverless platforms to your own infrastructure with RivetKit's flexible runtime options.

-

Don't see the runtime you want? Add your own.

+

Don't see the runtime you want? Add your own.

All-In-One

- + Rivet Rivet - + Cloudflare Cloudflare @@ -39,11 +39,11 @@ Supabase Help Wanted - + Bun Bun - + Node.js Node.js @@ -88,7 +88,7 @@

Frameworks

- + React React @@ -130,18 +130,21 @@ Hono Hono - + + Express + Express + + + Elysia + Elysia + + Vitest Vitest - - Resend - Resend - Better Auth Better Auth - On The Roadmap AI SDK diff --git a/docs/snippets/mvp-warning.mdx b/docs/snippets/mvp-warning.mdx deleted file mode 100644 index f5570703e..000000000 --- a/docs/snippets/mvp-warning.mdx +++ /dev/null @@ -1,3 +0,0 @@ - - RivetKit is still pre-v1.0. Please help us by report bugs on [GitHub Issues](https://github.com/rivet-gg/rivetkit/issues)! - diff --git a/docs/snippets/platform-extra-notes.mdx b/docs/snippets/platform-extra-notes.mdx deleted file mode 100644 index b88267b20..000000000 --- a/docs/snippets/platform-extra-notes.mdx +++ /dev/null @@ -1,10 +0,0 @@ -## CORS Configuration - -For security reasons, you should configure proper CORS settings in production. In the example above, we used `cors: { origin: "*" }` which allows requests from any domain. - -For production deployments, specify the exact domains that should be allowed to connect to your actors. Learn more in the [CORS documentation](/concepts/cors). - -## Integration with Existing Projects - -If you already have an existing application and want to mount RivetKit on a subpath, see our [Hono integration guide](/integrations/hono). Remember to specify the same path in `config.basePath` as where you mount the router. - diff --git a/docs/snippets/setup-actor.mdx b/docs/snippets/setup-actor.mdx deleted file mode 100644 index 750c04edb..000000000 --- a/docs/snippets/setup-actor.mdx +++ /dev/null @@ -1,22 +0,0 @@ -```typescript src/app.ts -import { actor, setup } from "rivetkit"; - -// Create actor -const counter = actor({ - state: { count: 0 }, - actions: { - increment: (c, x: number) => { - c.state.count += x; - c.broadcast("newCount", c.state.count); - return c.state.count; - } - } -}); - -// Create the application -export const registry = setup({ - use: { counter }, - cors: { origin: "http://localhost:8080" } -}); -``` - diff --git a/docs/snippets/setup-next-steps.mdx b/docs/snippets/setup-next-steps.mdx deleted file mode 100644 index 03229781a..000000000 --- a/docs/snippets/setup-next-steps.mdx +++ /dev/null @@ -1,9 +0,0 @@ -## Next Steps - - - - - - - - diff --git a/docs/snippets/setup-typescript.mdx b/docs/snippets/setup-typescript.mdx deleted file mode 100644 index 1d4b97c5e..000000000 --- a/docs/snippets/setup-typescript.mdx +++ /dev/null @@ -1,23 +0,0 @@ -Set up TypeScript: - - -```sh npm -npm install typescript @types/node tsx --save-dev -npx tsc --init --target es2022 --module nodenext --moduleResolution nodenext --outDir dist -``` - -```sh pnpm -pnpm add -D typescript @types/node tsx -pnpm exec tsc --init --target es2022 --module nodenext --moduleResolution nodenext --outDir dist -``` - -```sh yarn -yarn add -D typescript @types/node tsx -yarn tsc --init --target es2022 --module nodenext --moduleResolution nodenext --outDir dist -``` - -```sh bun -bun add -D typescript @types/node tsx -bun tsc --init --target es2022 --module nodenext --moduleResolution nodenext --outDir dist -``` - diff --git a/docs/snippets/step-define-actor.mdx b/docs/snippets/step-define-actor.mdx deleted file mode 100644 index ca7aba0b3..000000000 --- a/docs/snippets/step-define-actor.mdx +++ /dev/null @@ -1,25 +0,0 @@ - - Create a file `actors/app.ts` in your project with your actor definition: - - ```typescript actors/app.ts - import { actor, setup } from "rivetkit"; - - // Create actor - const counter = actor({ - state: { count: 0 }, - actions: { - increment: (c, x: number) => { - c.state.count += x; - c.broadcast("newCount", c.state.count); - return c.state.count; - } - } - }); - - // Create the application - export const registry = setup({ - use: { counter }, - cors: { origin: "*" } // Configure CORS for your production domains in production - }); - ``` - diff --git a/docs/snippets/step-deploy.mdx b/docs/snippets/step-deploy.mdx deleted file mode 100644 index 8ae946791..000000000 --- a/docs/snippets/step-deploy.mdx +++ /dev/null @@ -1,11 +0,0 @@ - - Now that you have your project running, deploy your application to one of these platforms: - - - - - - - - - diff --git a/docs/snippets/step-run-studio.mdx b/docs/snippets/step-run-studio.mdx deleted file mode 100644 index 38efd0d78..000000000 --- a/docs/snippets/step-run-studio.mdx +++ /dev/null @@ -1,26 +0,0 @@ - - Launch the development server with: - - - ```sh npm - npx rivetkit/cli@latest dev actors/app.ts - ``` - - ```sh pnpm - pnpm exec rivetkit/cli@latest dev actors/app.ts - ``` - - ```sh yarn - yarn rivetkit/cli@latest dev actors/app.ts - ``` - - ```sh bun - bunx rivetkit/cli@latest dev actors/app.ts - ``` - - - This will automatically start your app and open the studio in your browser. The studio supports hot-reloading, state inspection, visual RPC testing, and more debugging tools. - - ![RivetKit Studio](/images/screenshots/studio/simple.png) - - diff --git a/docs/snippets/step-start-framework.mdx b/docs/snippets/step-start-framework.mdx deleted file mode 100644 index 5ab7fb7e3..000000000 --- a/docs/snippets/step-start-framework.mdx +++ /dev/null @@ -1,10 +0,0 @@ - - Start with your framework of choice: - - - - - - - - diff --git a/docs/snippets/step-update-client.mdx b/docs/snippets/step-update-client.mdx deleted file mode 100644 index 091c5463b..000000000 --- a/docs/snippets/step-update-client.mdx +++ /dev/null @@ -1,21 +0,0 @@ - - Update your client to connect to your deployed app: - - - - ```typescript TypeScript - const client = createClient(/* FILL ME IN */); - ``` - - ```rust Rust - let client = Client::new(/* FILL ME IN */, TransportKind::WebSocket, EncodingKind::Cbor); - ``` - - ```python Python - client = ActorClient( - # FILL ME IN - ) - ``` - - - diff --git a/docs/support/enterprise.mdx b/docs/support/enterprise.mdx deleted file mode 100644 index 1291bee15..000000000 --- a/docs/support/enterprise.mdx +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: "Enterprise" ---- - -RivetKit is developed and maintained by [Rivet](https://rivet.gg), offering comprehensive enterprise support and deployment solutions tailored to your organization's needs. - -Our enterprise services include: - -- Dedicated technical support -- Custom deployment configurations -- Training and implementation assistance - -For enterprise inquiries, please contact our solutions team at [rivet.gg/sales](https://rivet.gg/sales). - diff --git a/docs/workflows/overview.mdx b/docs/workflows/overview.mdx deleted file mode 100644 index ec51d8b17..000000000 --- a/docs/workflows/overview.mdx +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Rivet Workflows -icon: square-info -sidebarTitle: "Overview" -description: TODO ---- - -TODO diff --git a/examples/better-auth-external-db/README.md b/examples/better-auth-external-db/README.md index 936e59286..a7acac3e8 100644 --- a/examples/better-auth-external-db/README.md +++ b/examples/better-auth-external-db/README.md @@ -21,46 +21,38 @@ cd rivetkit/examples/better-auth-external-db npm install ``` -### Database Setup - -Initialize the SQLite database with Better Auth tables: - -```sh -npm run db:setup -``` - -This will create the `auth.sqlite` database file with the required tables for user authentication. - ### Development ```sh npm run dev ``` -Open your browser to `http://localhost:5173` to see the frontend and the backend will be running on `http://localhost:8080`. +The database migrations will run automatically on startup. Open your browser to `http://localhost:5173` to see the frontend and the backend will be running on `http://localhost:8080`. ## Features - **Authentication**: Email/password authentication using Better Auth -- **Protected Actors**: RivetKit actors with authentication via `onAuth` hook +- **Protected Actors**: Rivet Actors with authentication via `onAuth` hook - **Real-time Chat**: Authenticated chat room with real-time messaging - **External Database**: Shows how to configure Better Auth with external database (SQLite example) ## How It Works -1. **Better Auth Setup**: Configured with SQLite database for persistent user storage +1. **Better Auth Setup**: Configured with SQLite database for persistent user storage (auto-migrated in development) 2. **Protected Actor**: The `chatRoom` actor uses the `onAuth` hook to verify user sessions 3. **Frontend Integration**: React components handle authentication flow and chat interface 4. **Session Management**: Better Auth handles session creation, validation, and cleanup +5. **Auto-Migration**: Database schema is automatically migrated when starting the development server ## Database Commands -- `npm run db:setup` - Initialize SQLite database with Better Auth tables +- `npm run db:generate` - Generate migration files for database schema changes +- `npm run db:migrate` - Apply migrations to the database (used in production) ## Key Files - `src/backend/auth.ts` - Better Auth configuration with SQLite database -- `src/backend/registry.ts` - RivetKit actor with authentication +- `src/backend/registry.ts` - Rivet Actor with authentication - `src/frontend/components/AuthForm.tsx` - Login/signup form - `src/frontend/components/ChatRoom.tsx` - Authenticated chat interface - `auth.sqlite` - SQLite database file (auto-created) diff --git a/examples/better-auth-external-db/better-auth_migrations/2025-06-27T06-53-24.043Z.sql b/examples/better-auth-external-db/better-auth_migrations/2025-06-27T06-53-24.043Z.sql new file mode 100644 index 000000000..13b82ff1d --- /dev/null +++ b/examples/better-auth-external-db/better-auth_migrations/2025-06-27T06-53-24.043Z.sql @@ -0,0 +1,7 @@ +create table "user" ("id" text not null primary key, "name" text not null, "email" text not null unique, "emailVerified" integer not null, "image" text, "createdAt" date not null, "updatedAt" date not null); + +create table "session" ("id" text not null primary key, "expiresAt" date not null, "token" text not null unique, "createdAt" date not null, "updatedAt" date not null, "ipAddress" text, "userAgent" text, "userId" text not null references "user" ("id")); + +create table "account" ("id" text not null primary key, "accountId" text not null, "providerId" text not null, "userId" text not null references "user" ("id"), "accessToken" text, "refreshToken" text, "idToken" text, "accessTokenExpiresAt" date, "refreshTokenExpiresAt" date, "scope" text, "password" text, "createdAt" date not null, "updatedAt" date not null); + +create table "verification" ("id" text not null primary key, "identifier" text not null, "value" text not null, "expiresAt" date not null, "createdAt" date, "updatedAt" date); \ No newline at end of file diff --git a/examples/better-auth-external-db/package.json b/examples/better-auth-external-db/package.json index 5c4cc3ab1..45c2950a3 100644 --- a/examples/better-auth-external-db/package.json +++ b/examples/better-auth-external-db/package.json @@ -4,13 +4,14 @@ "private": true, "type": "module", "scripts": { - "dev": "concurrently \"npm run dev:backend\" \"npm run dev:frontend\"", + "dev": "pnpm db:migrate && concurrently \"npm run dev:backend\" \"npm run dev:frontend\"", "dev:backend": "tsx --watch src/backend/server.ts", "dev:frontend": "vite", "build": "vite build", "check-types": "tsc --noEmit", "test": "vitest run", - "db:setup": "tsx scripts/setup-db.ts" + "db:generate": "pnpm dlx @better-auth/cli@latest generate --config src/backend/auth.ts", + "db:migrate": "pnpm dlx @better-auth/cli@latest migrate --config src/backend/auth.ts -y" }, "devDependencies": { "@rivetkit/actor": "workspace:*", diff --git a/examples/better-auth-external-db/scripts/setup-db.ts b/examples/better-auth-external-db/scripts/setup-db.ts deleted file mode 100644 index 48b99b0eb..000000000 --- a/examples/better-auth-external-db/scripts/setup-db.ts +++ /dev/null @@ -1,66 +0,0 @@ -import Database from "better-sqlite3"; - -// Create SQLite database and tables for Better Auth -const db = new Database("/tmp/auth.sqlite"); - -// Create user table -db.exec(` - CREATE TABLE IF NOT EXISTS user ( - id TEXT PRIMARY KEY, - email TEXT UNIQUE NOT NULL, - emailVerified BOOLEAN NOT NULL DEFAULT FALSE, - name TEXT NOT NULL, - createdAt INTEGER NOT NULL, - updatedAt INTEGER NOT NULL - ); -`); - -// Create session table -db.exec(` - CREATE TABLE IF NOT EXISTS session ( - id TEXT PRIMARY KEY, - userId TEXT NOT NULL, - expiresAt INTEGER NOT NULL, - token TEXT UNIQUE NOT NULL, - createdAt INTEGER NOT NULL, - updatedAt INTEGER NOT NULL, - ipAddress TEXT, - userAgent TEXT, - FOREIGN KEY (userId) REFERENCES user(id) ON DELETE CASCADE - ); -`); - -// Create account table -db.exec(` - CREATE TABLE IF NOT EXISTS account ( - id TEXT PRIMARY KEY, - userId TEXT NOT NULL, - accountId TEXT NOT NULL, - providerId TEXT NOT NULL, - accessToken TEXT, - refreshToken TEXT, - idToken TEXT, - accessTokenExpiresAt INTEGER, - refreshTokenExpiresAt INTEGER, - scope TEXT, - password TEXT, - createdAt INTEGER NOT NULL, - updatedAt INTEGER NOT NULL, - FOREIGN KEY (userId) REFERENCES user(id) ON DELETE CASCADE - ); -`); - -// Create verification table -db.exec(` - CREATE TABLE IF NOT EXISTS verification ( - id TEXT PRIMARY KEY, - identifier TEXT NOT NULL, - value TEXT NOT NULL, - expiresAt INTEGER NOT NULL, - createdAt INTEGER NOT NULL, - updatedAt INTEGER NOT NULL - ); -`); - -console.log("Database tables created successfully!"); -db.close(); diff --git a/examples/hono-react/README.md b/examples/hono-react/README.md index 2accebca7..ea7078c88 100644 --- a/examples/hono-react/README.md +++ b/examples/hono-react/README.md @@ -26,8 +26,8 @@ npm install npm run dev ``` -This will start both the Hono backend server and Vite React frontend. Open your browser to http://localhost:5173 to see the React app connected to RivetKit actors. +This will start both the Hono backend server and Vite React frontend. Open your browser to http://localhost:5173 to see the React app connected to Rivet Actors. ## License -Apache 2.0 \ No newline at end of file +Apache 2.0 diff --git a/examples/react/README.md b/examples/react/README.md index 2accebca7..ea7078c88 100644 --- a/examples/react/README.md +++ b/examples/react/README.md @@ -26,8 +26,8 @@ npm install npm run dev ``` -This will start both the Hono backend server and Vite React frontend. Open your browser to http://localhost:5173 to see the React app connected to RivetKit actors. +This will start both the Hono backend server and Vite React frontend. Open your browser to http://localhost:5173 to see the React app connected to Rivet Actors. ## License -Apache 2.0 \ No newline at end of file +Apache 2.0 diff --git a/examples/starter/README.md b/examples/starter/README.md index 72a1e4b7d..80edf99b0 100644 --- a/examples/starter/README.md +++ b/examples/starter/README.md @@ -57,7 +57,7 @@ Deploy to Rivet Cloud: rivet deploy ``` -Your RivetKit actors will be deployed as Rivet actors with automatic scaling and management. +Your Rivet Actors will be deployed as Rivet actors with automatic scaling and management. ## License diff --git a/packages/core/src/drivers/rivet/actor-meta.ts b/packages/core/src/drivers/rivet/actor-meta.ts index d0167fe18..3daf11da1 100644 --- a/packages/core/src/drivers/rivet/actor-meta.ts +++ b/packages/core/src/drivers/rivet/actor-meta.ts @@ -195,7 +195,7 @@ function convertActorToMeta(actor: RivetActor): ActorMeta | undefined { throw new Error(`Actor ${actor.id} does not have a actor role.`); } if (actor.tags.framework !== "rivetkit") { - throw new Error(`Actor ${actor.id} is not an RivetKit actor.`); + throw new Error(`Actor ${actor.id} is not an Rivet Actor.`); } return {