Skip to main content

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

For Node, install the Node platform instead:
Effect, the Effect SQL drivers and Drizzle are peer dependencies pinned to the exact versions the framework is tested against.

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
On Node, replace the platform import with import { NodeCrypto, NodeRuntime } from "@effect/platform-node", replace BunCrypto.layer with NodeCrypto.layer and BunRuntime.runMain with NodeRuntime.runMain, then run:
Node’s native TypeScript support runs this app without a build step. In a served app, provide 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
For Node tests, install Vitest (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

Replace Database.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.
On a Postgres server the alpha is single-runner: run one runtime process per database.