# 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`](https://github.com/rivet-dev/rivet/tree/main/examples/hello-world-effect) and [`chat-room-effect`](https://github.com/rivet-dev/rivet/tree/main/examples/chat-room-effect) examples for complete runnable projects. ## Steps ### Install Rivet Add `rivetkit`, the Effect SDK, and its Effect peers: ```sh 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`](https://effect.website/docs/schema/introduction/) payloads and successes, validated end to end: ```ts src/actors/counter/api.ts @nocheck 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: ```ts src/actors/counter/live.ts @nocheck 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: ```ts src/main.ts @nocheck 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: ```sh 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](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: ```ts src/client.ts @nocheck 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: ```sh npx tsx src/client.ts ``` See the [`chat-room-effect`](https://github.com/rivet-dev/rivet/tree/main/examples/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: ```ts client.ts @nocheck 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](/docs/clients/javascript) 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](#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 | `Logger` | Typed | | Sleep request | `Actor.Sleep` | Typed | | Actor address (`actorId` / `name` / `key`) | `Actor.CurrentAddress` | Typed | | Registry serve / test / web handler | `Registry` | Typed | | [Events & broadcast](/docs/actors/events) | Not yet wrapped | `rawRivetkitContext.broadcast(...)` | | [Schedule](/docs/actors/schedule) | Not yet wrapped | `rawRivetkitContext.schedule.*` | | [Embedded SQLite](/docs/actors/sqlite) | Not yet wrapped | `rawRivetkitContext.db.execute(...)` | | [Destroy](/docs/actors/lifecycle) | 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](/docs/actors). 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: ```ts src/actors/counter/live.ts @nocheck 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](/docs/actors/actions) — Define the RPC surface clients call on your actor. - [State](/docs/actors/state) — Persist and load actor state across sleeps and restarts. - [Events](/docs/actors/events) — Broadcast realtime updates to connected clients. _Source doc path: /docs/actors/quickstart/effect_