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

gerred
@devgerred
Nice work, @rivet_gg - nailed it

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


John Curtis
@Social_Quotient
Loving RivetKit direction!

Local-First Newsletter
@localfirstnews
Featured in newsletter

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

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

sam
@samgoodwin89
”Durable Objects without the boilerplate”

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


alistair
@alistaiir
RivetKit looks super awesome.
Join the Community
Help make RivetKit the universal way to build & scale stateful serverless applications.
Performance in every act - thanks to Rivet Actors.
Click here to file a complaint for bad puns.