Promo Image

Introducing RivetKit: Backend Libraries That Replace SaaS


Use Libraries, Not SaaS

Every API call to a cloud provider is a dependency you can't control.

When we built Rivet Actors – an open-source alternative to Cloudflare Durable Objects – we set out to provide a portable foundation for the novel "stateful serverless" architecture that is gaining traction. However, we felt that we weren't doing enough by just being an open-source alternative – we knew we needed to take a step further to make this ubiquitous.

That's why today, we're introducing RivetKit — a port of our core Rivet Actors service exposed as a portable TypeScript library that runs anywhere & can easily be added to your existing backend.

Instead of needing to sign up for a service or run a Docker container to run Rivet Actors, now it runs natively as a lightweight TypeScript library within your codebase & integrates with your existing infrastructure (e.g. Redis).


Benefits of Libraries Over SaaS

Our decision to launch RivetKit as a library was underpinned by the following constraints that we kept hearing developers voice:

  • Fast to Get Started: Simple package install with no API keys, accounts, or authentication setup
  • Minimize Critical Dependencies: Fewer dependencies on external services means fewer points of failure
  • Cost: More external services results in higher cost or compute required compared to running as a library
  • Testing: SaaS & service dependencies are often difficult to unit test, while libraries are very easy to integrate with libraries like Vitest
  • Performance: Less network hops, serialization/deserialization, and external databases results in significantly lower latency
  • Security & Data Privacy: Your data never leaves your infrastructure, eliminating third-party risks
  • No Vendor Lock-in: Full control over your tech stack with ability to modify, fork, or switch libraries

The Old Way: Multiple External Dependencies

With @rivetkit/actors: Just A Library & Your Database


Portability

As a library, RivetKit can run anywhere your infrastructure runs. Currently, RivetKit supports Redis, filesystem, Rivet Cloud, and Cloudflare Workers. Drivers for more platforms such as Postgres, Vercel, and more are trivial to add.


Rivet Actors: Doing more with less

At its core, RivetKit provides Rivet Actors: long-running tasks with persistence & realtime. Rivet Actors provide a simple primitive that provides the same benefits that would usually require many other external services: RPC, realtime, persistence, and long-running tasks.

Rivet Actors excel at:

  • Stateful Services: Applications where maintaining state across interactions is critical. For example, Collaborative Apps with shared editing and automatic persistence.
  • Realtime Systems: Applications requiring fast, in-memory state modifications or push updates to connected clients. For example, Multiplayer Games with game rooms and player state.
  • Long-Running Processes: Tasks that execute over extended periods or in multiple steps. For example, AI Agents with ongoing conversations and stateful tool calls.
  • Durability: Processes that must survive crashes and restarts without data loss. For example, Durable Execution workflows that continue after system restarts.
  • Horizontal Scalability: Systems that need to scale by distributing load across many instances. For example, Realtime Stream Processing for stateful event handling.
  • Local-First Architecture: Systems that synchronize state between offline clients. For example, Local-First Sync between devices.

A simple Rivet actor that combines persistence & realtime looks like this:

TypeScript
import { actor } from "@rivetkit/actor";

const chatRoom = actor({
  // FEATURE: Durable state
  state: { messages: [] as Array<{text: string, userId: string}> },

  // FEATURE: RPC  
  actions: {
    sendMessage: (c, userId: string, text: string) => {
      const message = { text, userId };
      c.state.messages.push(message);

      // FEATURE: Realtime
      c.broadcast("newMessage", message);

      return message;
    },
    
    getMessages: (c) => c.state.messages
  }
});

And this can be called from your client of choice. For example, JavaScript:

const client = /* ... */;

// Connect to actor
const room = client.chatRoom.getOrCreate("random").connect();

// Listen for events
room.on("newMessage", () => /* ... */);

// RPC
await room.getMessages();

RivetKit v0.9 and Its Predecessor, ActorCore: Rearchitected As A Library

RivetKit is massive rearchitecture of its predecessor, ActorCore. In the process, ActorCore was rebuilt from the ground up to be a library instead of a framework.

Previously, ActorCore required running as a standalone framework. To setup your actors, you'd write:

TypeScript
import { actor, setup } from "actor-core";

const myActor = actor(/* ... */);

export const app = setup({ actors: { myActor } });

Then run the ActorCore framework with:

Command Line
npx @actor-core/cli dev

Now, RivetKit is a lightweight library that can be integrated in to your existing backend like this:

TypeScript
import { actor, setup } from "@rivetkit/actor";

// Setup actors
const myActor = actor(/* ... */);
export const registry = setup({ use: { myActor } });

// NEW: Setup RivetKit server
const { client, serve } = registry.createServer();

// Your existing backend
const app = new Hono();

app.route("/foo", c => {
  // NEW: Communicate with your actors
  const res = client.myActor.getOrCreate().foo();
  return c.text(res);
});

serve(app);

And run with vanilla Node.js:

Command Line
npx tsx server.ts

More Improvements in v0.9

Inline client

Previously, ActorCore v0.8 required communicating over HTTP with your actors. To talk to actors from your backend, you needed to make a separate HTTP request to the actor manager server.

Now, RivetKit provides an "inline client" that can communicate with actors directly within your backend. For example:

