Quickstart
Responsibility: take a developer from an empty directory to a running, tested actor app.Authority: operational.
Owner role: API / SDK.
Change policy: change with the public API; run every command here against a freshly packed tarball before a release. You need Bun 1.4.2 or later, or Node.js 24 or later. No Docker and no database server: the app stores its data with PGlite, an embedded Postgres database, in
./.data. File-backed PGlite requires Linux or macOS on a local filesystem with either runtime.
1. Install
2. Declare an actor
src/counter/contract.ts is the actor’s public shape. Clients import only this file.
src/counter/contract.ts
src/counter/layer.ts implements it. The handler runs inside the turn’s transaction.
src/counter/layer.ts
3. Run it
src/main.ts
import { NodeCrypto, NodeRuntime } from "@effect/platform-node", replace BunCrypto.layer with NodeCrypto.layer and BunRuntime.runMain with NodeRuntime.runMain, then run:
NodeHttpServer.layer(createServer, { port: 8080 }) from @effect/platform-node, with createServer imported from node:http, instead of BunHttpServer.layer({ port: 8080 }). Actors.serve uses the same Effect HTTP layer on both runtimes.
Each run is a new process. The count survives because the turn that incremented it committed to the database files in ./.data before the reply. Delete ./.data to start again.
4. Test it
ActorTest from @rikalabs/akter/testing runs the real turn path against a throwaway database, with fault injection and inspection.
src/counter/layer.test.ts
npm install --save-dev vitest), import afterAll, expect, and test from vitest instead of bun:test, and provide NodeCrypto.layer instead of BunCrypto.layer. Run npx vitest run; the actor, receipt and crash assertions stay unchanged.
The first test opens a new PGlite directory, which runs initdb inside WebAssembly and applies the framework’s migrations, so it takes a few seconds. The injected afterCommit crash is logged as an entity defect; that log line is expected.
5. Switch to a Postgres server
ReplaceDatabase.pglite({ dataDir: "./.data" }) with Database.postgres({ url }), where url is a Redacted connection string such as Redacted.make(process.env.DATABASE_URL!). In tests, pass { url } as ActorTest.layer({ database }). Startup creates the framework tables. Use a database for this app alone.
What PGlite is for
PGlite has one connection and belongs to the one process that opened its data directory, so:- run one process against a data directory; a second process opening it at the same time is refused;
- nothing on PGlite proves lock contention, independent connections, multi-runner relay, or process-kill recovery, which the framework verifies on a Postgres server only;
- file-backed PGlite is a production backend for one process per data directory, within the limits of ADR 0035: the data directory is locked to one process, a process crash recovers to the last commit (power loss is not claimed), backups are stopped copies, and there are no replicas or multiple runners. Move to a Postgres server when those limits bind; see the support matrix.