Stateful Serverless That Runs Anywhere
The easiest way to build
stateful,
AI agent,
collaborative, or
local-first
applications.
Deploy to Rivet, Cloudflare, Bun, Node.js, and more.
Long-Lived, Stateful Compute
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.
Durable State Without a Database
Your code’s state is saved automatically—no database, ORM, or config needed. Just use regular JavaScript objects or SQLite (available in April).
Blazing-Fast Reads & Writes
State is stored on the same machine as your compute, so reads and writes are ultra-fast. No database round trips, no latency spikes.
Realtime, Made Simple
Update state and broadcast changes in realtime. No external pub/sub systems, no polling – just built-in low-latency events.
Store Data Near Your Users
Your state lives close to your users on the edge – not in a faraway data center – so every interaction feels instant.
Serverless & Scalable
No servers to manage. Your code runs on-demand and scales automatically with usage.
Reconsider What Your Backend Can Do
Build powerful applications with ActorCore’s comprehensive feature set.
Chat Room
AI Agent
Local-First Sync
Per-Tenant Saas
Per-User Databases
Yjs CRDT
Collaborative Document
Stream Processing
Multiplayer Game
Rate Limiter
import { actor } from "actor-core";
export type Message = { sender: string; text: string; timestamp: number; }
const chatRoom = actor({
// State is automatically persisted
state: {
messages: [] as Message[]
},
actions: {
sendMessage: (c, sender: string, text: string) => {
const message = { sender, text, timestamp: Date.now() };
// Any changes to state are automatically saved
c.state.messages.push(message);
// Broadcast events trigger real-time updates in connected clients
c.broadcast("newMessage", message);
},
getHistory: (c) => c.state.messages
}
});
export default chatRoom;
import { createClient } from "actor-core/client";
import { createReactActorCore } from "@actor-core/react";
import { useState, useEffect } from "react";
import type { App } from "../actors/app";
import type { Message } from "./actor";
const client = createClient<App>("http://localhost:6420");
const { useActor, useActorEvent } = createReactActorCore(client);
export function ChatRoom({ roomId = "general" }) {
// Connect to specific chat room using tags
const [{ actor }] = useActor("chatRoom", {
tags: { roomId }
});
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
// Load initial state
useEffect(() => {
if (actor) {
// Load chat history
actor.getHistory().then(setMessages);
}
}, [actor]);
// Listen for real-time updates from the server
useActorEvent({ actor, event: "newMessage" }, (message) => {
setMessages(prev => [...prev, message]);
});
const sendMessage = () => {
if (actor && input.trim()) {
actor.sendMessage("User", input);
setInput("");
}
};
return (
<div className="chat-container">
<div className="room-header">
<h3>Chat Room: {roomId}</h3>
</div>
<div className="messages">
{messages.length === 0 ? (
<div className="empty-message">No messages yet. Start the conversation!</div>
) : (
messages.map((msg, i) => (
<div key={i} className="message">
<b>{msg.sender}:</b> {msg.text}
<span className="timestamp">
{new Date(msg.timestamp).toLocaleTimeString()}
</span>
</div>
))
)}
</div>
<div className="input-area">
<input
value={input}
onChange={e => setInput(e.target.value)}
onKeyPress={e => e.key === "Enter" && sendMessage()}
placeholder="Type a message..."
/>
<button onClick={sendMessage}>Send</button>
</div>
</div>
);
}
import { actor } from "actor-core";
import { drizzle } from "@actor-core/drizzle";
import { messages } from "./schema";
export type Message = { sender: string; text: string; timestamp: number; }
const chatRoom = actor({
sql: drizzle(),
actions: {
sendMessage: async (c, sender: string, text: string) => {
const message = {
sender,
text,
timestamp: Date.now(),
};
// Insert the message into SQLite
await c.db.insert(messages).values(message);
// Broadcast to all connected clients
c.broadcast("newMessage", message);
// Return the created message (matches JS memory version)
return message;
},
getHistory: async (c) => {
// Query all messages ordered by timestamp
const result = await c.db
.select()
.from(messages)
.orderBy(messages.timestamp);
return result as Message[];
}
}
});
export default chatRoom;
import { createClient } from "actor-core/client";
import { createReactActorCore } from "@actor-core/react";
import { useState, useEffect } from "react";
import type { App } from "../actors/app";
import type { Message } from "./actor";
const client = createClient<App>("http://localhost:6420");
const { useActor, useActorEvent } = createReactActorCore(client);
export function ChatRoom({ roomId = "general" }) {
// Connect to specific chat room using tags
const [{ actor }] = useActor("chatRoom", {
tags: { roomId }
});
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
// Load initial state
useEffect(() => {
if (actor) {
// Load chat history
actor.getHistory().then(setMessages);
}
}, [actor]);
// Listen for real-time updates from the server
useActorEvent({ actor, event: "newMessage" }, (message) => {
setMessages(prev => [...prev, message]);
});
const sendMessage = () => {
if (actor && input.trim()) {
actor.sendMessage("User", input);
setInput("");
}
};
return (
<div className="chat-container">
<div className="room-header">
<h3>Chat Room: {roomId}</h3>
</div>
<div className="messages">
{messages.length === 0 ? (
<div className="empty-message">No messages yet. Start the conversation!</div>
) : (
messages.map((msg, i) => (
<div key={i} className="message">
<b>{msg.sender}:</b> {msg.text}
<span className="timestamp">
{new Date(msg.timestamp).toLocaleTimeString()}
</span>
</div>
))
)}
</div>
<div className="input-area">
<input
value={input}
onChange={e => setInput(e.target.value)}
onKeyPress={e => e.key === "Enter" && sendMessage()}
placeholder="Type a message..."
/>
<button onClick={sendMessage}>Send</button>
</div>
</div>
);
}
import { actor } from "actor-core";
import { generateText, tool } from "ai";
import { openai } from "@ai-sdk/openai";
import { getWeather } from "./my-utils";
export type Message = { role: "user" | "assistant"; content: string; timestamp: number; }
const aiAgent = actor({
// State is automatically persisted
state: {
messages: [] as Message[]
},
actions: {
// Get conversation history
getMessages: (c) => c.state.messages,
// Send a message to the AI and get a response
sendMessage: async (c, userMessage: string) => {
// Add user message to conversation
const userMsg: Message = {
role: "user",
content: userMessage,
timestamp: Date.now()
};
c.state.messages.push(userMsg);
// Generate AI response using Vercel AI SDK with tools
const { text } = await generateText({
model: openai("o3-mini"),
prompt: userMessage,
messages: c.state.messages,
tools: {
weather: tool({
description: 'Get the weather in a location',
parameters: {
location: {
type: 'string',
description: 'The location to get the weather for',
},
},
execute: async ({ location }) => {
return await getWeather(location);
},
}),
},
});
// Add AI response to conversation
const assistantMsg: Message = {
role: "assistant",
content: text,
timestamp: Date.now()
};
c.state.messages.push(assistantMsg);
// Broadcast the new message to all connected clients
c.broadcast("messageReceived", assistantMsg);
return assistantMsg;
},
}
});
export default aiAgent;
import { createClient } from "actor-core/client";
import { createReactActorCore } from "@actor-core/react";
import { useState, useEffect } from "react";
import type { App } from "../actors/app";
import type { Message } from "./actor";
const client = createClient<App>("http://localhost:6420");
const { useActor, useActorEvent } = createReactActorCore(client);
export function AIAssistant() {
const [{ actor }] = useActor("aiAgent", { tags: { conversationId: "default" } });
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
// Load initial messages
useEffect(() => {
if (actor) {
actor.getMessages().then(setMessages);
}
}, [actor]);
// Listen for real-time messages
useActorEvent({ actor, event: "messageReceived" }, (message) => {
setMessages(prev => [...prev, message as Message]);
setIsLoading(false);
});
const handleSendMessage = async () => {
if (actor && input.trim()) {
setIsLoading(true);
// Add user message to UI immediately
const userMessage = { role: "user", content: input } as Message;
setMessages(prev => [...prev, userMessage]);
// Send to actor (AI response will come through the event)
await actor.sendMessage(input);
setInput("");
}
};
return (
<div className="ai-chat">
<div className="messages">
{messages.length === 0 ? (
<div className="empty-message">
Ask the AI assistant a question to get started
</div>
) : (
messages.map((msg, i) => (
<div key={i} className={`message ${msg.role}`}>
<div className="avatar">
{msg.role === "user" ? "👤" : "🤖"}
</div>
<div className="content">{msg.content}</div>
</div>
))
)}
{isLoading && (
<div className="message assistant loading">
<div className="avatar">🤖</div>
<div className="content">Thinking...</div>
</div>
)}
</div>
<div className="input-area">
<input
value={input}
onChange={e => setInput(e.target.value)}
onKeyPress={e => e.key === "Enter" && handleSendMessage()}
placeholder="Ask the AI assistant..."
disabled={isLoading}
/>
<button
onClick={handleSendMessage}
disabled={isLoading || !input.trim()}
>
Send
</button>
</div>
</div>
);
}
import { actor } from "actor-core";
import { drizzle } from "@actor-core/drizzle";
import { generateText, tool } from "ai";
import { openai } from "@ai-sdk/openai";
import { getWeather } from "./my-utils";
import { messages } from "./schema";
export type Message = { role: "user" | "assistant"; content: string; timestamp: number; }
const aiAgent = actor({
sql: drizzle(),
actions: {
// Get conversation history
getMessages: async (c) => {
const result = await c.db
.select()
.from(messages)
.orderBy(messages.timestamp.asc());
return result;
},
// Send a message to the AI and get a response
sendMessage: async (c, userMessage: string) => {
const now = Date.now();
// Add user message to conversation
const userMsg = {
conversationId: c.actorId, // Use the actor instance ID
role: "user",
content: userMessage,
};
// Store user message
await c.db
.insert(messages)
.values(userMsg);
// Get all messages
const allMessages = await c.db
.select()
.from(messages)
.orderBy(messages.timestamp.asc());
// Generate AI response using Vercel AI SDK with tools
const { text } = await generateText({
model: openai("o3-mini"),
prompt: userMessage,
messages: allMessages,
tools: {
weather: tool({
description: 'Get the weather in a location',
parameters: {
location: {
type: 'string',
description: 'The location to get the weather for',
},
},
execute: async ({ location }) => {
return await getWeather(location);
},
}),
},
});
// Add AI response to conversation
const assistantMsg = {
role: "assistant",
content: text,
};
// Store assistant message
await c.db
.insert(messages)
.values(assistantMsg);
// Broadcast the new message to all connected clients
c.broadcast("messageReceived", assistantMsg);
return assistantMsg;
},
}
});
export default aiAgent;
import { createClient } from "actor-core/client";
import { createReactActorCore } from "@actor-core/react";
import { useState, useEffect } from "react";
import type { App } from "../actors/app";
import type { Message } from "./actor";
const client = createClient<App>("http://localhost:6420");
const { useActor, useActorEvent } = createReactActorCore(client);
export function AIAssistant() {
const [{ actor }] = useActor("aiAgent", { tags: { conversationId: "default" } });
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
// Load initial messages
useEffect(() => {
if (actor) {
actor.getMessages().then(setMessages);
}
}, [actor]);
// Listen for real-time messages
useActorEvent({ actor, event: "messageReceived" }, (message) => {
setMessages(prev => [...prev, message as Message]);
setIsLoading(false);
});
const handleSendMessage = async () => {
if (actor && input.trim()) {
setIsLoading(true);
// Add user message to UI immediately
const userMessage = { role: "user", content: input } as Message;
setMessages(prev => [...prev, userMessage]);
// Send to actor (AI response will come through the event)
await actor.sendMessage(input);
setInput("");
}
};
return (
<div className="ai-chat">
<div className="messages">
{messages.length === 0 ? (
<div className="empty-message">
Ask the AI assistant a question to get started
</div>
) : (
messages.map((msg, i) => (
<div key={i} className={`message ${msg.role}`}>
<div className="avatar">
{msg.role === "user" ? "👤" : "🤖"}
</div>
<div className="content">{msg.content}</div>
</div>
))
)}
{isLoading && (
<div className="message assistant loading">
<div className="avatar">🤖</div>
<div className="content">Thinking...</div>
</div>
)}
</div>
<div className="input-area">
<input
value={input}
onChange={e => setInput(e.target.value)}
onKeyPress={e => e.key === "Enter" && handleSendMessage()}
placeholder="Ask the AI assistant..."
disabled={isLoading}
/>
<button
onClick={handleSendMessage}
disabled={isLoading || !input.trim()}
>
Send
</button>
</div>
</div>
);
}
import { actor } from "actor-core";
export type Contact = { id: string; name: string; email: string; phone: string; updatedAt: number; }
const contacts = actor({
// State is automatically persisted
state: {
contacts: {}
},
actions: {
// Gets changes after the last timestamp (when coming back online)
getChanges: (c, after: number = 0) => {
const changes = Object.values(c.state.contacts)
.filter(contact => contact.updatedAt > after);
return {
changes,
timestamp: Date.now()
};
},
// Pushes new changes from the client & handles conflicts
pushChanges: (c, contacts: Contact[]) => {
let changed = false;
contacts.forEach(contact => {
const existing = c.state.contacts[contact.id];
if (!existing || existing.updatedAt < contact.updatedAt) {
c.state.contacts[contact.id] = contact;
changed = true;
}
});
if (changed) {
c.broadcast("contactsChanged", {
contacts: Object.values(c.state.contacts)
});
}
return { timestamp: Date.now() };
}
}
});
export default contacts;
import { createClient } from "actor-core/client";
import { createReactActorCore } from "@actor-core/react";
import { useState, useEffect, useRef } from "react";
import type { Contact } from "./actor";
const client = createClient("http://localhost:6420");
const { useActor, useActorEvent } = createReactActorCore(client);
export function ContactsApp() {
const { actor } = useActor("contacts");
const [contacts, setContacts] = useState<Contact[]>([]);
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [phone, setPhone] = useState("");
const [syncStatus, setSyncStatus] = useState("Idle");
const lastSyncTime = useRef(0);
// Load initial contacts
useEffect(() => {
if (!actor) return;
actor.getChanges(0).then(data => {
setContacts(data.changes);
lastSyncTime.current = data.timestamp;
setSyncStatus("Synced");
});
}, [actor]);
// Handle contact events
useActorEvent({ actor, event: "contactsChanged" }, ({ contacts: updatedContacts }) => {
setContacts(prev => {
const contactMap = new Map(prev.map(c => [c.id, c]));
updatedContacts.forEach(contact => {
const existing = contactMap.get(contact.id);
if (!existing || existing.updatedAt < contact.updatedAt) {
contactMap.set(contact.id, contact);
}
});
return Array.from(contactMap.values());
});
});
// Sync periodically
useEffect(() => {
if (!actor) return;
const sync = async () => {
setSyncStatus("Syncing...");
try {
// Get remote changes
const changes = await actor.getChanges(lastSyncTime.current);
// Apply remote changes
if (changes.changes.length > 0) {
setContacts(prev => {
const contactMap = new Map(prev.map(c => [c.id, c]));
changes.changes.forEach(contact => {
const existing = contactMap.get(contact.id);
if (!existing || existing.updatedAt < contact.updatedAt) {
contactMap.set(contact.id, contact);
}
});
return Array.from(contactMap.values());
});
}
// Push local changes
const localChanges = contacts.filter(c => c.updatedAt > lastSyncTime.current);
if (localChanges.length > 0) {
await actor.pushChanges(localChanges);
}
lastSyncTime.current = changes.timestamp;
setSyncStatus("Synced");
} catch (error) {
setSyncStatus("Offline");
}
};
const intervalId = setInterval(sync, 5000);
return () => clearInterval(intervalId);
}, [actor, contacts]);
// Add new contact (local first)
const addContact = () => {
if (!name.trim()) return;
const newContact: Contact = {
id: Date.now().toString(),
name,
email,
phone,
updatedAt: Date.now()
};
setContacts(prev => [...prev, newContact]);
if (actor) {
actor.pushChanges([newContact]);
}
setName("");
setEmail("");
setPhone("");
};
// Delete contact (implemented as update with empty name)
const deleteContact = (id: string) => {
setContacts(prev => {
const updatedContacts = prev.map(c =>
c.id === id
? { ...c, name: "", updatedAt: Date.now() }
: c
);
if (actor) {
const deleted = updatedContacts.find(c => c.id === id);
if (deleted) {
actor.pushChanges([deleted]);
}
}
return updatedContacts.filter(c => c.name !== "");
});
};
// Manual sync
const handleSync = async () => {
if (!actor) return;
setSyncStatus("Syncing...");
try {
// Push all contacts
await actor.pushChanges(contacts);
// Get all changes
const changes = await actor.getChanges(0);
setContacts(changes.changes);
lastSyncTime.current = changes.timestamp;
setSyncStatus("Synced");
} catch (error) {
setSyncStatus("Offline");
}
};
return (
<div className="contacts-app">
<div className="contacts-header">
<h2>Contacts</h2>
<div className="sync-status">
<span>{syncStatus}</span>
<button onClick={handleSync}>
Sync Now
</button>
</div>
</div>
<div className="add-contact">
<input
type="text"
placeholder="Name"
value={name}
onChange={e => setName(e.target.value)}
/>
<input
type="email"
placeholder="Email"
value={email}
onChange={e => setEmail(e.target.value)}
/>
<input
type="tel"
placeholder="Phone"
value={phone}
onChange={e => setPhone(e.target.value)}
/>
<button onClick={addContact}>Add Contact</button>
</div>
<div className="contacts-list">
{contacts.filter(c => c.name !== "").map(contact => (
<div key={contact.id} className="contact-item">
<div className="contact-info">
<div className="contact-name">{contact.name}</div>
<div className="contact-details">
<div>{contact.email}</div>
<div>{contact.phone}</div>
</div>
</div>
<button
className="delete-button"
onClick={() => deleteContact(contact.id)}
>
Delete
</button>
</div>
))}
</div>
</div>
);
}
import { actor } from "actor-core";
import { drizzle } from "@actor-core/drizzle";
import { contacts } from "./schema";
export type Contact = { id: string; name: string; email: string; phone: string; updatedAt: number; }
const contactSync = actor({
sql: drizzle(),
actions: {
// Gets changes after the last timestamp (when coming back online)
getChanges: async (c, after: number = 0) => {
const changes = await c.db
.select()
.from(contacts)
.where(contacts.updatedAt.gt(after));
return {
changes,
timestamp: Date.now()
};
},
// Pushes new changes from the client & handles conflicts
pushChanges: async (c, contactList: Contact[]) => {
let changed = false;
for (const contact of contactList) {
// Check if contact exists with a newer timestamp
const existing = await c.db
.select()
.from(contacts)
.where(contacts.id.equals(contact.id))
.get();
if (!existing || existing.updatedAt < contact.updatedAt) {
// Insert or update the contact
await c.db
.insert(contacts)
.values(contact)
.onConflictDoUpdate({
target: contacts.id,
set: contact
});
changed = true;
}
}
if (changed) {
// Get all contacts to broadcast
const allContacts = await c.db
.select()
.from(contacts);
c.broadcast("contactsChanged", {
contacts: allContacts
});
}
return { timestamp: Date.now() };
}
}
});
export default contactSync;
import { createClient } from "actor-core/client";
import { createReactActorCore } from "@actor-core/react";
import { useState, useEffect, useRef } from "react";
import type { Contact } from "./actor";
const client = createClient("http://localhost:6420");
const { useActor, useActorEvent } = createReactActorCore(client);
export function ContactsApp() {
const { actor } = useActor("contacts");
const [contacts, setContacts] = useState<Contact[]>([]);
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [phone, setPhone] = useState("");
const [syncStatus, setSyncStatus] = useState("Idle");
const lastSyncTime = useRef(0);
// Load initial contacts
useEffect(() => {
if (!actor) return;
actor.getChanges(0).then(data => {
setContacts(data.changes);
lastSyncTime.current = data.timestamp;
setSyncStatus("Synced");
});
}, [actor]);
// Handle contact events
useActorEvent({ actor, event: "contactsChanged" }, ({ contacts: updatedContacts }) => {
setContacts(prev => {
const contactMap = new Map(prev.map(c => [c.id, c]));
updatedContacts.forEach(contact => {
const existing = contactMap.get(contact.id);
if (!existing || existing.updatedAt < contact.updatedAt) {
contactMap.set(contact.id, contact);
}
});
return Array.from(contactMap.values());
});
});
// Sync periodically
useEffect(() => {
if (!actor) return;
const sync = async () => {
setSyncStatus("Syncing...");
try {
// Get remote changes
const changes = await actor.getChanges(lastSyncTime.current);
// Apply remote changes
if (changes.changes.length > 0) {
setContacts(prev => {
const contactMap = new Map(prev.map(c => [c.id, c]));
changes.changes.forEach(contact => {
const existing = contactMap.get(contact.id);
if (!existing || existing.updatedAt < contact.updatedAt) {
contactMap.set(contact.id, contact);
}
});
return Array.from(contactMap.values());
});
}
// Push local changes
const localChanges = contacts.filter(c => c.updatedAt > lastSyncTime.current);
if (localChanges.length > 0) {
await actor.pushChanges(localChanges);
}
lastSyncTime.current = changes.timestamp;
setSyncStatus("Synced");
} catch (error) {
setSyncStatus("Offline");
}
};
const intervalId = setInterval(sync, 5000);
return () => clearInterval(intervalId);
}, [actor, contacts]);
// Add new contact (local first)
const addContact = () => {
if (!name.trim()) return;
const newContact: Contact = {
id: Date.now().toString(),
name,
email,
phone,
updatedAt: Date.now()
};
setContacts(prev => [...prev, newContact]);
if (actor) {
actor.pushChanges([newContact]);
}
setName("");
setEmail("");
setPhone("");
};
// Delete contact (implemented as update with empty name)
const deleteContact = (id: string) => {
setContacts(prev => {
const updatedContacts = prev.map(c =>
c.id === id
? { ...c, name: "", updatedAt: Date.now() }
: c
);
if (actor) {
const deleted = updatedContacts.find(c => c.id === id);
if (deleted) {
actor.pushChanges([deleted]);
}
}
return updatedContacts.filter(c => c.name !== "");
});
};
// Manual sync
const handleSync = async () => {
if (!actor) return;
setSyncStatus("Syncing...");
try {
// Push all contacts
await actor.pushChanges(contacts);
// Get all changes
const changes = await actor.getChanges(0);
setContacts(changes.changes);
lastSyncTime.current = changes.timestamp;
setSyncStatus("Synced");
} catch (error) {
setSyncStatus("Offline");
}
};
return (
<div className="contacts-app">
<div className="contacts-header">
<h2>Contacts</h2>
<div className="sync-status">
<span>{syncStatus}</span>
<button onClick={handleSync}>
Sync Now
</button>
</div>
</div>
<div className="add-contact">
<input
type="text"
placeholder="Name"
value={name}
onChange={e => setName(e.target.value)}
/>
<input
type="email"
placeholder="Email"
value={email}
onChange={e => setEmail(e.target.value)}
/>
<input
type="tel"
placeholder="Phone"
value={phone}
onChange={e => setPhone(e.target.value)}
/>
<button onClick={addContact}>Add Contact</button>
</div>
<div className="contacts-list">
{contacts.filter(c => c.name !== "").map(contact => (
<div key={contact.id} className="contact-item">
<div className="contact-info">
<div className="contact-name">{contact.name}</div>
<div className="contact-details">
<div>{contact.email}</div>
<div>{contact.phone}</div>
</div>
</div>
<button
className="delete-button"
onClick={() => deleteContact(contact.id)}
>
Delete
</button>
</div>
))}
</div>
</div>
);
}
import { actor } from "actor-core";
import { authenticate } from "./my-utils";
// Simple tenant organization actor
const tenant = actor({
// Example initial state
state: {
members: [
{ id: "user-1", name: "Alice", email: "[email protected]", role: "admin" },
{ id: "user-2", name: "Bob", email: "[email protected]", role: "member" }
],
invoices: [
{ id: "inv-1", amount: 100, date: Date.now(), paid: true },
{ id: "inv-2", amount: 200, date: Date.now(), paid: false }
]
},
// Authentication
createConnState: async (c, { params }) => {
const token = params.token;
const userId = await authenticate(token);
return { userId };
},
actions: {
// Get all members
getMembers: (c) => {
return c.state.members;
},
// Get all invoices (only admin can access)
getInvoices: (c) => {
// Find the user's role by their userId
const userId = c.conn.userId;
const user = c.state.members.find(m => m.id === userId);
// Only allow admins to see invoices
if (!user || user.role !== "admin") {
throw new UserError("Permission denied: requires admin role");
}
return c.state.invoices;
}
}
});
export default tenant;
import { createClient } from "actor-core/client";
import { createReactActorCore } from "@actor-core/react";
import { useState, useEffect } from "react";
import type { App } from "../actors/app";
// Create client and hooks
const client = createClient<App>("http://localhost:6420");
const { useActor } = createReactActorCore(client);
export function OrgDashboard({ orgId }: { orgId: string }) {
// State for data
const [members, setMembers] = useState<any[]>([]);
const [invoices, setInvoices] = useState<any[]>([]);
const [error, setError] = useState("");
// Login as admin or regular user
const loginAsAdmin = () => {
setToken("auth:user-1"); // Alice is admin
};
const loginAsMember = () => {
setToken("auth:user-2"); // Bob is member
};
// Authentication token
const [token, setToken] = useState("");
// Connect to tenant actor with authentication token
const [{ actor }] = useActor("tenant", {
params: { token },
tags: { orgId }
});
// Load data when actor is available
useEffect(() => {
if (!actor || !token) return;
const loadData = async () => {
try {
// Get members (available to all users)
const membersList = await actor.getMembers();
setMembers(membersList);
// Try to get invoices (only available to admins)
try {
const invoicesList = await actor.getInvoices();
setInvoices(invoicesList);
setError("");
} catch (err: any) {
setError(err.message);
}
} catch (err) {
console.error("Failed to load data");
}
};
loadData();
}, [actor, token]);
// Login screen when not authenticated
if (!token) {
return (
<div>
<h2>Organization Dashboard</h2>
<p>Choose a login:</p>
<button onClick={loginAsAdmin}>Login as Admin (Alice)</button>
<button onClick={loginAsMember}>Login as Member (Bob)</button>
</div>
);
}
return (
<div>
<h2>Organization Dashboard</h2>
<p>Logged in as: {token.split(":")[1]}</p>
{/* Members Section - available to all users */}
<div>
<h3>Members</h3>
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Role</th>
</tr>
</thead>
<tbody>
{members.map(member => (
<tr key={member.id}>
<td>{member.name}</td>
<td>{member.email}</td>
<td>{member.role}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Invoices Section - only displayed to admins */}
<div>
<h3>Invoices</h3>
{error ? (
<div style={{ color: "red" }}>{error}</div>
) : (
<table>
<thead>
<tr>
<th>Invoice #</th>
<th>Date</th>
<th>Amount</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{invoices.map(invoice => (
<tr key={invoice.id}>
<td>{invoice.id}</td>
<td>{new Date(invoice.date).toLocaleDateString()}</td>
<td>${invoice.amount}</td>
<td>{invoice.paid ? "Paid" : "Unpaid"}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
);
}
import { actor } from "actor-core";
import { drizzle } from "@actor-core/drizzle";
import { members, invoices } from "./schema";
import { authenticate } from "./my-utils";
// Simple tenant organization actor
const tenant = actor({
sql: drizzle(),
// Authentication
createConnState: async (c, { params }) => {
const token = params.token;
const userId = await authenticate(token);
return { userId };
},
actions: {
// Get all members
getMembers: async (c) => {
const result = await c.db
.select()
.from(members);
return result;
},
// Get all invoices (only admin can access)
getInvoices: async (c) => {
// Find the user's role by their userId
const userId = c.conn.userId;
const user = await c.db
.select()
.from(members)
.where(members.id.equals(userId))
.get();
// Only allow admins to see invoices
if (!user || user.role !== "admin") {
throw new Error("Permission denied: requires admin role");
}
const result = await c.db
.select()
.from(invoices);
return result;
}
}
});
export default tenant;
import { createClient } from "actor-core/client";
import { createReactActorCore } from "@actor-core/react";
import { useState, useEffect } from "react";
import type { App } from "../actors/app";
// Create client and hooks
const client = createClient<App>("http://localhost:6420");
const { useActor } = createReactActorCore(client);
export function OrgDashboard({ orgId }: { orgId: string }) {
// State for data
const [members, setMembers] = useState<any[]>([]);
const [invoices, setInvoices] = useState<any[]>([]);
const [error, setError] = useState("");
// Login as admin or regular user
const loginAsAdmin = () => {
setToken("auth:user-1"); // Alice is admin
};
const loginAsMember = () => {
setToken("auth:user-2"); // Bob is member
};
// Authentication token
const [token, setToken] = useState("");
// Connect to tenant actor with authentication token
const [{ actor }] = useActor("tenant", {
params: { token },
tags: { orgId }
});
// Load data when actor is available
useEffect(() => {
if (!actor || !token) return;
const loadData = async () => {
try {
// Get members (available to all users)
const membersList = await actor.getMembers();
setMembers(membersList);
// Try to get invoices (only available to admins)
try {
const invoicesList = await actor.getInvoices();
setInvoices(invoicesList);
setError("");
} catch (err: any) {
setError(err.message);
}
} catch (err) {
console.error("Failed to load data");
}
};
loadData();
}, [actor, token]);
// Login screen when not authenticated
if (!token) {
return (
<div>
<h2>Organization Dashboard</h2>
<p>Choose a login:</p>
<button onClick={loginAsAdmin}>Login as Admin (Alice)</button>
<button onClick={loginAsMember}>Login as Member (Bob)</button>
</div>
);
}
return (
<div>
<h2>Organization Dashboard</h2>
<p>Logged in as: {token.split(":")[1]}</p>
{/* Members Section - available to all users */}
<div>
<h3>Members</h3>
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Role</th>
</tr>
</thead>
<tbody>
{members.map(member => (
<tr key={member.id}>
<td>{member.name}</td>
<td>{member.email}</td>
<td>{member.role}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Invoices Section - only displayed to admins */}
<div>
<h3>Invoices</h3>
{error ? (
<div style={{ color: "red" }}>{error}</div>
) : (
<table>
<thead>
<tr>
<th>Invoice #</th>
<th>Date</th>
<th>Amount</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{invoices.map(invoice => (
<tr key={invoice.id}>
<td>{invoice.id}</td>
<td>{new Date(invoice.date).toLocaleDateString()}</td>
<td>${invoice.amount}</td>
<td>{invoice.paid ? "Paid" : "Unpaid"}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
);
}
import { actor } from "actor-core";
import { authenticate } from "./my-utils";
export type Note = { id: string; content: string; updatedAt: number };
// User notes actor
const notes = actor({
state: {
notes: [] as Note[]
},
// Authenticate
createConnState: async (c, { params }) => {
const token = params.token;
const userId = await authenticate(token);
return { userId };
},
actions: {
// Get all notes
getNotes: (c) => c.state.notes,
// Update note or create if it doesn't exist
updateNote: (c, { id, content }) => {
const noteIndex = c.state.notes.findIndex(note => note.id === id);
let note;
if (noteIndex >= 0) {
// Update existing note
note = c.state.notes[noteIndex];
note.content = content;
note.updatedAt = Date.now();
c.broadcast("noteUpdated", note);
} else {
// Create new note
note = {
id: id || `note-${Date.now()}`,
content,
updatedAt: Date.now()
};
c.state.notes.push(note);
c.broadcast("noteAdded", note);
}
return note;
},
// Delete note
deleteNote: (c, { id }) => {
const noteIndex = c.state.notes.findIndex(note => note.id === id);
if (noteIndex >= 0) {
c.state.notes.splice(noteIndex, 1);
c.broadcast("noteDeleted", { id });
}
}
}
});
export default notes;
import { createClient } from "actor-core/client";
import { createReactActorCore } from "@actor-core/react";
import { useState, useEffect } from "react";
const client = createClient("http://localhost:6420");
const { useActor, useActorEvent } = createReactActorCore(client);
export function NotesApp({ userId }: { userId: string }) {
const [notes, setNotes] = useState<Array<{ id: string, content: string }>>([]);
const [newNote, setNewNote] = useState("");
// Connect to actor with auth token
const [{ actor }] = useActor("notes", {
params: { userId, token: "demo-token" }
});
// Load initial notes
useEffect(() => {
if (actor) {
actor.getNotes().then(setNotes);
}
}, [actor]);
// Add a new note
const addNote = async () => {
if (actor && newNote.trim()) {
await actor.updateNote({ id: `note-${Date.now()}`, content: newNote });
setNewNote("");
}
};
// Delete a note
const deleteNote = (id: string) => {
if (actor) {
actor.deleteNote({ id });
}
};
// Listen for realtime updates
useActorEvent({ actor, event: "noteAdded" }, (note) => {
setNotes(notes => [...notes, note]);
});
useActorEvent({ actor, event: "noteUpdated" }, (updatedNote) => {
setNotes(notes => notes.map(note =>
note.id === updatedNote.id ? updatedNote : note
));
});
useActorEvent({ actor, event: "noteDeleted" }, ({ id }) => {
setNotes(notes => notes.filter(note => note.id !== id));
});
return (
<div>
<h2>My Notes</h2>
<div>
<input
value={newNote}
onChange={e => setNewNote(e.target.value)}
placeholder="Enter a new note"
/>
<button onClick={addNote}>Add</button>
</div>
<ul>
{notes.map(note => (
<li key={note.id}>
{note.content}
<button onClick={() => deleteNote(note.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
import { actor } from "actor-core";
import { drizzle } from "@actor-core/drizzle";
import { notes } from "./schema";
import { authenticate } from "./my-utils";
export type Note = { id: string; content: string; updatedAt: number };
// User notes actor
const userNotes = actor({
sql: drizzle(),
// Authenticate
createConnState: async (c, { params }) => {
const token = params.token;
const userId = await authenticate(token);
return { userId };
},
actions: {
// Get all notes
getNotes: async (c) => {
const result = await c.db
.select()
.from(notes);
return result;
},
// Update note or create if it doesn't exist
updateNote: async (c, { id, content }) => {
// Ensure the note ID exists or create a new one
const noteId = id || `note-${Date.now()}`;
// Check if note exists
const existingNote = await c.db
.select()
.from(notes)
.where(notes.id.equals(noteId))
.get();
if (existingNote) {
// Update existing note
await c.db
.update(notes)
.set({
content
})
.where(notes.id.equals(noteId));
const updatedNote = {
id: noteId,
content
};
c.broadcast("noteUpdated", updatedNote);
return updatedNote;
} else {
// Create new note
const newNote = {
id: noteId,
content
};
await c.db
.insert(notes)
.values(newNote);
c.broadcast("noteAdded", newNote);
return newNote;
}
},
// Delete note
deleteNote: async (c, { id }) => {
// Delete the note
await c.db
.delete(notes)
.where(notes.id.equals(id));
c.broadcast("noteDeleted", { id });
}
}
});
export default userNotes;
import { createClient } from "actor-core/client";
import { createReactActorCore } from "@actor-core/react";
import { useState, useEffect } from "react";
const client = createClient("http://localhost:6420");
const { useActor, useActorEvent } = createReactActorCore(client);
export function NotesApp({ userId }: { userId: string }) {
const [notes, setNotes] = useState<Array<{ id: string, content: string }>>([]);
const [newNote, setNewNote] = useState("");
// Connect to actor with auth token
const [{ actor }] = useActor("notes", {
params: { userId, token: "demo-token" }
});
// Load initial notes
useEffect(() => {
if (actor) {
actor.getNotes().then(setNotes);
}
}, [actor]);
// Add a new note
const addNote = async () => {
if (actor && newNote.trim()) {
await actor.updateNote({ id: `note-${Date.now()}`, content: newNote });
setNewNote("");
}
};
// Delete a note
const deleteNote = (id: string) => {
if (actor) {
actor.deleteNote({ id });
}
};
// Listen for realtime updates
useActorEvent({ actor, event: "noteAdded" }, (note) => {
setNotes(notes => [...notes, note]);
});
useActorEvent({ actor, event: "noteUpdated" }, (updatedNote) => {
setNotes(notes => notes.map(note =>
note.id === updatedNote.id ? updatedNote : note
));
});
useActorEvent({ actor, event: "noteDeleted" }, ({ id }) => {
setNotes(notes => notes.filter(note => note.id !== id));
});
return (
<div>
<h2>My Notes</h2>
<div>
<input
value={newNote}
onChange={e => setNewNote(e.target.value)}
placeholder="Enter a new note"
/>
<button onClick={addNote}>Add</button>
</div>
<ul>
{notes.map(note => (
<li key={note.id}>
{note.content}
<button onClick={() => deleteNote(note.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
import { actor } from "actor-core";
import * as Y from 'yjs';
import { encodeStateAsUpdate, applyUpdate } from 'yjs';
const yjsDocument = actor({
// State: just the serialized Yjs document data
state: {
docData: "", // Base64 encoded Yjs document
lastModified: 0
},
// In-memory Yjs objects (not serialized)
createVars: () => ({
doc: new Y.Doc()
}),
// Initialize document from state when actor starts
onStart: (c) => {
if (c.state.docData) {
const binary = atob(c.state.docData);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
applyUpdate(c.vars.doc, bytes);
}
},
// Handle client connections
onConnect: (c) => {
// Send initial document state to client
const update = encodeStateAsUpdate(c.vars.doc);
const base64 = bufferToBase64(update);
c.conn.send("initialState", { update: base64 });
},
actions: {
// Apply a Yjs update from a client
applyUpdate: (c, updateBase64: string) => {
// Convert base64 to binary
const binary = atob(updateBase64);
const update = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
update[i] = binary.charCodeAt(i);
}
// Apply update to Yjs document
applyUpdate(c.vars.doc, update);
// Save document state
const fullState = encodeStateAsUpdate(c.vars.doc);
c.state.docData = bufferToBase64(fullState);
c.state.lastModified = Date.now();
// Broadcast to all clients
c.broadcast("update", { update: updateBase64 });
}
}
});
// Helper to convert ArrayBuffer to base64
function bufferToBase64(buffer: Uint8Array): string {
let binary = '';
for (let i = 0; i < buffer.byteLength; i++) {
binary += String.fromCharCode(buffer[i]);
}
return btoa(binary);
}
export default yjsDocument;
import { createClient } from "actor-core/client";
import { createReactActorCore } from "@actor-core/react";
import { useState, useEffect, useRef } from "react";
import * as Y from 'yjs';
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
import type { App } from "../actors/app";
const client = createClient<App>("http://localhost:6420");
const { useActor, useActorEvent } = createReactActorCore(client);
export function YjsEditor({ documentId = "shared-doc" }) {
// Connect to specific document using tags
const { actor } = useActor("yjsDocument", {
tags: { documentId }
});
// Document state
const [isLoading, setIsLoading] = useState(true);
const [text, setText] = useState("");
// Local Yjs document
const yDocRef = useRef<Y.Doc | null>(null);
// Flag to prevent infinite update loops
const updatingFromServer = useRef(false);
const updatingFromLocal = useRef(false);
// Track if we've initialized observation
const observationInitialized = useRef(false);
// Initialize local Yjs document and connect
useEffect(() => {
// Create Yjs document
const yDoc = new Y.Doc();
yDocRef.current = yDoc;
setIsLoading(false);
return () => {
// Clean up Yjs document
yDoc.destroy();
};
}, [actor]);
// Set up text observation
useEffect(() => {
const yDoc = yDocRef.current;
if (!yDoc || observationInitialized.current) return;
// Get the Yjs Text type from the document
const yText = yDoc.getText('content');
// Observe changes to the text
yText.observe(() => {
// Only update UI if change wasn't from server
if (!updatingFromServer.current) {
// Update React state
setText(yText.toString());
if (actor && !updatingFromLocal.current) {
// Set flag to prevent loops
updatingFromLocal.current = true;
// Convert update to base64 and send to server
const update = encodeStateAsUpdate(yDoc);
const base64 = bufferToBase64(update);
actor.applyUpdate(base64).finally(() => {
updatingFromLocal.current = false;
});
}
}
});
observationInitialized.current = true;
}, [actor]);
// Handle initial state from server
useActorEvent({ actor, event: "initialState" }, ({ update }) => {
const yDoc = yDocRef.current;
if (!yDoc) return;
// Set flag to prevent update loops
updatingFromServer.current = true;
try {
// Convert base64 to binary and apply to document
const binary = atob(update);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
// Apply update to Yjs document
applyUpdate(yDoc, bytes);
// Update React state
const yText = yDoc.getText('content');
setText(yText.toString());
} catch (error) {
console.error("Error applying initial update:", error);
} finally {
updatingFromServer.current = false;
}
});
// Handle updates from other clients
useActorEvent({ actor, event: "update" }, ({ update }) => {
const yDoc = yDocRef.current;
if (!yDoc) return;
// Set flag to prevent update loops
updatingFromServer.current = true;
try {
// Convert base64 to binary and apply to document
const binary = atob(update);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
// Apply update to Yjs document
applyUpdate(yDoc, bytes);
// Update React state
const yText = yDoc.getText('content');
setText(yText.toString());
} catch (error) {
console.error("Error applying update:", error);
} finally {
updatingFromServer.current = false;
}
});
// Handle text changes from user
const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
if (!yDocRef.current) return;
const newText = e.target.value;
const yText = yDocRef.current.getText('content');
// Only update if text actually changed
if (newText !== yText.toString()) {
// Set flag to avoid loops
updatingFromLocal.current = true;
// Update Yjs document (this will trigger observe callback)
yDocRef.current.transact(() => {
yText.delete(0, yText.length);
yText.insert(0, newText);
});
updatingFromLocal.current = false;
}
};
if (isLoading) {
return <div className="loading">Loading collaborative document...</div>;
}
return (
<div className="yjs-editor">
<h3>Collaborative Document: {documentId}</h3>
<textarea
value={text}
onChange={handleTextChange}
placeholder="Start typing... All changes are synchronized in real-time!"
className="collaborative-textarea"
/>
</div>
);
}
// Helper to convert ArrayBuffer to base64
function bufferToBase64(buffer: Uint8Array): string {
let binary = '';
for (let i = 0; i < buffer.byteLength; i++) {
binary += String.fromCharCode(buffer[i]);
}
return btoa(binary);
}
import { actor } from "actor-core";
import { drizzle } from "@actor-core/drizzle";
import * as Y from 'yjs';
import { encodeStateAsUpdate, applyUpdate } from 'yjs';
import { documents } from "./schema";
const yjsDocument = actor({
sql: drizzle(),
// In-memory Yjs objects (not serialized)
createVars: () => ({
doc: new Y.Doc()
}),
// Initialize document from state when actor starts
onStart: async (c) => {
// Get document data from database
const documentData = await c.db
.select()
.from(documents)
.get();
if (documentData?.docData) {
try {
// Parse the docData from string to binary
const binary = atob(documentData.docData);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
applyUpdate(c.vars.doc, bytes);
} catch (error) {
console.error("Failed to load document", error);
}
}
},
// Handle client connections
onConnect: (c) => {
// Send initial document state to client
const update = encodeStateAsUpdate(c.vars.doc);
const base64 = bufferToBase64(update);
c.conn.send("initialState", { update: base64 });
},
actions: {
// Apply a Yjs update from a client
applyUpdate: async (c, updateBase64: string) => {
try {
// Convert base64 to binary
const binary = atob(updateBase64);
const update = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
update[i] = binary.charCodeAt(i);
}
// Apply update to Yjs document
applyUpdate(c.vars.doc, update);
// Save document state to database
const fullState = encodeStateAsUpdate(c.vars.doc);
const docData = bufferToBase64(fullState);
// Store in database
await c.db
.insert(documents)
.values({
docData
})
.onConflictDoUpdate({
target: documents.id,
set: {
docData
}
});
// Broadcast to all clients
c.broadcast("update", { update: updateBase64 });
} catch (error) {
console.error("Failed to apply update", error);
}
}
}
});
// Helper to convert ArrayBuffer to base64
function bufferToBase64(buffer: Uint8Array): string {
let binary = '';
for (let i = 0; i < buffer.byteLength; i++) {
binary += String.fromCharCode(buffer[i]);
}
return btoa(binary);
}
export default yjsDocument;
import { createClient } from "actor-core/client";
import { createReactActorCore } from "@actor-core/react";
import { useState, useEffect, useRef } from "react";
import * as Y from 'yjs';
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
import type { App } from "../actors/app";
const client = createClient<App>("http://localhost:6420");
const { useActor, useActorEvent } = createReactActorCore(client);
export function YjsEditor({ documentId = "shared-doc" }) {
// Connect to specific document using tags
const { actor } = useActor("yjsDocument", {
tags: { documentId }
});
// Document state
const [isLoading, setIsLoading] = useState(true);
const [text, setText] = useState("");
// Local Yjs document
const yDocRef = useRef<Y.Doc | null>(null);
// Flag to prevent infinite update loops
const updatingFromServer = useRef(false);
const updatingFromLocal = useRef(false);
// Track if we've initialized observation
const observationInitialized = useRef(false);
// Initialize local Yjs document and connect
useEffect(() => {
// Create Yjs document
const yDoc = new Y.Doc();
yDocRef.current = yDoc;
setIsLoading(false);
return () => {
// Clean up Yjs document
yDoc.destroy();
};
}, [actor]);
// Set up text observation
useEffect(() => {
const yDoc = yDocRef.current;
if (!yDoc || observationInitialized.current) return;
// Get the Yjs Text type from the document
const yText = yDoc.getText('content');
// Observe changes to the text
yText.observe(() => {
// Only update UI if change wasn't from server
if (!updatingFromServer.current) {
// Update React state
setText(yText.toString());
if (actor && !updatingFromLocal.current) {
// Set flag to prevent loops
updatingFromLocal.current = true;
// Convert update to base64 and send to server
const update = encodeStateAsUpdate(yDoc);
const base64 = bufferToBase64(update);
actor.applyUpdate(base64).finally(() => {
updatingFromLocal.current = false;
});
}
}
});
observationInitialized.current = true;
}, [actor]);
// Handle initial state from server
useActorEvent({ actor, event: "initialState" }, ({ update }) => {
const yDoc = yDocRef.current;
if (!yDoc) return;
// Set flag to prevent update loops
updatingFromServer.current = true;
try {
// Convert base64 to binary and apply to document
const binary = atob(update);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
// Apply update to Yjs document
applyUpdate(yDoc, bytes);
// Update React state
const yText = yDoc.getText('content');
setText(yText.toString());
} catch (error) {
console.error("Error applying initial update:", error);
} finally {
updatingFromServer.current = false;
}
});
// Handle updates from other clients
useActorEvent({ actor, event: "update" }, ({ update }) => {
const yDoc = yDocRef.current;
if (!yDoc) return;
// Set flag to prevent update loops
updatingFromServer.current = true;
try {
// Convert base64 to binary and apply to document
const binary = atob(update);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
// Apply update to Yjs document
applyUpdate(yDoc, bytes);
// Update React state
const yText = yDoc.getText('content');
setText(yText.toString());
} catch (error) {
console.error("Error applying update:", error);
} finally {
updatingFromServer.current = false;
}
});
// Handle text changes from user
const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
if (!yDocRef.current) return;
const newText = e.target.value;
const yText = yDocRef.current.getText('content');
// Only update if text actually changed
if (newText !== yText.toString()) {
// Set flag to avoid loops
updatingFromLocal.current = true;
// Update Yjs document (this will trigger observe callback)
yDocRef.current.transact(() => {
yText.delete(0, yText.length);
yText.insert(0, newText);
});
updatingFromLocal.current = false;
}
};
if (isLoading) {
return <div className="loading">Loading collaborative document...</div>;
}
return (
<div className="yjs-editor">
<h3>Collaborative Document: {documentId}</h3>
<textarea
value={text}
onChange={handleTextChange}
placeholder="Start typing... All changes are synchronized in real-time!"
className="collaborative-textarea"
/>
</div>
);
}
// Helper to convert ArrayBuffer to base64
function bufferToBase64(buffer: Uint8Array): string {
let binary = '';
for (let i = 0; i < buffer.byteLength; i++) {
binary += String.fromCharCode(buffer[i]);
}
return btoa(binary);
}
import { actor } from "actor-core";
export type Cursor = { x: number, y: number, userId: string };
const document = actor({
state: {
text: "",
cursors: {} as Record<string, Cursor>,
},
actions: {
getText: (c) => c.state.text,
// Update the document (real use case has better conflict resolution)
setText: (c, text: string) => {
// Save document state
c.state.text = text;
// Broadcast update
c.broadcast("textUpdated", {
text,
userId: c.conn.id
});
},
getCursors: (c) => c.state.cursors,
updateCursor: (c, x: number, y: number) => {
// Update user location
const userId = c.conn.id;
c.state.cursors[userId] = { x, y, userId };
// Broadcast location
c.broadcast("cursorUpdated", {
userId,
x,
y
});
},
}
});
export default document;
import { createClient } from "actor-core/client";
import { createReactActorCore } from "@actor-core/react";
import { useState, useEffect } from "react";
import type { App } from "../actors/app";
const client = createClient<App>("http://localhost:6420");
const { useActor, useActorEvent } = createReactActorCore(client);
export function DocumentEditor() {
// Connect to actor for this document ID from URL
const documentId = new URLSearchParams(window.location.search).get('id') || 'default-doc';
const [{ actor, connectionId }] = useActor("document", { tags: { id: documentId } });
// Local state
const [text, setText] = useState("");
const [cursorPos, setCursorPos] = useState({ x: 0, y: 0 });
const [otherCursors, setOtherCursors] = useState({});
// Load initial document state
useEffect(() => {
if (actor) {
actor.getText().then(setText);
actor.getCursors().then(setOtherCursors);
}
}, [actor]);
// Listen for updates from other users
useActorEvent({ actor, event: "textUpdated" }, ({ text: newText, userId: senderId }) => {
if (senderId !== connectionId) setText(newText);
});
useActorEvent({ actor, event: "cursorUpdated" }, ({ userId: cursorUserId, x, y }) => {
if (cursorUserId !== connectionId) {
setOtherCursors(prev => ({
...prev,
[cursorUserId]: { x, y, userId: cursorUserId }
}));
}
});
// Update cursor position
const updateCursor = (e) => {
if (!actor) return;
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
if (x !== cursorPos.x || y !== cursorPos.y) {
setCursorPos({ x, y });
actor.updateCursor(x, y);
}
};
return (
<div className="document-editor">
<h2>Document: {documentId}</h2>
<div onMouseMove={updateCursor}>
<textarea
value={text}
onChange={(e) => {
const newText = e.target.value;
setText(newText);
actor?.setText(newText);
}}
placeholder="Start typing..."
/>
{/* Other users' cursors */}
{Object.values(otherCursors).map((cursor: any) => (
<div
key={cursor.userId}
style={{
position: 'absolute',
left: `${cursor.x}px`,
top: `${cursor.y}px`,
width: '10px',
height: '10px',
backgroundColor: 'red',
borderRadius: '50%'
}}
/>
))}
</div>
<div>
<p>Connected users: You and {Object.keys(otherCursors).length} others</p>
</div>
</div>
);
}
import { actor } from "actor-core";
import { drizzle } from "@actor-core/drizzle";
import { documents, cursors } from "./schema";
export type Cursor = { x: number, y: number, userId: string };
const document = actor({
sql: drizzle(),
actions: {
getText: async (c) => {
const doc = await c.db
.select()
.from(documents)
.get();
return doc?.text || "";
},
// Update the document (real use case has better conflict resolution)
setText: async (c, text: string) => {
// Save document state
await c.db
.insert(documents)
.values({
text
})
.onConflictDoUpdate({
target: documents.id,
set: {
text
}
});
// Broadcast update
c.broadcast("textUpdated", {
text,
userId: c.conn.id
});
},
getCursors: async (c) => {
const result = await c.db
.select()
.from(cursors);
// Convert array to record object keyed by userId
return result.reduce((acc, cursor) => {
acc[cursor.userId] = {
x: cursor.x,
y: cursor.y,
userId: cursor.userId
};
return acc;
}, {} as Record<string, Cursor>);
},
updateCursor: async (c, x: number, y: number) => {
// Update user location
const userId = c.conn.id;
await c.db
.insert(cursors)
.values({
userId,
x,
y
})
.onConflictDoUpdate({
target: cursors.userId,
set: {
x,
y
}
});
// Broadcast location
c.broadcast("cursorUpdated", {
userId,
x,
y
});
},
}
});
export default document;
import { createClient } from "actor-core/client";
import { createReactActorCore } from "@actor-core/react";
import { useState, useEffect } from "react";
import type { App } from "../actors/app";
const client = createClient<App>("http://localhost:6420");
const { useActor, useActorEvent } = createReactActorCore(client);
export function DocumentEditor() {
// Connect to actor for this document ID from URL
const documentId = new URLSearchParams(window.location.search).get('id') || 'default-doc';
const [{ actor, connectionId }] = useActor("document", { tags: { id: documentId } });
// Local state
const [text, setText] = useState("");
const [cursorPos, setCursorPos] = useState({ x: 0, y: 0 });
const [otherCursors, setOtherCursors] = useState({});
// Load initial document state
useEffect(() => {
if (actor) {
actor.getText().then(setText);
actor.getCursors().then(setOtherCursors);
}
}, [actor]);
// Listen for updates from other users
useActorEvent({ actor, event: "textUpdated" }, ({ text: newText, userId: senderId }) => {
if (senderId !== connectionId) setText(newText);
});
useActorEvent({ actor, event: "cursorUpdated" }, ({ userId: cursorUserId, x, y }) => {
if (cursorUserId !== connectionId) {
setOtherCursors(prev => ({
...prev,
[cursorUserId]: { x, y, userId: cursorUserId }
}));
}
});
// Update cursor position
const updateCursor = (e) => {
if (!actor) return;
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
if (x !== cursorPos.x || y !== cursorPos.y) {
setCursorPos({ x, y });
actor.updateCursor(x, y);
}
};
return (
<div className="document-editor">
<h2>Document: {documentId}</h2>
<div onMouseMove={updateCursor}>
<textarea
value={text}
onChange={(e) => {
const newText = e.target.value;
setText(newText);
actor?.setText(newText);
}}
placeholder="Start typing..."
/>
{/* Other users' cursors */}
{Object.values(otherCursors).map((cursor: any) => (
<div
key={cursor.userId}
style={{
position: 'absolute',
left: `${cursor.x}px`,
top: `${cursor.y}px`,
width: '10px',
height: '10px',
backgroundColor: 'red',
borderRadius: '50%'
}}
/>
))}
</div>
<div>
<p>Connected users: You and {Object.keys(otherCursors).length} others</p>
</div>
</div>
);
}
import { actor } from "actor-core";
export type StreamState = {
topValues: number[];
};
// Simple top-K stream processor example
const streamProcessor = actor({
state: {
topValues: [] as number[]
},
actions: {
getTopValues: (c) => c.state.topValues,
// Add value and keep top 3
addValue: (c, value: number) => {
// Insert new value if needed
const insertAt = c.state.topValues.findIndex(v => value > v);
if (insertAt === -1) {
c.state.topValues.push(value);
} else {
c.state.topValues.splice(insertAt, 0, value);
}
// Keep only top 3
if (c.state.topValues.length > 3) {
c.state.topValues.length = 3;
}
// Broadcast update to all clients
c.broadcast("updated", { topValues: c.state.topValues });
return c.state.topValues;
},
}
});
export default streamProcessor;
import { createClient } from "actor-core/client";
import { createReactActorCore } from "@actor-core/react";
import { useState, useEffect } from "react";
import type { App } from "../actors/app";
import type { StreamState } from "./actor"; // Import shared types from actor
const client = createClient<App>("http://localhost:6420");
const { useActor, useActorEvent } = createReactActorCore(client);
export function StreamExample() {
const [{ actor }] = useActor("streamProcessor");
const [topValues, setTopValues] = useState<number[]>([]);
const [newValue, setNewValue] = useState<number>(0);
// Load initial values
useEffect(() => {
if (actor) {
actor.getTopValues().then(setTopValues);
}
}, [actor]);
// Listen for updates from other clients
useActorEvent({ actor, event: "updated" }, ({ topValues }) => {
setTopValues(topValues);
});
// Add a new value to the stream
const handleAddValue = () => {
if (actor) {
actor.addValue(newValue).then(setTopValues);
setNewValue(0);
}
};
return (
<div>
<h2>Top 3 Values</h2>
<ul>
{topValues.map((value, i) => (
<li key={i}>{value}</li>
))}
</ul>
<input
type="number"
value={newValue}
onChange={(e) => setNewValue(Number(e.target.value))}
/>
<button onClick={handleAddValue}>Add Value</button>
</div>
);
}
import { actor } from "actor-core";
import { drizzle } from "@actor-core/drizzle";
import { streams, streamValues } from "./schema";
export type StreamState = { topValues: number[]; };
// Simple top-K stream processor example
const streamProcessor = actor({
sql: drizzle(),
actions: {
getTopValues: async (c) => {
// Get the top 3 values sorted in descending order
const result = await c.db
.select()
.from(streamValues)
.orderBy(streamValues.value.desc())
.limit(3);
return result.map(r => r.value);
},
// Add value and keep top 3
addValue: async (c, value: number) => {
// Insert the new value
await c.db
.insert(streamValues)
.values({
value
});
// Get the updated top 3 values
const topValues = await c.db
.select()
.from(streamValues)
.orderBy(streamValues.value.desc())
.limit(3);
// Delete values that are no longer in the top 3
if (topValues.length === 3) {
await c.db
.delete(streamValues)
.where(streamValues.value.lt(topValues[2].value));
}
const topValuesArray = topValues.map(r => r.value);
// Broadcast update to all clients
c.broadcast("updated", { topValues: topValuesArray });
return topValuesArray;
},
}
});
export default streamProcessor;
import { createClient } from "actor-core/client";
import { createReactActorCore } from "@actor-core/react";
import { useState, useEffect } from "react";
import type { App } from "../actors/app";
import type { StreamState } from "./actor"; // Import shared types from actor
const client = createClient<App>("http://localhost:6420");
const { useActor, useActorEvent } = createReactActorCore(client);
export function StreamExample() {
const [{ actor }] = useActor("streamProcessor");
const [topValues, setTopValues] = useState<number[]>([]);
const [newValue, setNewValue] = useState<number>(0);
// Load initial values
useEffect(() => {
if (actor) {
actor.getTopValues().then(setTopValues);
}
}, [actor]);
// Listen for updates from other clients
useActorEvent({ actor, event: "updated" }, ({ topValues }) => {
setTopValues(topValues);
});
// Add a new value to the stream
const handleAddValue = () => {
if (actor) {
actor.addValue(newValue).then(setTopValues);
setNewValue(0);
}
};
return (
<div>
<h2>Top 3 Values</h2>
<ul>
{topValues.map((value, i) => (
<li key={i}>{value}</li>
))}
</ul>
<input
type="number"
value={newValue}
onChange={(e) => setNewValue(Number(e.target.value))}
/>
<button onClick={handleAddValue}>Add Value</button>
</div>
);
}
import { actor } from "actor-core";
export type Position = { x: number; y: number };
export type Input = { x: number; y: number };
export type Player = { id: string; position: Position; input: Input };
const gameRoom = actor({
state: {
players: {} as Record<string, Player>,
mapSize: 800
},
onStart: (c) => {
// Set up game update loop
setInterval(() => {
const worldUpdate = { playerList: [] };
for (const id in c.state.players) {
const player = c.state.players[id];
const speed = 5;
// Update position based on input
player.position.x += player.input.x * speed;
player.position.y += player.input.y * speed;
// Keep player in bounds
player.position.x = Math.max(0, Math.min(player.position.x, c.state.mapSize));
player.position.y = Math.max(0, Math.min(player.position.y, c.state.mapSize));
// Add to list for broadcast
worldUpdate.playerList.push(player);
}
// Broadcast world state
c.broadcast("worldUpdate", worldUpdate);
}, 50);
},
// Add player to game
onConnect: (c) => {
const id = c.conn.id;
c.state.players[id] = {
id,
position: {
x: Math.floor(Math.random() * c.state.mapSize),
y: Math.floor(Math.random() * c.state.mapSize)
},
input: { x: 0, y: 0 }
};
},
// Remove player from game
onDisconnect: (c) => {
delete c.state.players[c.conn.id];
},
actions: {
// Update movement
setInput: (c, input: Input) => {
const player = c.state.players[c.conn.id];
if (player) player.input = input;
}
}
});
export default gameRoom;
import { createClient } from "actor-core/client";
import { createReactActorCore } from "@actor-core/react";
import { useState, useEffect, useRef } from "react";
import type { Player } from "./actor";
const client = createClient("http://localhost:6420");
const { useActor, useActorEvent } = createReactActorCore(client);
export function MultiplayerGame() {
const [{ actor, connectionId }] = useActor("gameRoom");
const [players, setPlayers] = useState<Player[]>([]);
const canvasRef = useRef<HTMLCanvasElement>(null);
const keysPressed = useRef<Record<string, boolean>>({});
// Set up game
useEffect(() => {
if (!actor) return;
// Set up keyboard handlers
const handleKeyDown = (e: KeyboardEvent) => {
keysPressed.current[e.key.toLowerCase()] = true;
};
const handleKeyUp = (e: KeyboardEvent) => {
keysPressed.current[e.key.toLowerCase()] = false;
};
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
// Input update loop
const inputInterval = setInterval(() => {
const input = { x: 0, y: 0 };
if (keysPressed.current["w"] || keysPressed.current["arrowup"]) input.y = -1;
if (keysPressed.current["s"] || keysPressed.current["arrowdown"]) input.y = 1;
if (keysPressed.current["a"] || keysPressed.current["arrowleft"]) input.x = -1;
if (keysPressed.current["d"] || keysPressed.current["arrowright"]) input.x = 1;
actor.setInput(input);
}, 50);
// Rendering loop
const renderLoop = () => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Use for loop instead of forEach
for (let i = 0; i < players.length; i++) {
const player = players[i];
ctx.fillStyle = player.id === connectionId ? "blue" : "gray";
ctx.beginPath();
ctx.arc(player.position.x, player.position.y, 10, 0, Math.PI * 2);
ctx.fill();
}
requestAnimationFrame(renderLoop);
};
const animationId = requestAnimationFrame(renderLoop);
return () => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("keyup", handleKeyUp);
clearInterval(inputInterval);
cancelAnimationFrame(animationId);
};
}, [actor, connectionId, players]);
// Listen for world updates
useActorEvent({ actor, event: "worldUpdate" }, ({ players: updatedPlayers }) => {
setPlayers(updatedPlayers);
});
return (
<div>
<canvas
ref={canvasRef}
width={800}
height={600}
style={{ border: "1px solid black" }}
/>
<p>Move: WASD or Arrow Keys</p>
</div>
);
}
import { actor } from "actor-core";
import { drizzle } from "@actor-core/drizzle";
import { players, gameSettings } from "./schema";
export type Position = { x: number; y: number };
export type Input = { x: number; y: number };
export type Player = { id: string; position: Position; input: Input };
const gameRoom = actor({
sql: drizzle(),
// Store game settings and player inputs in memory for performance
createVars: () => ({
playerCache: {} as Record<string, Player>,
mapSize: 800
}),
onStart: async (c) => {
// Get or initialize game settings
const settings = await c.db
.select()
.from(gameSettings)
.get();
if (settings) {
c.vars.mapSize = settings.mapSize;
} else {
await c.db
.insert(gameSettings)
.values({
mapSize: c.vars.mapSize
});
}
// Load existing players into memory
const existingPlayers = await c.db
.select()
.from(players);
for (const player of existingPlayers) {
c.vars.playerCache[player.id] = {
id: player.id,
position: {
x: player.positionX,
y: player.positionY
},
input: {
x: player.inputX,
y: player.inputY
}
};
}
// Set up game update loop
setInterval(async () => {
const worldUpdate = { playerList: [] };
let changed = false;
for (const id in c.vars.playerCache) {
const player = c.vars.playerCache[id];
const speed = 5;
// Update position based on input
player.position.x += player.input.x * speed;
player.position.y += player.input.y * speed;
// Keep player in bounds
player.position.x = Math.max(0, Math.min(player.position.x, c.vars.mapSize));
player.position.y = Math.max(0, Math.min(player.position.y, c.vars.mapSize));
// Add to list for broadcast
worldUpdate.playerList.push(player);
changed = true;
}
// Save player positions to database if changed
if (changed) {
for (const id in c.vars.playerCache) {
const player = c.vars.playerCache[id];
await c.db
.update(players)
.set({
positionX: player.position.x,
positionY: player.position.y
})
.where(players.id.equals(id));
}
// Broadcast world state
c.broadcast("worldUpdate", worldUpdate);
}
}, 50);
},
// Add player to game
onConnect: async (c) => {
const id = c.conn.id;
const randomX = Math.floor(Math.random() * c.vars.mapSize);
const randomY = Math.floor(Math.random() * c.vars.mapSize);
// Create player in memory cache
c.vars.playerCache[id] = {
id,
position: {
x: randomX,
y: randomY
},
input: { x: 0, y: 0 }
};
// Save player to database
await c.db
.insert(players)
.values({
id,
positionX: randomX,
positionY: randomY,
inputX: 0,
inputY: 0
})
.onConflictDoUpdate({
target: players.id,
set: {
positionX: randomX,
positionY: randomY,
inputX: 0,
inputY: 0
}
});
},
// Remove player from game
onDisconnect: async (c) => {
const id = c.conn.id;
// Remove from memory cache
delete c.vars.playerCache[id];
// Remove from database
await c.db
.delete(players)
.where(players.id.equals(id));
},
actions: {
// Update movement
setInput: async (c, input: Input) => {
const id = c.conn.id;
const player = c.vars.playerCache[id];
if (player) {
// Update in memory for fast response
player.input = input;
// Update in database
await c.db
.update(players)
.set({
inputX: input.x,
inputY: input.y
})
.where(players.id.equals(id));
}
}
}
});
export default gameRoom;
import { createClient } from "actor-core/client";
import { createReactActorCore } from "@actor-core/react";
import { useState, useEffect, useRef } from "react";
import type { Player } from "./actor";
const client = createClient("http://localhost:6420");
const { useActor, useActorEvent } = createReactActorCore(client);
export function MultiplayerGame() {
const [{ actor, connectionId }] = useActor("gameRoom");
const [players, setPlayers] = useState<Player[]>([]);
const canvasRef = useRef<HTMLCanvasElement>(null);
const keysPressed = useRef<Record<string, boolean>>({});
// Set up game
useEffect(() => {
if (!actor) return;
// Set up keyboard handlers
const handleKeyDown = (e: KeyboardEvent) => {
keysPressed.current[e.key.toLowerCase()] = true;
};
const handleKeyUp = (e: KeyboardEvent) => {
keysPressed.current[e.key.toLowerCase()] = false;
};
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
// Input update loop
const inputInterval = setInterval(() => {
const input = { x: 0, y: 0 };
if (keysPressed.current["w"] || keysPressed.current["arrowup"]) input.y = -1;
if (keysPressed.current["s"] || keysPressed.current["arrowdown"]) input.y = 1;
if (keysPressed.current["a"] || keysPressed.current["arrowleft"]) input.x = -1;
if (keysPressed.current["d"] || keysPressed.current["arrowright"]) input.x = 1;
actor.setInput(input);
}, 50);
// Rendering loop
const renderLoop = () => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Use for loop instead of forEach
for (let i = 0; i < players.length; i++) {
const player = players[i];
ctx.fillStyle = player.id === connectionId ? "blue" : "gray";
ctx.beginPath();
ctx.arc(player.position.x, player.position.y, 10, 0, Math.PI * 2);
ctx.fill();
}
requestAnimationFrame(renderLoop);
};
const animationId = requestAnimationFrame(renderLoop);
return () => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("keyup", handleKeyUp);
clearInterval(inputInterval);
cancelAnimationFrame(animationId);
};
}, [actor, connectionId, players]);
// Listen for world updates
useActorEvent({ actor, event: "worldUpdate" }, ({ players: updatedPlayers }) => {
setPlayers(updatedPlayers);
});
return (
<div>
<canvas
ref={canvasRef}
width={800}
height={600}
style={{ border: "1px solid black" }}
/>
<p>Move: WASD or Arrow Keys</p>
</div>
);
}
import { actor } from "actor-core";
// Simple rate limiter - allows 5 requests per minute
const rateLimiter = actor({
state: {
count: 0,
resetAt: 0
},
actions: {
// Check if request is allowed
checkLimit: (c) => {
const now = Date.now();
// Reset if expired
if (now > c.state.resetAt) {
c.state.count = 0;
c.state.resetAt = now + 60000; // 1 minute window
}
// Check if under limit
const allowed = c.state.count < 5;
// Increment if allowed
if (allowed) {
c.state.count++;
}
return {
allowed,
remaining: 5 - c.state.count,
resetsIn: Math.round((c.state.resetAt - now) / 1000)
};
}
}
});
export default rateLimiter;
import { createClient } from "actor-core/client";
import { createReactActorCore } from "@actor-core/react";
import { useState } from "react";
import type { App } from "../actors/app";
const client = createClient<App>("http://localhost:6420");
const { useActor } = createReactActorCore(client);
export function RateLimiter() {
// Connect to API rate limiter for user-123
const [{ actor }] = useActor("rateLimiter", { tags: { userId: "user-123" } });
const [result, setResult] = useState<{
allowed: boolean;
remaining: number;
resetsIn: number;
} | null>(null);
// Make a request
const makeRequest = async () => {
if (!actor) return;
const response = await actor.checkLimit();
setResult(response);
};
return (
<div>
<h2>Rate Limiter (5 req/min)</h2>
<button onClick={makeRequest}>Make Request</button>
{result && (
<div>
<p>Status: {result.allowed ? "Allowed" : "Blocked"}</p>
<p>Remaining: {result.remaining}</p>
<p>Resets in: {result.resetsIn} seconds</p>
</div>
)}
</div>
);
}
import { actor } from "actor-core";
import { drizzle } from "@actor-core/drizzle";
import { limiters } from "./schema";
// Simple rate limiter - allows 5 requests per minute
const rateLimiter = actor({
sql: drizzle(),
actions: {
// Check if request is allowed
checkLimit: async (c) => {
const now = Date.now();
// Get the current limiter state from database
const limiterState = await c.db
.select()
.from(limiters)
.get();
// If no record exists, create one
if (!limiterState) {
await c.db.insert(limiters).values({
count: 1,
resetAt: now + 60000 // 1 minute window
});
return {
allowed: true,
remaining: 4,
resetsIn: 60
};
}
// Reset if expired
if (now > limiterState.resetAt) {
await c.db.update(limiters)
.set({
count: 1,
resetAt: now + 60000 // 1 minute window
});
return {
allowed: true,
remaining: 4,
resetsIn: 60
};
}
// Check if under limit
const allowed = limiterState.count < 5;
// Increment if allowed
if (allowed) {
await c.db.update(limiters)
.set({ count: limiterState.count + 1 });
}
return {
allowed,
remaining: 5 - (allowed ? limiterState.count + 1 : limiterState.count),
resetsIn: Math.round((limiterState.resetAt - now) / 1000)
};
}
}
});
export default rateLimiter;
import { createClient } from "actor-core/client";
import { createReactActorCore } from "@actor-core/react";
import { useState } from "react";
import type { App } from "../actors/app";
const client = createClient<App>("http://localhost:6420");
const { useActor } = createReactActorCore(client);
export function RateLimiter() {
// Connect to API rate limiter for user-123
const [{ actor }] = useActor("rateLimiter", { tags: { userId: "user-123" } });
const [result, setResult] = useState<{
allowed: boolean;
remaining: number;
resetsIn: number;
} | null>(null);
// Make a request
const makeRequest = async () => {
if (!actor) return;
const response = await actor.checkLimit();
setResult(response);
};
return (
<div>
<h2>Rate Limiter (5 req/min)</h2>
<button onClick={makeRequest}>Make Request</button>
{result && (
<div>
<p>Status: {result.allowed ? "Allowed" : "Blocked"}</p>
<p>Remaining: {result.remaining}</p>
<p>Resets in: {result.resetsIn} seconds</p>
</div>
)}
</div>
);
}
We’re working on publishing full examples related to these snippets. If you find an error, please create an issue.
Runs On Your Stack
Deploy ActorCore anywhere - from serverless platforms to your own infrastructure with our flexible runtime options.
Don’t see the runtime you want? Add your own.
Works With Your Tools
Seamlessly integrate ActorCore with your favorite frameworks, languages, and tools.
Don’t see what you need? Request an integration.
Roadmap For 2025
We ship fast, so we want to share what you can expect to see before the end of the year.
Help shape our roadmap by creating issues and joining our Discord.
SQLite Support
SQLite in Studio
Local-First Extensions
Auth Extensions
Workflows
Queues
MCP
Actor-Actor Actions
Cancellable Schedules
Cron Jobs
Drizzle Support
Prisma v7 Support
Read Replicas
Middleware
Schema Validation
Vite Integration
OpenTelemetry
More Examples
Studio
File system driver
Redis driver
Bun support
P2P topology
React client
Rust client
Resend Integration
Vitest Integration
Non-serialized state
create-actor
actor-core dev
Hono Integration
Supercharged Local Development with the Studio
Like Postman, but for all of your stateful serverless needs.
What People Are Saying
From the platform formerly known as Twitter

gerred
@devgerred
Nice work, @rivet_gg - nailed it

Samo
@samk0_com
Great UX & DX possible thanks to @ActorCore_org


John Curtis
@Social_Quotient
Loving ActorCore direction!

Local-First Newsletter
@localfirstnews
Featured in newsletter

Chinomso
@Chinoman10_
Alternatively, some dude (@NathanFlurry) recently told me about @ActorCore_org, which optionally brings you vendor-flexibility (no lock-in since it’s abstracted for you).

uripont
@uripont_
Crazy to think that there are so many things to highlight that is actually hard to convey it in a few words.

sam
@samgoodwin89
”Durable Objects without the boilerplate”

Kacper Wojciechowski
@j0g1t
Your outie uses @ActorCore_org to develop realtime applications.


alistair
@alistaiir
ActorCore looks super awesome.
Join the Community
Help make ActorCore the universal way to build & scale stateful serverless applications.
Frequently Asked Questions
Common questions about stateful serverless and ActorCore.
ActorCore is a framework written in TypeScript that provides high-level functionality. Rivet is an open-source serverless platform written in Rust with features tailored for stateful serverless.
You can think of it as ActorCore is to Rivet as Next.js is to Vercel.
While Rivet is the primary maintainer of ActorCore, we intend for this to be community driven.
Stateful serverless is very similar to actors: it’s essentially actors with persistence, and usually doesn’t have as rigid constraints on message handling. This makes it more flexible while maintaining the core benefits of the actor model.
Stateless serverless works well when you have an external resource that maintains state. Stateful serverless, on the other hand, is almost like a mini-database.
Sometimes it makes sense to use stateless serverless to make requests to multiple stateful serverless instances, orchestrating complex operations across multiple state boundaries.
By storing state in memory and flushing to a persistence layer, we can serve requests instantly instead of waiting for a round trip to the database. There are additional optimizations that can be made around your state to tune the durability of it.
Additionally, data is stored near your users at the edge, ensuring round-trip times of less than 50ms when they request it. This edge-first approach eliminates the latency typically associated with centralized databases.
Some software makes sense to separate – e.g., for data lakes or highly relational data. But at the end of the day, data has to be partitioned somewhere at some point.
Usually “faster” databases like Cassandra, DynamoDB, or Vitess make consistency tradeoffs to get better performance. Stateful serverless forces you to think about how your data is sharded for better performance, better scalability, and less consistency footguns.
OLAP, data lakes, graph databases, and highly relational data are currently not ideal use cases for stateful serverless, though it will get better at handling these use cases over time.
Yes, but only as much as storing data in a single database row does. We’re working on building out read replicas to allow you to perform read-only actions on actors.
Things are cooking! Check out our blog post about what a W3C standard for stateful serverless might look like and the awesome people who are collaborating on this.
Have more questions? Join our Discord or go to GitHub Discussions.
Why We Created ActorCore
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 ActorCore. We see maintaining ActorCore 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
Performance in every act - thanks to ActorCore.
Click here to file a complaint for bad puns.