TypeScript
import { registry } from "./registry";
import { Hono } from "hono";

// Start RivetKit with memory driver (for development)
const { client, serve } = registry.createServer();

// Setup Hono app
const app = new Hono();

// Example API endpoint
app.post("/increment/:name", async (c) => {
	const name = c.req.param("name");

	// Get or create actor and call action
	const counter = client.counter.getOrCreate(name);
	const newCount = await counter.increment(1);

	return c.json({ count: newCount });
});

// Start server with RivetKit
serve(app);

Granular onAuth Lifecycle Hook

The new onAuth lifecycle hook provides a way to handle authentication to actors based on intent.

onAuth runs on the edge server before reaching the actor itself – meaning it doesn't require any compute to run on the actor & is safe from denial of service attacks.

For example:

TypeScript
import { actor, Unauthorized } from "@rivetkit/actor";

const chatRoom = actor({
  // NEW: Auth hook
  onAuth: async (opts) => {
    const { req, params, intents } = opts;
    
    // Extract token from params or headers
    const token = params.authToken || req.headers.get("Authorization");
    if (!token) throw new Unauthorized();
    
    // Validate token and return user data
    const user = await validateJWT(token);
    return { 
      userId: user.id, 
      role: user.role,
      permissions: user.permissions 
    };
  },

  state: { messages: [] },
  
  actions: {
    sendMessage: (c, text: string) => {
      // NEW: Access auth data via c.conn.auth
      const { userId, role } = c.conn.auth;
      
      if (role !== "member") {
        throw new UserError("Insufficient permissions");
      }
      
      const message = {
        id: crypto.randomUUID(),
        userId,
        text,
        timestamp: Date.now(),
      };
      
      c.state.messages.push(message);
      c.broadcast("newMessage", message);
      return message;
    }
  }
})

See the auth documentation for more details.

Better Auth Integration

See the RivetKit + Better Auth integration.

Communicating Between Actors

Actors can now communicate with each other by using the client in the context. For example:

TypeScript
import { actor } from "@rivetkit/actor";
import type { registry } from "./registry";

export const orderProcessor = actor({
  state: { orders: [] },
  
  actions: {
    processOrder: async (c, order: Order) => {
      // NEW: Get the client
      const client = c.client<typeof registry>();
      
      // Call another actor to check inventory
      const inventory = client.inventory.getOrCreate(order.productId);
      const available = await inventory.reserveStock(order.quantity);
      if (!available) throw new UserError("Insufficient stock");
      
      // Process payment through payment actor
      const payment = client.payment.getOrCreate(order.customerId);
      const result = await payment.processPayment(order.amount);
      
      // Update order state
      c.state.orders.push({
        ...order,
        status: "processed",
        paymentId: result.paymentId,
      });
      
      return { success: true, orderId: order.id };
    }
  }
});

React Shared Actor Connections

When using the React integration for Rivet Actors, calling useActor will share the same underlying WebSocket or SSE connection when communicating with actors & will automatically dispose of the connection when no longer in use.

This means you can call useActor liberally inside your components without complex connection handling logic.

Replacing Tags With Compound Keys

Previously, ActorCore v0.8 used a system of key-value tags to organize actors. This turned to be overkill in practice and added unnecessary complexity under the hood.

This has been replaced with compound keys. Compound keys are either a simple string or an array of strings. The array of strings allows actor keys to inject user-provided strings without having to worry about injection attacks.

For example:

TypeScript
const chatThread = client.chatThread.getOrCreate([roomId, threadId]);

See the new actor handles documentation.

Pass Input to create

Initializing actors with custom state on what they're responsible for is a common pattern. For example, define an actor like this:

TypeScript
const game = actor({  
  actions: {
    getRoomInfo: (c) => {
      return `Room: ${opts.input.gameMode} with players ${opts.input.maxPlayers}`
    },
  },
  // ...etc...
});

And create it like this:

TypeScript
// Create new actor
const newGame = await client.game.create(["game-456"], {
  input: { gameMode: "classic", maxPlayers: 4 }
});

Read more about creating actors.

Stateless Communication With Actors

Actors now default to using HTTP connections unless explicitly calling .connect() to upgrade to a WebSocket or SSE connection. This allows for faster passing of messages without compromising on enabling high performance WebSocket- & SSE-based connections.

For example:

TypeScript
const counter = client.counter.getOrCreate("live-counter");

// NEW: Connect to the counter
const connection = counter.connect();

// Listen for events
connection.on("countChanged", (newCount: number) => {
  console.log("Count updated:", newCount);
});

// Call actions through the connection
const result = await connection.increment(1);

// Clean up when done
await connection.dispose();

Read the new connections documentation.

Secured Params & Input

Parameters & input data to actors are now end-to-end encrypted securely. Previously, they were passed as a query parameter which is frequently leaked in logs. Now, they're passed a secure header.

OpenAPI Spec

RivetKit now provides an OpenAPI spec to integrate your own clients. Read the specification documentation and see the full openapi.json.


More Information

RivetKit will be a collection of powerful lightweight libraries directly into your codebase for better performance, control, and cost efficiency. No SaaS fees or API dependencies.


Ready to replace your SaaS dependencies with portable libraries? Get started with RivetKit today.