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.

npx create-actor@latest

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.

Example

Chat Room

AI Agent

Local-First Sync

Per-Tenant Saas

Per-User Databases

Yjs CRDT

Collaborative Document

Stream Processing

Multiplayer Game

Rate Limiter

State
JavaScript
SQLiteAvailable In April
actor.tsRuns on the server
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;
App.tsxRuns in the browser
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>
  );
}
actor.tsRuns on the server
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;
App.tsxRuns in the browser
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>
  );
}
actor.tsRuns on the server
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;
App.tsxRuns in the browser
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>
  );
}
actor.tsRuns on the server
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;
App.tsxRuns in the browser
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>
  );
}
actor.tsRuns on the server
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;
App.tsxRuns in the browser
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>
  );
}
actor.tsRuns on the server
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;
App.tsxRuns in the browser
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>
  );
}
actor.tsRuns on the server
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;
App.tsxRuns in the browser
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>
  );
}
actor.tsRuns on the server
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;
App.tsxRuns in the browser
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>
  );
}
actor.tsRuns on the server
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;
App.tsxRuns in the browser
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>
  );
}
actor.tsRuns on the server
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;
App.tsxRuns in the browser
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>
  );
}
actor.tsRuns on the server
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;
App.tsxRuns in the browser
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);
}
actor.tsRuns on the server
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;
App.tsxRuns in the browser
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);
}
actor.tsRuns on the server
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;
App.tsxRuns in the browser
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>
  );
}
actor.tsRuns on the server
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;
App.tsxRuns in the browser
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>
  );
}
actor.tsRuns on the server
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;
App.tsxRuns in the browser
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>
  );
}
actor.tsRuns on the server
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;
App.tsxRuns in the browser
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>
  );
}
actor.tsRuns on the server
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;
App.tsxRuns in the browser
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>
  );
}
actor.tsRuns on the server
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;
App.tsxRuns in the browser
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>
  );
}
actor.tsRuns on the server
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;
App.tsxRuns in the browser
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>
  );
}
actor.tsRuns on the server
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;
App.tsxRuns in the browser
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.

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.