Files
zopu-code/.agents/skills/ai-agent/reference/actors/quickstart/effect.md

8.6 KiB

Effect.ts Quickstart (Beta)

Source: src/content/docs/actors/quickstart/effect.mdx Canonical URL: https://rivet.dev/docs/actors/quickstart/effect Description: Build a Rivet Actor with the Effect SDK


Effect support is in beta. The @rivetkit/effect API may change between releases. See the hello-world-effect and chat-room-effect examples for complete runnable projects.

Steps

Install Rivet

Add rivetkit, the Effect SDK, and its Effect peers:

npm install rivetkit @rivetkit/effect effect @effect/platform-node

Define Your Actor

Split each actor into a public contract and a server-only implementation so the contract can be imported from client code without leaking server details.

The contract declares the actor and its actions. Actions are standalone values with explicit effect/Schema payloads and successes, validated end to end:

import { Action, Actor } from "@rivetkit/effect";
import { Schema } from "effect";

export const Increment = Action.make("Increment", {
	payload: { amount: Schema.Number },
	success: Schema.Number,
});

export const GetCount = Action.make("GetCount", {
	success: Schema.Number,
});

export const Counter = Actor.make("Counter", {
	actions: [Increment, GetCount],
});

The implementation registers the actor with .toLayer. The wake function runs once when the actor awakes and returns the action handlers. Persisted state is accessed through a SubscriptionRef-like State API:

import { Actor, State } from "@rivetkit/effect";
import { Effect, Schema } from "effect";
import { Counter } from "./api.ts";

export const CounterLive = Counter.toLayer(
	Effect.fnUntraced(function* ({ rawRivetkitContext, state }) {
		return Counter.of({
			Increment: Effect.fnUntraced(function* ({ payload }) {
				const next = yield* State.updateAndGet(state, (current) => ({
					count: current.count + payload.amount,
				})).pipe(Effect.orDie);

				// Broadcast the new value to every connected client.
				rawRivetkitContext.broadcast("newCount", next.count);

				return next.count;
			}),
			GetCount: () =>
				State.get(state).pipe(
					Effect.map((current) => current.count),
					Effect.orDie,
				),
		});
	}),
	{
		state: {
			schema: Schema.Struct({ count: Schema.Number }),
			initialValue: () => ({ count: 0 }),
		},
		name: "Counter",
		icon: "calculator",
	},
);

Serve The Registry

Compose the actor layers and serve them with Registry.serve. Registry.layer() reads engine config from the environment, and the actor layer is provided a Client so actors can call other actors:

import { NodeRuntime } from "@effect/platform-node";
import { Client, Registry } from "@rivetkit/effect";
import { Layer } from "effect";
import { CounterLive } from "./actors/counter/live.ts";

const endpoint = process.env.RIVET_ENDPOINT ?? "http://127.0.0.1:6420";

const ActorsLayer = CounterLive.pipe(Layer.provide(Client.layer({ endpoint })));

const MainLayer = Registry.serve(ActorsLayer).pipe(Layer.provide(Registry.layer()));

// Keeps the layer alive. Tears down on SIGINT/SIGTERM.
Layer.launch(MainLayer).pipe(NodeRuntime.runMain);

Run The Server

Set RIVET_RUN_ENGINE=1 to spawn a local Rivet Engine alongside the server. The engine binary is downloaded and cached the first time you run, so there is nothing else to install:

RIVET_RUN_ENGINE=1 npx tsx --watch src/main.ts

Your server now connects to the Rivet Engine on http://localhost:6420. Clients connect directly to the engine on this port.

Visit http://localhost:6420 in your browser (or point your AI agent at it) to open the Rivet developer tools and inspect your actors live.

To point at a remote engine instead, set RIVET_ENDPOINT=https://... and omit RIVET_RUN_ENGINE.

Connect To The Rivet Actor

This code can run either in your frontend or within your backend:

Effect

The Effect client imports the same actor contract from your registry. Counter.client yields a typed accessor backed by the client layer:

import { NodeRuntime } from "@effect/platform-node";
import { Client } from "@rivetkit/effect";
import { Effect } from "effect";
import { Counter } from "./actors/counter/api.ts";

const program = Effect.gen(function* () {
	const counter = (yield* Counter.client).getOrCreate("my-counter");

	const count = yield* counter.Increment({ amount: 3 });
	yield* Effect.log(`New count: ${count}`);

	const total = yield* counter.GetCount();
	yield* Effect.log(`Total: ${total}`);
});

const ClientLayer = Client.layer({ endpoint: "http://localhost:6420" });

program.pipe(Effect.provide(ClientLayer), NodeRuntime.runMain);

With the server still running, start the client in another terminal:

npx tsx src/client.ts

See the chat-room-effect example for a larger project with typed errors and actor-to-actor calls.

TypeScript

A plain RivetKit client can call your Effect actor by name through the same engine. Actor and action names are resolved at runtime, so the client is untyped here:

import { createClient } from "rivetkit/client";

const client = createClient("http://localhost:6420");

const counter = client.Counter.getOrCreate(["my-counter"]);

const count = await counter.Increment({ amount: 3 });
console.log("New count:", count);

See the JavaScript client documentation for more information.

Deploy

Feature Support

The Effect SDK wraps the most common actor features with typed, schema-validated APIs. Everything else is still fully usable through the raw RivetKit context (see Raw Escape Hatch below), so no feature is off limits, it just isn't typed yet.

Feature Effect-native API Access
Actor contract & actions Actor.make, Action.make Typed
Persisted state State.get / set / update / updateAndGet / changes Typed
Typed client Actor.client, Client.layer Typed
Typed errors RivetError Typed
Logging RivetLogger Typed
Sleep request Actor.Sleep Typed
Actor address (actorId / name / key) Actor.CurrentAddress Typed
Registry serve / test / web handler Registry Typed
Events & broadcast Not yet wrapped rawRivetkitContext.broadcast(...)
Schedule Not yet wrapped rawRivetkitContext.schedule.*
Embedded SQLite Not yet wrapped rawRivetkitContext.db.execute(...)
Destroy Not yet wrapped rawRivetkitContext.destroy()
Queues, connections, vars, alarms Not yet wrapped rawRivetkitContext.*
Lifecycle hooks (onSleep / onDestroy) Not yet wrapped rawRivetkitContext.*
Raw HTTP / WebSocket handlers Not yet wrapped rawRivetkitContext.*

Raw Escape Hatch

Every wake function receives rawRivetkitContext, the underlying RivetKit actor context. Reach for it to use any feature that does not have a typed wrapper yet. The typed state argument and the raw context point at the same actor, so you can mix both:

export const CounterLive = Counter.toLayer(
	Effect.fnUntraced(function* ({ rawRivetkitContext, state }) {
		return Counter.of({
			Increment: Effect.fnUntraced(function* ({ payload }) {
				// Typed state wrapper
				const next = yield* State.updateAndGet(state, (current) => ({
					count: current.count + payload.amount,
				})).pipe(Effect.orDie);

				// Untyped features run through the raw context
				rawRivetkitContext.broadcast("newCount", next.count);
				rawRivetkitContext.schedule.after(1_000, "tick", {});

				return next.count;
			}),
		});
	}),
	{
		state: {
			schema: Schema.Struct({ count: Schema.Number }),
			initialValue: () => ({ count: 0 }),
		},
		name: "Counter",
	},
);

Calls through rawRivetkitContext are not validated by effect/Schema and their payloads are typed as they are in the base RivetKit API.

Next Steps

  • Actions — Define the RPC surface clients call on your actor.

  • State — Persist and load actor state across sleeps and restarts.

  • Events — Broadcast realtime updates to connected clients.

Source doc path: /docs/actors/quickstart/effect