--- url: https://accord-docs-aqu.pages.dev/docs/concepts/motivation.md --- # Motivation Domain-Driven Design has good ideas: aggregates that protect invariants, events that record what happened, reactions that describe what happens next. It also has a reputation for ceremony. Accord keeps the ideas and drops the ceremony. ## What Accord gives you * **A small, explicit API.** You define business rules with ordinary classes or functions. No base repositories, no buses to configure, no decorators. * **Failures as values.** A business refusal like `InsufficientFunds` is a tagged value, not an exception. Operations return `[value, null]` or `[null, error]`, so the compiler reminds you to look at the error. See [Tuples, not throws](/docs/concepts/results). * **One commit boundary.** A successful command saves the new state, the emitted events, and a receipt together. A rejection or exception discards the working copy. * **Reactions that rebuild.** Views and projections fold events and can replay history. Policies react to new events only. See [Events and delivery](/docs/concepts/delivery). * **Optional everything else.** The core has no runtime dependencies. SQLite persistence and Effect integration are separate entry points you import only when you need them. ## The building blocks | Primitive | What it is for | | --- | --- | | [`aggregate`](/docs/api/aggregate) | State plus the methods that change it and emit events | | [`event`, `rejection`](/docs/api/events) | Tagged, schema-validated facts and refusals | | [`vo`](/docs/api/value-object) | Value objects with validation and domain methods | | [`view`](/docs/api/view) | Read models folded from events | | [`policy`](/docs/api/policy) | "When X happens, do Y" | | [`projection`](/docs/api/projection) | Replayable writes to another store | | [`service`](/docs/api/service), [`external`](/docs/api/external) | Typed dependencies and adapters | | [`workflow`](/docs/api/workflow) | Checkpointed steps with compensation (in memory) | | [`model`, `wire`](/docs/api/model) | Register everything and get a typed runtime | | [`api`](/docs/api/http) | Expose selected commands and views over HTTP | | [`story`](/docs/api/story) | Scenario tests | ## When not to use it (yet) Accord v0.1 is an in-process runtime. There's no background scheduler, no distributed workflow engine, and no automatic migrations. If you need those today, read [Limits of v0.1](/docs/guides/limits) first. --- --- url: https://accord-docs-aqu.pages.dev/docs/concepts/results.md --- # Tuples, not throws Every Accord operation returns a tuple: `[value, null]` on success, `[null, error]` on failure. Errors are objects with a `_tag`. ```ts const [receipt, error] = await bank.account.deposit('a-1', 100) // receipt is unknown until error is narrowed. if (error !== null) { console.error(error._tag) } else { console.log(receipt.reply.balance) } ``` ::: warning Always check the error slot. A successful operation can return `null` or `undefined` as its value, so `if (receipt)` is not a success check. ::: ## Three kinds of failure | Tag | Where it comes from | | --- | --- | | Your rejections, e.g. `NotOpen` | You returned `fail(NotOpen())` | | `InvalidInput` | Input failed a schema at a boundary (`{ _tag, message }`) | | `Unexpected` | Something threw, or a handler returned something that isn't a valid tuple | `Unexpected` is a real `Error` with `_tag: 'Unexpected'` and the original thrown value in `cause`. ::: tip Constructing an event with invalid data throws a `TypeError` inside your method. The runtime catches it, discards the working instance, and returns `Unexpected`. If a value can be invalid for business reasons, check it first and return a rejection instead. ::: ## Producing results ```ts import { fail, ok, rejection } from '@marcelsud/accord' const NotOpen = rejection('NotOpen') ok({ balance: 100 }) // [{ balance: 100 }, null] fail(NotOpen()) // [null, { _tag: 'NotOpen' }] ``` Wrap code that throws with `trycatch`: ```ts import { trycatch } from '@marcelsud/accord' const [data, error] = trycatch(() => JSON.parse(text)) const [response, fetchError] = await trycatch(fetch(url)) ``` ## Handling them Use `match` for exhaustive handling. Missing or unknown cases fail type checking. ```ts import { match } from '@marcelsud/accord' const message = match(error, { NotOpen: () => 'Open the account first', DepositLimit: ({ maximum }) => `The limit is ${maximum}`, InvalidInput: ({ message }) => message, Unexpected: () => 'Something went wrong', Conflict: () => 'Try again', CommandConflict: () => 'Duplicate command', }) ``` See [Results](/docs/api/result) for the full API. --- --- url: https://accord-docs-aqu.pages.dev/docs/concepts/delivery.md --- # Events and delivery Commands commit events. Consumers (views, policies, and projections) receive them later, when you call `flush()`. ```ts const [receipt, error] = await bank.account.deposit('a-1', 100) if (error !== null) throw error // Deliver pending events to policies and projections. const [, deliveryError] = await bank.flush() if (deliveryError !== null) throw deliveryError ``` ## The commit A successful command saves the new state, its events, and a receipt in one step. The receipt describes the aggregate commit, not the success of every consumer. Each event is wrapped in an **envelope**: | Field | Meaning | | --- | --- | | `id` | Stable event ID, e.g. `["Bank","Account","a-1"]:2`. Use it as an idempotency key | | `stream` | The source stream | | `seq` | Sequence number within the stream | | `at` | Timestamp (ms) | | `source` | `{ type, id }` of the aggregate or view | | `event` | The tagged event payload | ## Delivery * **`flush()` is explicit.** There is no background worker or automatic backoff. Call `flush()` yourself (an `atLeast` read also flushes). * **Failures retry.** A failed consumer stops that flush and keeps its checkpoint. The next `flush()` retries the unacknowledged delivery. * **At-least-once.** Projection writes and policy actions must tolerate repeated delivery. * **Ordering is per source stream.** There's no global ordering across sources. ::: warning Don't await `flush()` from inside a consumer that is already running in that flush. ::: ## Who sees history | Consumer | Replays history? | Rebuild trigger | | --- | --- | --- | | [View](/docs/api/view) | Yes | Bump `version` | | [Projection](/docs/api/projection) | Yes | Bump `version` | | [Policy](/docs/api/policy) | No, new events only | New policies start at the current position | ## Read your own writes Views are eventually consistent. Pass the receipt to catch up first: ```ts const [summary] = await bank.summary.get('a-1', { atLeast: receipt }) ``` Without `atLeast`, you get whatever is materialized now, which may be `null`. --- --- url: https://accord-docs-aqu.pages.dev/docs/getting-started/basic.md --- # Getting Started Using Accord is easy. Let's build a tiny bank: one aggregate, one event, one rejection, and a view. ## Requirements * Node.js **24** or newer (Accord uses `node:sqlite`, `using`, and runs `.ts` files directly in Node 24) * TypeScript is recommended; Accord infers almost every type for you. ## Install Accord isn't published to npm yet. Build a tarball from the repository: ```sh git clone https://github.com/marcelsud/accord.git cd accord npm ci --ignore-scripts npm pack # creates marcelsud-accord-0.1.0.tgz ``` Then install it in your project: ::: code-group ```sh [npm] npm install ../accord/marcelsud-accord-0.1.0.tgz ``` ```sh [yarn] yarn add ../accord/marcelsud-accord-0.1.0.tgz ``` ```sh [pnpm] pnpm add ../accord/marcelsud-accord-0.1.0.tgz ``` ```sh [bun] bun add ../accord/marcelsud-accord-0.1.0.tgz ``` ::: Accord is ESM-only, so set `"type": "module"` in your `package.json`. ## Hello Bank ```ts [index.ts] import { aggregate, event, integer, model, ok, refine, wire } from '@marcelsud/accord' const Cents = refine(integer, (amount) => amount > 0, 'positive cents') const Deposited = event('Deposited', { amount: Cents, balance: integer }) class Account extends aggregate('Account', { balance: 0 }) { deposit(amount: number) { const event = Deposited({ amount, balance: this.state.balance + amount }) this.state.balance = event.balance this.emit(event) return ok({ balance: this.state.balance }) } } const Bank = model('Bank', { aggregates: { account: Account } }) const bank = wire(Bank) const [receipt, error] = await bank.account.deposit('account-123', 100) if (error !== null) throw error console.log(receipt.reply) // { balance: 100 } ``` Run it: ```sh node index.ts ``` What happened: 1. `aggregate('Account', { balance: 0 })` creates a base class. The initial state types `this.state`. 2. `event('Deposited', shape)` creates a factory that validates its payload at runtime. 3. `model()` registers the aggregate under the alias `account`. 4. `wire()` returns a typed client. `bank.account.deposit(id, ...args)` takes the instance ID first. ## Reject bad requests Business refusals are values. Declare them with `rejection()` and return them with `fail()`. ```ts import { aggregate, event, fail, integer, model, ok, rejection, wire } from '@marcelsud/accord' const Deposited = event('Deposited', { amount: integer, balance: integer }) const NotOpen = rejection('NotOpen') class Account extends aggregate('Account', { open: false, balance: 0 }) { open() { this.state.open = true return ok(undefined) } deposit(amount: number) { if (!this.state.open) return fail(NotOpen()) this.state.balance += amount this.emit(Deposited({ amount, balance: this.state.balance })) return ok({ balance: this.state.balance }) } } const bank = wire(model('Bank', { aggregates: { account: Account } })) const [, error] = await bank.account.deposit('a-1', 100) console.log(error) // { _tag: 'NotOpen' } await bank.account.open('a-1') const [receipt] = await bank.account.deposit('a-1', 100) console.log(receipt?.reply) // { balance: 100 } ``` A `fail()` result discards the working instance. Nothing is saved. ## Add a view A view folds events into read state, keyed by the aggregate ID by default. ```ts import { on, view } from '@marcelsud/accord' const Summary = view( 'Summary', { from: Account, initial: { balance: 0 } }, on(Deposited, (_state, { balance }) => ({ balance })) ) const Bank = model('Bank', { aggregates: { account: Account }, views: { summary: Summary }, }) const bank = wire(Bank) await bank.account.open('a-1') const [receipt] = await bank.account.deposit('a-1', 100) const [summary] = await bank.summary.get('a-1', { atLeast: receipt }) console.log(summary) // { balance: 100 } ``` `atLeast: receipt` catches the view up to your command before reading. ## Next * Keep your data across restarts with [SQLite](/docs/getting-started/sqlite). * Use Accord from [Effect](/docs/getting-started/effect). * Learn the full [Aggregate API](/docs/api/aggregate). --- --- url: https://accord-docs-aqu.pages.dev/docs/getting-started/sqlite.md --- # SQLite The default store lives in memory. Swap in SQLite to recover state, event history, views, and consumer checkpoints after a restart. It uses Node.js 24's built-in `node:sqlite` driver. No extra npm dependency. ## Usage ```ts import { wire } from '@marcelsud/accord' import { sqlite } from '@marcelsud/accord/sqlite' import { Bank } from './domain.ts' using storage = sqlite('app.sqlite') const bank = wire(Bank, { storage, services: { rules: { maximum: 1_000 } } }) const [, openError] = await bank.account.open('a-1') if (openError !== null) throw openError // Await commands and flush before the storage is disposed. const [, error] = await bank.flush() if (error !== null) throw error ``` * `Bank` and its services come from your application (here, the [Bank example](/examples/bank) domain). * `sqlite(filename)` opens or creates the file. `':memory:'` works too, but doesn't survive the process. * `using` closes the store when the scope ends. You can also call `storage.close()`. * Reopen the same file with the same model, consumer names, and versions to resume delivery. ::: warning Await every operation before closing. Closing with pending operations is rejected. ::: ## Try the restart demo From the Accord repository, after `npm run check`: ```sh npm run example:durable -- /tmp/accord-runtime.sqlite deposit 100 npm run example:durable -- /tmp/accord-runtime.sqlite drain npm run example:durable -- /tmp/accord-runtime.sqlite drain ``` The first process commits a deposit and exits without flushing. The next process delivers it. Both drains print a projected total of `100`: the projection's SQL write and its checkpoint commit together, so the second drain doesn't count the deposit again. ## Next * [SQLite store reference](/docs/api/sqlite): transactions, recovery, and concurrency. * [Durable bank example](/examples/durable-bank). --- --- url: https://accord-docs-aqu.pages.dev/docs/getting-started/effect.md --- # Effect Accord works without [Effect](https://effect.website). If you use Effect, `@marcelsud/accord/effect` turns your model into an Effect service whose calls are lazy Effects. ## Install Effect is an optional peer dependency. Install `effect@^4.1.2`: ::: code-group ```sh [npm] npm install effect@^4.1.2 ``` ```sh [yarn] yarn add effect@^4.1.2 ``` ```sh [pnpm] pnpm add effect@^4.1.2 ``` ```sh [bun] bun add effect@^4.1.2 ``` ::: ## Usage ```ts import { Effect, Layer } from 'effect' import { effectify } from '@marcelsud/accord/effect' import { Bank as BankModel, DepositRules } from './domain.ts' const Bank = effectify(BankModel, 'Bank') const RulesLive = Layer.succeed(effectify(DepositRules), { maximum: 1_000 }) const BankLive = Bank.layer.pipe(Layer.provide(RulesLive)) const program = Effect.gen(function* () { const bank = yield* Bank const id = 'a-1' yield* bank.account.open(id) const receipt = yield* bank.account.deposit(id, 100) return yield* bank.summary.get(id, { atLeast: receipt }) }) const summary = await Effect.runPromise(Effect.provide(program, BankLive)) console.log(summary) // { balance: 100 } ``` * `effectify(BankModel, 'Bank')` creates a named service key with a `layer`. * The layer requires the model's declared dependencies. Provide them with layers built from `effectify(Dependency)`. * A successful tuple becomes the Effect's value; its error becomes a typed failure. The bank domain is in the [Bank example](/examples/bank). ## Next See the [Effect API reference](/docs/api/effect). --- --- url: https://accord-docs-aqu.pages.dev/docs/api/schema.md --- # Schemas Schemas are runtime values. Use them in event, rejection, service, and contract shapes. Accord validates data at those boundaries and infers TypeScript types from them. ## Import ```ts import { integer, refine, s, string } from '@marcelsud/accord' ``` Every primitive is exported directly and on the `s` namespace (`s.integer`, `s.string`, ...). ## Usage ```ts import { array, integer, literal, nullable, optional, refine, string, union } from '@marcelsud/accord' const Cents = refine(integer, (n) => n > 0, 'positive cents') const Status = union(literal('PENDING'), literal('OPEN'), literal('CLOSED')) const Customer = { name: string, email: nullable(string), nickname: optional(string), tags: array(string), status: Status, address: { city: string, zip: string }, // shapes nest } ``` A plain object of schemas is a **shape**. Shapes reject unknown properties. ## Primitives | Schema | Accepts | | --- | --- | | `string` | Any string | | `number` | A finite number | | `integer` (also `s.int`) | A safe integer | | `boolean` | `true` or `false` | | `unknown` | Anything, unchecked | ## Combinators ### literal(value) Exactly `value` (string, number, boolean, or `null`). ### optional(schema) `schema` or `undefined`. In a shape, the property becomes optional. ### nullable(schema) `schema` or `null`. ### array(schema) An array whose items all match `schema`. ### union(...schemas) The first alternative that parses. ### refine(schema, check, description) `schema` plus a predicate. On failure the error says `Expected `. ### object(shape) Turns a shape into a `Schema`. You rarely need it: shapes work anywhere a schema does. ## parse(schema, value) Validate a value yourself. It throws a `TypeError` on failure. ```ts import { parse } from '@marcelsud/accord' parse(Cents, 100) // 100 parse(Cents, -1) // throws TypeError: Expected positive cents ``` ## Custom schemas Any object with a synchronous `parse(value: unknown): T` is a schema. ```ts import type { Schema } from '@marcelsud/accord' const Email: Schema = { parse(value) { if (typeof value === 'string' && value.includes('@')) return value throw new TypeError('Expected email') }, } ``` ## Infer Get the TypeScript type of a schema with `Infer`. ```ts import type { Infer } from '@marcelsud/accord' type Status = Infer // 'PENDING' | 'OPEN' | 'CLOSED' ``` --- --- url: https://accord-docs-aqu.pages.dev/docs/api/result.md --- # Results Accord operations return tuples. This page covers the helpers for building and consuming them. For the why, see [Tuples, not throws](/docs/concepts/results). ## Import ```ts import { fail, match, ok, trycatch, unexpected } from '@marcelsud/accord' import type { Result } from '@marcelsud/accord' ``` ## Result ```ts type Result = | readonly [value: T, error: null] | readonly [value: unknown, error: E] ``` Check `error !== null` to narrow the value. ## ok(value) ```ts ok({ balance: 100 }) // [{ balance: 100 }, null] ``` ## fail(error) ```ts fail(NotOpen()) // [null, { _tag: 'NotOpen' }] ``` ## trycatch(task) Converts thrown or rejected values into `Unexpected`. ```ts const [data, error] = trycatch(() => JSON.parse(text)) // sync function const [res, fetchError] = await trycatch(() => fetch(url)) // async function const [value] = await trycatch(Promise.resolve(3)) // promise ``` * Prefer passing a function when the work should start later. * `trycatch` doesn't flatten an existing tuple. Accord operations already return tuples; don't wrap them. ## match(value, cases) Exhaustive pattern matching on `_tag`. Handlers receive the narrowed value. ```ts const next = match(event, { Opened: () => ({ ...state, open: true }), Deposited: ({ amount }) => ({ ...state, balance: state.balance + amount }), }) ``` Missing or unknown cases fail type checking. Adding a new event or rejection points you at every `match` that needs updating. At runtime, an unhandled tag throws a `TypeError`. ## unexpected(cause) Builds an `Unexpected` error: an `Error` with `_tag: 'Unexpected'` and `cause`. ## Built-in error tags | Tag | Shape | When | | --- | --- | --- | | `Unexpected` | `Error & { _tag, cause }` | A throw, or an invalid runtime result | | `InvalidInput` | `{ _tag, message }` | Input failed a schema | | `Conflict` | `{ _tag, expected, actual }` | `expectedVersion` didn't match | | `CommandConflict` | `{ _tag, commandId }` | A `commandId` was reused with different input | | `ConsistencyTimeout` | `{ _tag, stream, version }` | An `atLeast` read couldn't catch up | | `Unauthorized`, `NotFound` | `{ _tag }` | HTTP transport | --- --- url: https://accord-docs-aqu.pages.dev/docs/api/events.md --- # Events and Rejections Events record what happened. Rejections explain why something didn't. Both are **tagged factories** with the same syntax. ## Import ```ts import { event, integer, refine, rejection } from '@marcelsud/accord' ``` ## Usage There are exactly two forms: with no shape, or with a shape. ```ts const Cents = refine(integer, (n) => n > 0, 'positive cents') const Opened = event('Opened') const Deposited = event('Deposited', { amount: Cents, balance: integer }) const NotOpen = rejection('NotOpen') const InsufficientFunds = rejection('InsufficientFunds', { balance: integer, requested: Cents, }) Opened() // { _tag: 'Opened' } Deposited({ amount: 100, balance: 100 }) // { amount: 100, balance: 100, _tag: 'Deposited' } InsufficientFunds({ balance: 100, requested: 150 }) ``` * No shape means no properties. * A shape fixes the property types **and validates them at runtime**. Invalid data throws a `TypeError`. * There's no schema-free property inference. * The tag is the name. You can't override `_tag`. ## Using them Emit events from an [aggregate](/docs/api/aggregate) and return rejections with `fail()`: ```ts deposit(amount: number) { if (!this.state.open) return fail(NotOpen()) this.emit(Deposited({ amount, balance: this.state.balance + amount })) // ... } ``` Subscribe to events in [views](/docs/api/view), [policies](/docs/api/policy), and [projections](/docs/api/projection) by passing the factory: `on(Deposited, handler)` or `on: [Deposited]`. ## Versioning event tags The tag is what gets persisted. When a payload changes incompatibly, give the new event a new tag and keep the old factory around for history: ```ts // v1.ts export const DealChanged = event('DealChanged', schemaV1) // v2.ts export const DealChanged = event('DealChangedV2', schemaV2) ``` See [Versioning and migrations](/docs/guides/migrations). --- --- url: https://accord-docs-aqu.pages.dev/docs/api/value-object.md --- # Value Objects Extend `vo(name, schema)` to get a validated value with domain methods. ## Import ```ts import { integer, vo } from '@marcelsud/accord' ``` ## Usage ```ts class Balance extends vo('Balance', integer) { inc(amount: number): Balance { return Balance.of(this.value + amount) } } const balance = Balance.of(100).inc(50) balance.snapshot() // 150 balance.snapshot((cents) => ({ cents })) // { cents: 150 } ``` ## API ### Class.of(value) Validates `value` against the schema and returns an instance of **your subclass**. Invalid values throw a `TypeError`. ### this.value The validated raw value (protected). It's a `structuredClone` copy of what you passed in. ### snapshot() Returns a copy of the raw value. ### snapshot(project) Copies the raw value and maps it with `project`. ### Class.valueObjectName The `name` you passed to `vo()`. ## Value objects and state Keep aggregate state plain. Use value objects for the arithmetic, then write raw values back: ```ts deposit(amount: number) { const balance = Balance.of(state.balance).inc(amount).snapshot() state.balance = balance // ... } ``` ::: tip Use raw values that round-trip through `structuredClone`. That's what Accord stores. ::: --- --- url: https://accord-docs-aqu.pages.dev/docs/api/aggregate.md --- # Aggregate An aggregate holds state and the methods that change it. A successful method commits its state and emitted events together. A rejection or exception discards everything. `aggregate()` has three forms. Pick the one that reads best for your domain. | Form | Signature | Use when | | --- | --- | --- | | [Class](#class-aggregates) | `aggregate(name, initialState)` | You like classes and `this.state` | | [Factory](#factory-aggregates) | `aggregate(name, initialState, factory, options?)` | Methods share a context or need services | | [Event-evolved](#event-evolved-aggregates) | `aggregate(name, contract, evolve)` | State is a fold of events; you want declared commands (and HTTP) | ## Import ```ts import { aggregate } from '@marcelsud/accord' ``` ## Class aggregates Extend `aggregate(name, initialState)` and write ordinary methods. The initial value types `this.state`. ```ts const Deposited = event('Deposited', { amount: Cents, balance: integer }) class Account extends aggregate('Account', { balance: 0 }) { deposit(amount: number) { const event = Deposited({ amount, balance: this.state.balance + amount }) this.state.balance = event.balance this.emit(event) return ok({ balance: this.state.balance }) } } ``` Give empty collections and nullable fields a type annotation when the default can't express the full type: ```ts class Cart extends aggregate('Cart', { items: [] as string[], coupon: null as string | null }) {} ``` ### Instance API | Member | Description | | --- | --- | | `Account.create()` | A fresh instance with a copy of the defaults | | `Account.restore(state)` | An instance seeded with a copy of `state` | | `this.state` | Protected working state | | `this.emit(event)` | Buffer an event (protected) | | `getState()` | A copy of the state | | `pullEvents()` | Drain the pending events | Direct calls on an instance mutate that local instance. That's handy for unit tests. For stored state, go through `wire()`. ::: warning Reserved names `get`, `events`, `getState`, `pullEvents`, `emit`, `constructor`, and `then` can't be used as method names. ::: ## Factory aggregates Pass a factory that receives `{ state, emit }` and returns the methods. They share one context. ```ts const Account = aggregate( 'Account', { status: 'PENDING' as 'PENDING' | 'OPEN', balance: 0 }, ({ state, emit, rules }) => ({ open() { state.status = 'OPEN' emit(Opened()) return ok(undefined) }, deposit(amount: number) { if (state.status !== 'OPEN') return fail(NotOpen()) if (amount > rules.maximum) return fail(DepositLimit({ maximum: rules.maximum })) state.balance += amount emit(Deposited({ amount, balance: state.balance })) return ok({ balance: state.balance }) }, }), { uses: { rules: DepositRules } } ) ``` ### Options #### uses `uses: Record`. Declared [services](/docs/api/service) arrive in the factory context next to `state` and `emit`. ### Standalone instances ```ts const account = Account.create( { status: 'OPEN', balance: 100 }, { rules: { maximum: 1_000 } } ) const [reply, error] = account.deposit(50) account.getState() // { status: 'OPEN', balance: 150 } account.pullEvents() // [{ _tag: 'Deposited', amount: 50, balance: 150 }] ``` `Account.create()` copies the defaults; `Account.create(state, services)` copies `state`. Pass `undefined` as the seed to start from defaults with services. ## Event-evolved aggregates Declare commands with contracts, then fold events into state with `evolve`. ```ts import { aggregate, command, event, fail, integer, match, ok, refine, rejection, s } from '@marcelsud/accord' const Cents = refine(integer, (n) => n > 0, 'positive cents') const State = s.object({ open: s.boolean, balance: integer }) const Opened = event('Opened') const Deposited = event('Deposited', { amount: Cents }) const AlreadyOpen = rejection('AlreadyOpen') const NotOpen = rejection('NotOpen') const open = command( { state: State, input: {}, events: [Opened], rejects: [AlreadyOpen] }, (state) => (state.open ? fail(AlreadyOpen()) : ok({ events: [Opened()] })) ) const deposit = command( { state: State, input: { amount: Cents }, events: [Deposited], rejects: [NotOpen], reply: { balance: integer } }, (state, { amount }) => { if (!state.open) return fail(NotOpen()) return ok({ events: [Deposited({ amount })], reply: { balance: state.balance + amount } }) } ) export const Account = aggregate( 'Account', { state: State, initial: () => ({ open: false, balance: 0 }), commands: { open, deposit } }, (state, event) => match(event, { Opened: () => ({ ...state, open: true }), Deposited: ({ amount }) => ({ ...state, balance: state.balance + amount }), }) ) ``` Calls take an input object: `bank.account.deposit('a-1', { amount: 100 })`. Input is validated and returns `InvalidInput` when it doesn't match. ### Command contract | Field | Type | | | --- | --- | --- | | `state` | `SchemaInput` | State schema | | `input` | `SchemaInput` | Input schema | | `events` | `Event[]` | Events the command may emit | | `rejects` | `Rejection[]` | Declared rejections | | `reply` | `SchemaInput` | Reply schema | | `uses` | `Record` | Services | The handler returns `ok({ events, reply? })` or `fail(rejection)`. Calling it directly (`deposit(state, input)`) validates and returns a decision without saving anything. You can also declare the aggregate contract once as `{ state, commands }` and implement each command with `command(AccountContract, 'deposit', body)`. Those commands keep their declared names and state schema. Only `command()` definitions can be exposed over [HTTP](/docs/api/http). ## Calls through the runtime ```ts const bank = wire(model('Bank', { aggregates: { account: Account } })) const [receipt, error] = await bank.account.deposit('a-1', 100) const [state] = await bank.account.get('a-1') const [envelopes] = await bank.account.events('a-1', { after: 0 }) ``` * Calls for one ID run in order. Each starts from a copy of stored state (or the defaults). * A successful call saves state, events, and a receipt together. * `get(id)` returns a copy of the stored state (the initial value for a fresh ID). * `events(id, { after })` returns committed [envelopes](/docs/concepts/delivery#the-commit) after a sequence number. ### Receipt | Field | Description | | --- | --- | | `reply` | What your method returned in `ok()` | | `events` | The committed envelopes | | `version` | Number of successful calls | | `eventCursor` | Last committed event sequence (class and factory aggregates) | | `commandId`, `stream` | Identity of the commit | Pass the receipt to `view.get(key, { atLeast: receipt })` to [read your own write](/docs/api/view#read-your-own-writes). ### Call options (event-evolved commands) #### commandId `string`. Deduplicates successful commits per instance. Reusing it with different input returns `CommandConflict`. #### expectedVersion `number`. Optimistic concurrency. A stale version returns `Conflict`. ```ts await bank.account.deposit('a-1', { amount: 100 }, { commandId: 'req-42', expectedVersion: 1 }) ``` ## See also * [Model and wire](/docs/api/model) * [Events and Rejections](/docs/api/events) --- --- url: https://accord-docs-aqu.pages.dev/docs/api/model.md --- # Model and wire `model()` registers your definitions. `wire()` turns a model into a typed runtime client. ## Import ```ts import { memory, model, wire } from '@marcelsud/accord' ``` ## model(name, contract) ```ts const Bank = model('Bank', { aggregates: { account: Account, savings: SavingsAccount }, views: { summary: Summary }, policies: [NotifyDeposit], projections: [Totals], workflows: { job: Job }, streams: { activity: Activity }, }) ``` ### aggregates `Record`. The keys are the client aliases: `bank.account`, `bank.savings`. ### views `Record`. Exposed as `bank.summary`. ### policies `Policy[]`. ### projections `Projection[]`. ### workflows `Record`. Exposed as `bank.workflows.job`. ### streams `Record`. Exposed as `bank.streams.activity`. Rules checked when you call `model()`: * Every definition name must be unique and non-empty. * Aliases must be unique and can't be `workflows`, `streams`, `flush`, or `then`. * Views and consumers must subscribe to a registered source, and only to events that source declares. * Versions are safe integers `>= 1`. ## wire(model, options?) ```ts const bank = wire(Bank, { storage: memory(), services: { rules: { maximum: 1_000 } }, }) ``` ### storage `RuntimeStore`. Defaults to a fresh `memory()` store. Pass the same `memory()` object to share state between runtimes in one process, or a [SQLite store](/docs/api/sqlite) for durability. ### services `Record`. **Required** when anything in the model declares `uses`. TypeScript tells you what's missing. ## The runtime | Member | Description | | --- | --- | | `bank..(id, ...args)` | Run a command. See [Aggregate](/docs/api/aggregate#calls-through-the-runtime) | | `bank..get(id)` / `.events(id)` | Read state / history | | `bank..get(key, options?)` | Read a view. See [View](/docs/api/view) | | `bank.workflows.(input)` | Run a [workflow](/docs/api/workflow) | | `bank.streams.` | Append to and read a [stream](/docs/api/stream) | | `bank.flush()` | Deliver pending events to consumers | ## flush() ```ts const [, error] = await bank.flush() if (error !== null) throw error ``` Catches all consumers up and returns the first delivery failure. Failed deliveries keep their checkpoint and retry on the next flush. There's no background scheduler or automatic backoff. ## memory() The default in-process store. It provides atomic aggregate commits, keyed concurrency, receipts, replay, and consumer checkpoints, and also holds workflow checkpoints. --- --- url: https://accord-docs-aqu.pages.dev/docs/api/view.md --- # View A view folds events into read state. It can rebuild from history at any time. ## Import ```ts import { on, view } from '@marcelsud/accord' ``` ## Usage ```ts const Summary = view( 'Summary', { from: Account, initial: { balance: 0, deposits: 0 } }, on(Deposited, (state, { balance }) => ({ balance, deposits: state.deposits + 1 })), on(Withdrawn, (state, { balance }) => ({ ...state, balance })) ) const Bank = model('Bank', { aggregates: { account: Account }, views: { summary: Summary } }) ``` Each handler receives `(state, event, envelope)` and returns the next state **synchronously**, so the same handler works during replay. ## Options ### from `Aggregate | View`. The source. Views can also listen to other views; downstream views consume the forwarded source events. ### initial The starting state for each row. It also supplies the view's state type. ### schema `SchemaInput`. Validate state at runtime. ### version `number`, default `1`. Bump it when the calculation changes to rebuild from history. Versions can't decrease within a store. ### keyOf `(envelope) => string | string[] | undefined`. Rows are keyed by the source instance ID by default. Return several keys to fan out, or `undefined` to skip the event. ```ts const ByCurrency = view( 'ByCurrency', { from: Account, initial: { total: 0 }, keyOf: ({ event }) => event.currency }, on(Deposited, (state, { amount }) => ({ total: state.total + amount })) ) ``` ## Reading ```ts const [summary, error] = await bank.summary.get('a-1') const [snapshot] = await bank.summary.snapshot('a-1') // { state, cursor } const [history] = await bank.summary.events('a-1', { after: 0 }) ``` ### Read your own writes Without options, `get()` returns what is materialized now, which may be `null`. Pass a command receipt to flush and catch up first: ```ts const [receipt] = await bank.account.deposit('a-1', 100) const [summary] = await bank.summary.get('a-1', { atLeast: receipt }) ``` ### Watch Native watchers are async generators of snapshot tuples. Pass an `AbortSignal` to stop. ```ts const controller = new AbortController() for await (const [snapshot, error] of bank.summary.watch('a-1', { signal: controller.signal })) { if (error !== null) break console.log(snapshot.state) } ``` ## Contract form `view(name, { from, on, state, initial, version?, keyOf? }, evolve)` is also supported. `evolve` is `(state, event) => state`, usually with [`match`](/docs/api/result#match-value-cases). ```ts const Summary = view( 'Summary', { from: Account, on: [Deposited, Withdrawn], state: { balance: integer }, initial: () => ({ balance: 0 }) }, (state, event) => match(event, { Deposited: ({ amount }) => ({ balance: state.balance + amount }), Withdrawn: ({ amount }) => ({ balance: state.balance - amount }), }) ) ``` --- --- url: https://accord-docs-aqu.pages.dev/docs/api/policy.md --- # Policy A policy says "when this happens, do that". It reacts to **new** events only and never replays history. ## Import ```ts import { on, policy } from '@marcelsud/accord' ``` ## Usage ```ts const NotifyDeposit = policy( 'NotifyDeposit', { from: Account, uses: { notifications: Notifications } }, on(Deposited, (event, { notifications }, { id, source }) => notifications.send({ eventId: id, accountId: source.id, amount: event.amount, balance: event.balance, }) ) ) const Bank = model('Bank', { aggregates: { account: Account }, policies: [NotifyDeposit] }) ``` Each handler receives: 1. the tagged event payload, 2. the declared services plus execution context, 3. the [envelope](/docs/concepts/delivery#the-commit). Omit parameters you don't use. Return a `Result` or a promise of one. Add more `on()` handlers to react to other events from the same source. ## Options ### from `Aggregate | View`. The source. ### uses `Record`. Services the handlers need. See [Service](/docs/api/service). ### version `number`, default `1`. ## Delivery * Policies run when `bank.flush()` is called. * New policies start at the current position. Existing policies resume where they left off. * They skip history, including events forwarded while a view rebuilds. * Delivery is at-least-once. Use the envelope `id` as an idempotency key. * Duplicate event tags and empty handler lists are rejected. ## Effect handlers ```ts on(Deposited, effectify((event, services, envelope) => Effect.succeed(undefined))) ``` See [Effect](/docs/api/effect). ## Contract form `policy(name, { from, on, uses? }, body)` is also supported. The body receives `(envelope, context)`. --- --- url: https://accord-docs-aqu.pages.dev/docs/api/projection.md --- # Projection A projection writes events somewhere else, in batches. It's replayable, so writes must be idempotent. ## Import ```ts import { projection, trycatch } from '@marcelsud/accord' ``` ## Usage ```ts const Totals = projection( 'Totals', { from: Account, on: [Deposited], batch: { size: 100 } }, (batch) => trycatch(() => { for (const { source, event } of batch) increment.run(source.id, event.amount) }) ) const Bank = model('Bank', { aggregates: { account: Account }, projections: [Totals] }) ``` The body receives an array of envelopes, batched per source instance, and returns a `Result`. ## Options ### from `Aggregate | View`. ### on `Event[]`. The events to receive. ### uses `Record`. ### version `number`, default `1`. A new version replays **all** source history. ### batch `{ size: number }`, default `{ size: 100 }`. ## Idempotency * Use stable event IDs in destination upserts. * Bumping the version replays history. Migrate or clear the destination first, or write to a new versioned destination, otherwise counters double. ::: tip Atomic SQL effects With the [SQLite store](/docs/api/sqlite), write through `storage.database` inside the handler. The batch's SQL changes and its checkpoint commit or roll back together. ::: --- --- url: https://accord-docs-aqu.pages.dev/docs/api/service.md --- # Service A service declares a typed dependency. You declare it once, list it in `uses`, and supply it in `wire()`. ## Import ```ts import { integer, service } from '@marcelsud/accord' ``` ## Usage ```ts const DepositRules = service('DepositRules', { maximum: integer }) const Account = aggregate( 'Account', { balance: 0 }, ({ state, emit, rules }) => ({ deposit(amount: number) { if (amount > rules.maximum) return fail(DepositLimit({ maximum: rules.maximum })) // ... }, }), { uses: { rules: DepositRules } } ) const bank = wire(Bank, { services: { rules: { maximum: 1_000 } } }) ``` ## Where `uses` goes | Definition | Where | Received as | | --- | --- | --- | | Factory aggregate | 4th argument `{ uses }` | factory context, `ctx.rules` | | Command | contract `uses` | context argument | | Policy / projection | contract `uses` | second handler argument | | External / workflow | contract `uses` | `ctx` / `run.services` | ## Rules * Service names identify dependencies and must be unique. * Each alias must refer to the same dependency throughout a model. * `actor`, `signal`, `commandId`, `at`, `idempotencyKey`, `services`, `step`, `wait`, and `sleep` are reserved aliases. * A dependency can be a `service()` or an [`external()`](/docs/api/external) operation. * Supplied service values are validated against the service schema. * Commands receive trusted metadata from the runtime. HTTP callers can't supply an actor or dependency bindings. ## With Effect `effectify(DepositRules)` returns a stable Effect service key: ```ts const RulesLive = Layer.succeed(effectify(DepositRules), { maximum: 1_000 }) ``` --- --- url: https://accord-docs-aqu.pages.dev/docs/api/external.md --- # External An external operation is a typed adapter contract for another system: an SDK, an HTTP API, a queue. ## Import ```ts import { external, ok, string } from '@marcelsud/accord' ``` ## Usage ```ts const Uppercase = external( 'Uppercase', { input: { value: string }, reply: { value: string } }, (input) => ok({ value: input.value.toUpperCase() }) ) const uppercase = Uppercase.implement({}) const [value, error] = await uppercase({ value: 'accord' }) // [{ value: 'ACCORD' }, null] ``` `operation.implement(services)` creates the callable adapter. It validates input, reply, declared rejections, and dependency bindings. ## Contract ### input `SchemaInput`. ### reply `SchemaInput`. ### rejects `Rejection[]`. Expected vendor refusals must map to one of these. ### uses `Record`. An external operation can itself be listed in another definition's `uses`. ## Calling a real SDK This excerpt assumes a `sdk` client and a `Bounced` rejection from your app. Wrap the call in `trycatch`. SDK exceptions become `Unexpected`; map expected refusals to declared rejections. ```ts const SendEmail = external( 'SendEmail', { input: { to: string }, rejects: [Bounced] }, async (input, ctx) => { const [res, error] = await trycatch(() => sdk.send(input, { signal: ctx.signal, idempotencyKey: ctx.idempotencyKey }) ) if (error !== null) return fail(error) if (res.bounced) return fail(Bounced()) return ok(undefined) } ) ``` Returning a rejection that isn't listed in `rejects` becomes `Unexpected`. Pass `ctx.signal` and `ctx.idempotencyKey` to vendors that support them. A signal can't stop a vendor call that ignores it. --- --- url: https://accord-docs-aqu.pages.dev/docs/api/workflow.md --- # Workflow A workflow runs checkpointed steps with compensation. In v0.1 its checkpoints live **in memory** only. ## Import ```ts import { fail, integer, ok, string, workflow } from '@marcelsud/accord' ``` ## Usage ```ts const Job = workflow( 'Job', { input: { id: string }, reply: { value: integer }, key: (input) => input.id }, async (_input, run) => { const [value, error] = await run.step('compute', () => ok(42)) if (error !== null) return fail(error) return ok({ value }) } ) const bank = wire(model('Bank', { aggregates: { account: Account }, workflows: { job: Job } })) const [result] = await bank.workflows.job({ id: 'j-1' }) // { value: 42 } ``` A run is identified by `key(input)`. Calls with the same key and input share its result. Conflicting input returns `CommandConflict`. ## Contract ### input `SchemaInput`. ### reply `SchemaInput`. ### key `(input) => string`. The run identity. ### rejects `Rejection[]`. ### uses `Record`. Available as `run.services`. ## The run context ### run.step(name, task, { compensate? }) Saves a successful result and reuses it on the next invocation. The task receives a stable `commandId` and `idempotencyKey`. Pass the command ID to contract-based commands and the idempotency key to external adapters. ### run.wait(name, schema) Waits for a named, validated signal. Send it with: ```ts await bank.workflows.job.signal('j-1', 'approved', { by: 'ana' }) ``` A signal can arrive before the `wait`. ### run.sleep(name, milliseconds) Records an in-memory deadline and waits for it. ## Failure and resume * Use distinct names for steps, waits, and timers within one run. * Put external I/O **inside** steps. Body code outside a step can run again on resume. * A failure runs registered compensations in reverse order. Failed compensations retry on the next call, without redoing successful steps. * An abort keeps step checkpoints and doesn't start compensation. * Compensation callbacks get their own stable operation IDs. ::: warning Checkpoints, signals, compensation status, and deadlines live in the shared `memory()` store. Reusing it across runtimes preserves them within one process. There's no disk-backed or distributed workflow engine in v0.1, and SQLite-backed models containing workflows are rejected. ::: --- --- url: https://accord-docs-aqu.pages.dev/docs/api/stream.md --- # Stream A stream is a keyed, append-only list of events that isn't owned by an aggregate. Think activity feeds and audit logs. ## Import ```ts import { stream, string } from '@marcelsud/accord' ``` ## Usage ```ts const Activity = stream('Activity', { key: string, event: { text: string } }) const bank = wire(model('Bank', { aggregates: { account: Account }, streams: { activity: Activity } })) await bank.streams.activity.append('a-1', { text: 'Opened' }, { id: 'evt-1' }) const [items] = await bank.streams.activity.list('a-1') // [{ id: 'evt-1', event: { text: 'Opened' }, seq: 1 }] ``` ## Contract ### key `Schema`. ### event `SchemaInput`. Validates appended events. ## Client ### append(key, event, { id? }) Returns `{ cursor }`. Appending the same `id` and event again is a no-op; the same `id` with a different event returns `CommandConflict`. ### list(key, { after? }) Returns `{ id, event, seq }[]`. ### watch(key, { signal? }) An async generator of snapshot tuples. Streams are native runtime APIs. They aren't exposed over [HTTP](/docs/api/http). --- --- url: https://accord-docs-aqu.pages.dev/docs/api/http.md --- # HTTP API `api()` exposes selected commands and views over HTTP. `serve()` turns it into a `fetch`-style handler, and `client()` gives you a typed remote client. ## Import ```ts import { api, client, fail, ok, rejection, serve, wire } from '@marcelsud/accord' ``` ## Usage ```ts const Unauthorized = rejection('Unauthorized') const PublicBank = api( 'PublicBank', { model: Bank, commands: [Account.commands.open, Account.commands.deposit], views: [Summary], rejects: [Unauthorized], }, (request) => { if (request.headers.get('authorization') !== 'Bearer example') { return fail(Unauthorized()) } return ok({ actor: 'example-user' }) } ) // Server const fetchHandler = serve(PublicBank, { runtime: wire(Bank) }) // Client const remote = client('https://your-server/api', { headers: { authorization: 'Bearer example' }, }) const [summary, error] = await remote.summary.get('a-1') ``` `Account` here is an [event-evolved aggregate](/docs/api/aggregate#event-evolved-aggregates): only `command()` definitions can be exposed. ::: warning The fixed token only illustrates the contract. Plug in your own authentication for a real endpoint. ::: ## api(name, contract, authorize?) ### model The model to expose. ### commands `Command[]`. Only these are exposed. An empty list exposes none. ### views `View[]`. ### rejects `Rejection[]`. Authorization failures must be listed here. ### uses `Record`. ### authorize `(request, ctx) => Result<{ actor? }, Rejection>`. Receives the operation, target, input, and headers. The returned `actor` is the trusted command actor. **Omitting it allows anonymous access** to the selected operations. ## serve(api, { runtime }) Returns `(request: Request) => Promise`. Mount it in any server that speaks `Request`/`Response`. Requests are `POST` with a JSON body: `{ kind, operation, target, input, options }`, where `operation` is `alias.method`. ## client(url, { fetch?, headers? }) A typed client for commands and views. Pass a custom `fetch` to test without a server: ```ts const remote = client('http://test/api', { fetch: (input, init) => fetchHandler(new Request(input, init)), }) ``` ## What's supported * Commands, and view `get`, `snapshot`, and `events`. * Not supported: live subscriptions, workflows, streams, WebSocket, SSE, OpenAPI, or MCP. * Server-side `Unexpected` causes and stacks are stripped. The client recreates an `Error` with the sanitized response as its cause. * Command selection is enforced by definition identity on the server. The static client type is structural, so commands with identical contract types may look the same to TypeScript, but the server still rejects every unlisted command. --- --- url: https://accord-docs-aqu.pages.dev/docs/api/story.md --- # Story `story()` runs a scenario against a fresh in-memory runtime and returns a tuple. Thrown assertions become `Unexpected`. ## Import ```ts import { fail, ok, story } from '@marcelsud/accord' ``` ## Usage ```ts const [value, error] = await story(Bank, async (test) => { const [, givenError] = await test.given(Account, 'a-1', [Opened()]) if (givenError !== null) return fail(givenError) const [receipt, commandError] = await test.bank.account.deposit('a-1', { amount: 100 }) if (commandError !== null) return fail(commandError) test.equal(receipt.reply.balance, 100) return ok(undefined) }) if (error !== null) throw error ``` ## The test context ### test.bank A wired runtime for the model. ### test.given(aggregate, id, events) Seeds an [event-evolved aggregate](/docs/api/aggregate#event-evolved-aggregates) with history. It validates and folds the events into a fresh instance. Seeded events rebuild views and projections but **don't** run policies. Class and factory aggregates can't be seeded this way; call their methods instead. ### test.equal(actual, expected) Deep strict equality. Throws on mismatch, which the story reports as `Unexpected`. ## Options `story(model, body, options?)` accepts the same options as [`wire()`](/docs/api/model#wire-model-options): `storage` and `services`. `services` is required when the model declares dependencies. ```ts await story(Bank, body, { services: { rules: { maximum: 1_000 } } }) ``` See [Testing](/docs/guides/testing). --- --- url: https://accord-docs-aqu.pages.dev/docs/api/sqlite.md --- # SQLite store `sqlite()` is a durable store built on Node.js 24's built-in `node:sqlite`. It adds no npm runtime dependency, and the core entry point never imports it. ## Import ```ts import { sqlite } from '@marcelsud/accord/sqlite' ``` ## Usage ```ts using storage = sqlite('app.sqlite') const bank = wire(Bank, { storage, services }) const [, error] = await bank.flush() if (error !== null) throw error ``` ## API ### sqlite(filename) Opens or creates `filename`. `':memory:'` is supported but doesn't survive the process. ### storage.database The underlying `DatabaseSync` connection. Use it inside projections to commit SQL effects with the checkpoint. ### storage.close() Closes the connection. Also called when a `using` scope ends. Closing with pending operations is rejected. ## What gets stored Aggregate snapshots and versions, immutable event envelopes, command receipts, view rows and forwarded history, consumer registrations, per-stream checkpoints, consistency watermarks, and explicit streams. * Event IDs keep their sequences across restarts. * Empty policy registrations are durable, so reopening doesn't skip events emitted before the first flush. * Values are stored with Node's V8 serialization, not JSON. Dates, maps, `bigint`, and `undefined` survive. Values must support both `structuredClone` and V8 serialization. * Storage format version `1` is checked on open. Newer unsupported formats are rejected. ## Transactions Each successful command commits its snapshot, events, and receipt in one transaction. Each consumer batch also runs in a transaction. Projection handlers that write through **the same `storage.database` connection** join it: ```ts storage.database.exec(` CREATE TABLE IF NOT EXISTS deposit_totals ( account_id TEXT PRIMARY KEY, total INTEGER NOT NULL ) STRICT `) const increment = storage.database.prepare(` INSERT INTO deposit_totals(account_id, total) VALUES (?, ?) ON CONFLICT(account_id) DO UPDATE SET total = total + excluded.total `) const Totals = projection('Totals', { from: Account, on: [Deposited] }, (batch) => trycatch(() => { for (const { source, event } of batch) increment.run(source.id, event.amount) }) ) ``` * Return `ok()` only after all writes finish. Return `fail()` or throw to roll back the batch. * Don't issue `BEGIN`, `COMMIT`, or `ROLLBACK` in the handler. The runtime commits your writes and the checkpoint together. ## Recovery | Failure point | Recovery | | --- | --- | | After a command commits, before flush | The next process reads the stored events and delivers them | | During a shared-connection projection write | SQLite rolls back; the batch stays pending | | During checkpoint update or `COMMIT` | Projection changes and checkpoint both roll back | | After the batch commits, before success is reported | The saved checkpoint prevents applying it again | ::: warning These guarantees cover SQL effects on the same connection, for the same model, consumer name, and version. External HTTP calls, other connections (even to the same file), and other systems don't join the transaction. Use stable event IDs and idempotent destinations for those. ::: ## Concurrency * WAL mode with `synchronous=FULL`. * Runtime operations are serialized per file within a process. SQLite's writer lock prevents conflicting commits across processes; a conflicting process may get an `Unexpected` busy error and should retry. * Keep handlers short: a handler holds the writer transaction while it runs. * A handler must not call another runtime operation on the same SQLite file. Nested transactions are rejected. * Watchers are local to their connection. Use reads or `flush()` to observe other connections. ## Rebuilds Reopen with stable names and versions for normal recovery. Changing a projection or view version intentionally replays history. No automatic schema migration, retention, or rebuild orchestration is included. Models with workflows are rejected on SQLite. See [Versioning and migrations](/docs/guides/migrations). --- --- url: https://accord-docs-aqu.pages.dev/docs/api/effect.md --- # Effect `@marcelsud/accord/effect` adapts Accord to [Effect](https://effect.website) v4.1.2+ in the v4 line. ## Import ```ts import { effectify } from '@marcelsud/accord/effect' ``` ## effectify overloads | Call | Returns | | --- | --- | | `effectify(Model, 'Name')` | A named Effect service key with a `layer` | | `effectify(Model)` | An Effect that resolves dependencies and builds a client | | `effectify(Dependency)` | A stable Effect service key for a `service()` or `external()` | | `effectify(command)` | `(state, input) => Effect` running the command decision with its services from the environment | | `effectify(handler)` | An Effect-returning handler for `on()`, projections, workflows, and externals | ## Model as a service ```ts const Bank = effectify(BankModel, 'Bank') const RulesLive = Layer.succeed(effectify(DepositRules), { maximum: 1_000 }) const BankLive = Bank.layer.pipe(Layer.provide(RulesLive)) const program = Effect.gen(function* () { const bank = yield* Bank yield* bank.account.open('a-1') const receipt = yield* bank.account.deposit('a-1', 100) return yield* bank.summary.get('a-1', { atLeast: receipt }) }) await Effect.runPromise(Effect.provide(program, BankLive)) ``` * The layer requires the model's declared dependencies. * Effects that yield the same service from one layer provision share a runtime and its in-memory store. Use distinct names for distinct model services. ## Direct acquisition ```ts const program = Effect.gen(function* () { const bank = yield* effectify(BankModel) // ... }) ``` ## Semantics * Calls that return a promise of a tuple become lazy Effects. The value becomes the Effect result; the error becomes a typed failure. * Watch methods still return async generators. * If a caller is interrupted while waiting on a native call, that call may still finish in the runtime. * Native dependency shapes keep their tuple results when called from inside a generator. ## Effect handlers Wrap a handler with `effectify` and get services through their Effect keys: ```ts const NotifyDeposit = policy( 'NotifyDeposit', { from: Account, uses: { notifications: Notifications } }, on( Deposited, effectify((event, _services, { id, source }) => Effect.gen(function* () { const notifications = yield* effectify(Notifications) const [, error] = yield* Effect.promise((signal) => notifications.send( { eventId: id, accountId: source.id, amount: event.amount, balance: event.balance }, { signal } ) ) if (error !== null) return yield* Effect.fail(error) return undefined }) ) ) ) ``` `uses` declares the Effect requirements the handler may use. A typed Effect failure becomes a rejection; interrupts and defects become `Unexpected`. From the [Notifications example](/examples/notifications). --- --- url: https://accord-docs-aqu.pages.dev/docs/guides/testing.md --- # Testing Accord code is easy to test because everything returns values. You have three levels. ## 1. Unit test an aggregate directly No runtime needed. Create an instance, call a method, check the state and events. ```ts import { test } from 'node:test' import assert from 'node:assert/strict' import { Account } from './domain.ts' test('deposit adds to the balance', () => { const account = Account.create({ status: 'OPEN', balance: 100 }, { rules: { maximum: 1_000 } }) const [reply, error] = account.deposit(50) assert.equal(error, null) assert.deepEqual(reply, { balance: 150 }) assert.deepEqual(account.getState(), { status: 'OPEN', balance: 150 }) assert.deepEqual(account.pullEvents(), [{ _tag: 'Deposited', amount: 50, balance: 150 }]) }) ``` Use `Account.restore(state)` for class aggregates. ## 2. Run a story [`story()`](/docs/api/story) wires a fresh in-memory runtime and turns failed assertions into `Unexpected`. ```ts test('deposit through the runtime', async () => { const [, error] = await story( Bank, async (t) => { const [, openError] = await t.bank.account.open('a-1') if (openError !== null) return fail(openError) const [receipt, depositError] = await t.bank.account.deposit('a-1', 100) if (depositError !== null) return fail(depositError) const [summary] = await t.bank.summary.get('a-1', { atLeast: receipt }) t.equal(summary, { balance: 100 }) return ok(undefined) }, { services: { rules: { maximum: 1_000 } } } ) assert.equal(error, null) }) ``` ## 3. Fake external systems Services are plain values, so pass a fake through `wire()`: ```ts const sent: unknown[] = [] const bank = wire(Bank, { services: { notifications: { send: async (message) => { sent.push(message) return ok(undefined) }, }, }, }) ``` For HTTP, give [`client()`](/docs/api/http#client-url-fetch-headers) a custom `fetch` that calls your `serve()` handler directly. No server needed. ## Run tests Accord itself uses Node's built-in test runner: ```sh node --test ``` --- --- url: https://accord-docs-aqu.pages.dev/docs/guides/migrations.md --- # Versioning and migrations Accord stores events forever and keeps checkpoints per consumer name and version. Changing contracts is your job, and Accord gives you the tools to do it explicitly. ## What happens when you change things | Change | Result | | --- | --- | | Change an aggregate contract without migrating snapshots | Stored state keeps the old shape; explicit validation fails | | Change projection code but keep its version | Existing checkpoints prevent replay | | Bump a projection version while assuming new payloads | Replay fails on old events | | Bump a counter projection version and reuse its table | Replayed increments double the total | | Subscribe only to a new event tag | Legacy events are filtered out | These are all asserted in the [CRM example](/examples/crm). ## New event, new tag The tag is persisted. Keep the old factory and add a new tag: ```ts // v1.ts export const DealChanged = event('DealChanged', schemaV1) // v2.ts export const DealChanged = event('DealChangedV2', schemaV2) ``` Inside v2, subscribe to both and adapt old events when reading them: ```ts projection('Pipeline', { from: Deal, on: [V1.DealChanged, DealChanged], version: 2 }, body) ``` ## Rebuild into a new destination Bump the view or projection version **and** write to a fresh destination (a new table such as `pipeline_v2`). Replays then rebuild correctly. ::: warning View versions can't decrease within a store. Starting an old app version against an upgraded file is not a rollback plan. Restore the pre-migration database instead. ::: ## Snapshot migrations Snapshot migrations are offline and application-owned: 1. Stop application writers. 2. Back up the database. 3. Check the Accord storage format (`1`), update state while preserving versions and receipts, create the new projection destination, and record a migration ID, all in one transaction. 4. Start the new version, which rebuilds views and projections at version `2`. Keep policy names and versions unchanged so their delivery progress is preserved. ::: info Accord doesn't ship an automatic migration runner or an upcaster registry yet. The CRM example shows the explicit code against the current storage layout. ::: --- --- url: https://accord-docs-aqu.pages.dev/docs/guides/best-practices.md --- # Best Practices Accord is flexible. These habits keep your models simple and safe. ## Always check the error slot ```ts const [receipt, error] = await bank.account.deposit('a-1', 100) if (error !== null) return fail(error) // or handle it ``` Never test the value for truthiness. `null` and `undefined` are valid success values. ## Reject, don't throw Return `fail(SomeRejection())` for business refusals. Throws become `Unexpected` and lose their meaning for callers. Validate inputs before constructing events, because invalid event data throws. ## Keep state plain Store plain data that round-trips through `structuredClone` (and JSON if you use HTTP). Use [value objects](/docs/api/value-object) for logic, then write raw values back to state. ## Name things once Definition names, service names, and event tags are identities. They show up in stream IDs, checkpoints, and persisted data. Don't rename them casually; version them. ## Make consumers idempotent Delivery is at-least-once. Use the envelope `id` as the idempotency key for external calls, and upserts for projections. ## Put I/O inside workflow steps Code outside `run.step()` can run again on resume. ## Flush deliberately There's no background worker. Call `bank.flush()` where it makes sense: after a request, on a timer you own, or via `atLeast` reads. ## Keep handlers short on SQLite A consumer holds the writer transaction while it runs. Do slow external work in policies with idempotency keys rather than inside a shared-connection projection. --- --- url: https://accord-docs-aqu.pages.dev/docs/guides/limits.md --- # Limits of v0.1 Accord's first version is deliberately small. Here's what it does and doesn't do. ## Included * In-process runtime with a default memory store: atomic aggregate commits, keyed concurrency, receipts, replay, and consumer checkpoints. * Optional SQLite store with restart recovery and database transactions for commands and consumer batches. * HTTP transport for commands and view reads. * Optional Effect integration (Effect v4.1.2+). ## Not included (yet) * **npm release.** Install from a local `npm pack` tarball for now. * **Durable workflows.** Workflow state is memory-only; SQLite models with workflows are rejected. * **Background scheduling.** No automatic flush, retry backoff, or timers outside a running process. * **Migrations and retention.** No migration orchestration, upcaster registry, or retention policy. Histories grow forever. * **Distributed transactions.** Nothing spans external systems. * **Global ordering.** Ordering is per source stream only. * **Transports.** No WebSocket, SSE, OpenAPI, or MCP. Live subscriptions, workflows, and streams aren't on HTTP. * **License.** The package is marked `UNLICENSED`; no open-source license has been selected yet. ## Data rules * Stored domain data must support `structuredClone` (and V8 serialization on SQLite). * HTTP data must also round-trip through JSON. --- --- url: https://accord-docs-aqu.pages.dev/docs/guides/faq.md --- # Frequently Asked Questions ## Is Accord on npm? Not yet. Run `npm pack` in the repository and install the tarball. See [Getting Started](/docs/getting-started/basic). ## Do I need Effect? No. The core has zero runtime dependencies. Effect is an optional peer for `@marcelsud/accord/effect`. ## Which Node.js version? 24 or newer. Accord uses `node:sqlite` and other Node 24 features. ## Class, factory, or event-evolved aggregate? Start with a class. Use a factory when methods need [services](/docs/api/service) or a shared context. Use an [event-evolved aggregate](/docs/api/aggregate#event-evolved-aggregates) when you want state to be a fold of events, declared command contracts, `commandId`/`expectedVersion`, or HTTP exposure. ## Why does my view return `null`? Views materialize on `flush()`. Read with `{ atLeast: receipt }` to catch up first. ## Why did my policy run twice? Delivery is at-least-once. Make the action idempotent with the envelope `id`. ## Why did I get `Unexpected` instead of my error? Something threw, or you returned a rejection that isn't declared in the contract's `rejects`, or event data failed its schema. Check `error.cause`. ## Can I use it in the browser or on the edge? The core is plain ESM, but it is built and tested for Node.js 24. `serve()` returns a standard `Request => Response` handler. Other runtimes aren't documented yet. --- --- url: https://accord-docs-aqu.pages.dev/docs/guides/contributing.md --- # Contributing Accord lives at [github.com/marcelsud/accord](https://github.com/marcelsud/accord). Issues and pull requests are welcome. ## Develop ```sh git clone https://github.com/marcelsud/accord.git cd accord npm ci --ignore-scripts npm run check ``` | Script | What it does | | --- | --- | | `npm run typecheck` | Positive assertions and `@ts-expect-error` inference tests | | `npm test` | Build, compile tests, and run Node's built-in test runner | | `npm run build` | ESM and declaration files in `dist/` | | `npm run check` | Format check, typecheck, and tests | ## Docs This site lives in `website/` and uses [VitePress](https://vitepress.dev). ```sh cd website npm install npm run docs:dev ``` --- --- url: https://accord-docs-aqu.pages.dev/examples.md --- # Examples Runnable Accord apps, from a tiny bank to a durable CRM with contract migrations. Each one runs on Node.js 24 with no external accounts. The source lives in the [accord-examples](https://github.com/marcelsud/accord-examples) repository. | Example | What it shows | | --- | --- | | [Bank](/examples/bank) | Factory aggregates, value objects, a view, services, and the Effect version | | [Notifications + SQLite ledger](/examples/notifications) | Policies, projections, external system contracts, real HTTP adapters | | [Durable bank](/examples/durable-bank) | Restart recovery and atomic SQL projections across processes | | [CRM and migrations](/examples/crm) | Several aggregates, policies, a durable pipeline, and contract versioning | ## Run them ```sh git clone https://github.com/marcelsud/accord-examples.git cd accord-examples npm ci npm run build npm run bank ``` ::: info Moving to a separate repository The examples are being moved out of the main repository. Until that happens, run them from [`accord/examples`](https://github.com/marcelsud/accord/tree/main/examples): `npm ci --ignore-scripts && npm run check`, then `npm run example`, `npm run example:effect`, `npm run example:sqlite`, `npm run example:sqlite:effect`, `npm run example:durable`, and `npm run example:crm`. ::: --- --- url: https://accord-docs-aqu.pages.dev/examples/bank.md --- # Bank A factory aggregate with a status and a balance in cents, a `Balance` value object, a view, and deposit rules supplied as a service. Same domain, two runners: native tuples and Effect. Source: [`bank/`](https://github.com/marcelsud/accord-examples/tree/main/bank) ## Run it ```sh npm run bank npm run bank:effect ``` The native runner prints: ```txt Balance view: { balance: 100 } Account snapshot: { status: 'OPEN', balance: 100 } Savings snapshot: { balance: { cents: 100 } } Standalone savings preview: { balance: 150 } ``` The Effect runner prints `{ balance: 100 }`. ## The domain ```ts [bank/domain.ts] export const DepositRules = service('DepositRules', { maximum: Cents }) class Balance extends vo('Balance', integer) { get cents() { return this.value } inc(amount: number): Balance { return Balance.of(this.cents + parse(Cents, amount)) } } export const Account = aggregate( 'Account', { status: Status.of('PENDING'), balance: 0 }, ({ state, emit, rules }) => ({ open() { if (state.status !== 'PENDING') return fail(CannotOpen({ status: state.status })) state.status = Status.of('OPEN') emit(Opened()) return ok(undefined) }, deposit(amount: number) { if (state.status !== 'OPEN') return fail(NotOpen()) if (amount > rules.maximum) return fail(DepositLimit({ maximum: rules.maximum })) const balance = Balance.of(state.balance).inc(amount).cents state.balance = balance emit(Deposited({ amount, balance })) return ok({ balance }) }, }), { uses: { rules: DepositRules } } ) ``` `SavingsAccount` uses the same value object with a nested `{ balance: { cents } }` state. ## The native runner ```ts [bank/pure.ts] const bank = wire(Bank, { services: { rules: { maximum: 1_000 } } }) const [, openError] = await bank.account.open('a-1') if (openError !== null) throw openError const [receipt, error] = await bank.account.deposit('a-1', 100) if (error !== null) throw error const [summary] = await bank.summary.get('a-1', { atLeast: receipt }) console.log('Balance view:', summary) // { balance: 100 } ``` It also seeds `SavingsAccount.create(snapshot)` from stored state and previews a deposit locally. ## Standalone test ```ts const account = Account.create({ status: 'OPEN', balance: 100 }, { rules: { maximum: 1_000 } }) const [reply, error] = account.deposit(50) console.log(reply) // { balance: 150 } console.log(account.pullEvents()) // [{ _tag: 'Deposited', amount: 50, balance: 150 }] ``` ## Files | File | What it does | | --- | --- | | [`domain.ts`](https://github.com/marcelsud/accord-examples/tree/main/bank/domain.ts) | Aggregates, value object, rejections, view, model | | [`pure.ts`](https://github.com/marcelsud/accord-examples/tree/main/bank/pure.ts) | Native runner | | [`with-effect.ts`](https://github.com/marcelsud/accord-examples/tree/main/bank/with-effect.ts) | Effect runner with `effectify(Bank, 'Bank')` and a rules layer | --- --- url: https://accord-docs-aqu.pages.dev/examples/notifications.md --- # Notifications + SQLite ledger Open an account, deposit money, record a ledger entry in SQLite, and ask an external notification system to tell the account holder. The domain only knows two contracts: `Notifications.send` and `LedgerStore.record`. Source: [`notifications/`](https://github.com/marcelsud/accord-examples/tree/main/notifications) ## Run it ```sh npm run notifications npm run notifications:effect ``` Each prints the mode, a committed balance of `100`, one ledger row, and `Notifications sent: 1`. The runner starts a dummy notification service on a loopback port. No public service or account needed. Pass a fresh filename to inspect the ledger afterwards: ```sh npm run notifications -- /tmp/accord-pure.sqlite ``` ## The flow ```ts const [, openError] = await bank.account.open('a-1') if (openError !== null) throw openError const [receipt, error] = await bank.account.deposit('a-1', 100) if (error !== null) throw error console.log(receipt.reply.balance) // 100 const [, deliveryError] = await bank.flush() if (deliveryError !== null) throw deliveryError ``` 1. A method updates state and emits `Opened` or `Deposited`. State and events commit together. 2. `flush()` delivers events to the ledger projection and the notification policy. 3. The policy uses the envelope's stable event ID as the notification's idempotency key. ## Swap in a fake ```ts const notifications: NotificationPort = { async send(input) { console.log('Notification requested:', input.accountId) return ok({ id: `fake-${input.eventId}` }) }, } const { bank } = createBank(ledger, notifications) ``` ## Adapters * The HTTP adapters send the event ID as an idempotency key, validate acknowledgements, time out after five seconds, and forward cancellation. * Conflicts become `NotificationConflict`; failed responses become `NotificationsUnavailable`; network and decoding failures become `Unexpected`. * The SQLite adapter upserts by event ID, so replays are safe. A failed statement rolls back the whole batch. ::: warning The default runner persists the **ledger only**. Aggregate state and checkpoints stay in memory. For durable state, pass a SQLite runtime store and share its connection with the ledger: ```ts using storage = sqlite('bank.sqlite') using ledger = openLedger(storage.database) const { bank } = createBank(ledger, notifications, storage) ``` ::: ## Files | File | Responsibility | | --- | --- | | [`domain.ts`](https://github.com/marcelsud/accord-examples/tree/main/notifications/domain.ts) | Account, rejections, external system contracts | | [`pure.ts`](https://github.com/marcelsud/accord-examples/tree/main/notifications/pure.ts) | Native projection, policy, and model | | [`with-effect.ts`](https://github.com/marcelsud/accord-examples/tree/main/notifications/with-effect.ts) | Effect projection, policy, and model | | [`adapters/notifications.ts`](https://github.com/marcelsud/accord-examples/tree/main/notifications/adapters/notifications.ts) | Native notification adapter | | [`adapters/notifications-effect.ts`](https://github.com/marcelsud/accord-examples/tree/main/notifications/adapters/notifications-effect.ts) | Effect notification adapter | | [`adapters/dummy-notifications.ts`](https://github.com/marcelsud/accord-examples/tree/main/notifications/adapters/dummy-notifications.ts) | Dummy notification service | | [`adapters/sqlite.ts`](https://github.com/marcelsud/accord-examples/tree/main/notifications/adapters/sqlite.ts) | SQLite ledger | | [`run.ts`](https://github.com/marcelsud/accord-examples/tree/main/notifications/run.ts) | Binds adapters, wires the model, runs it | --- --- url: https://accord-docs-aqu.pages.dev/examples/durable-bank.md --- # Durable bank Commit a deposit in one process, deliver it in another. A counter projection writes through the runtime's SQLite connection, so its SQL change and checkpoint commit together and nothing is counted twice. Source: [`durable-bank/`](https://github.com/marcelsud/accord-examples/tree/main/durable-bank) ## Run it Use a new filename for a fresh demo: ```sh npm run durable -- /tmp/accord-runtime.sqlite deposit 100 npm run durable -- /tmp/accord-runtime.sqlite drain npm run durable -- /tmp/accord-runtime.sqlite drain ``` Both drains print a projected total of `100`. ## The projection ```ts using storage = sqlite(filename) storage.database.exec(` CREATE TABLE IF NOT EXISTS deposit_totals ( account_id TEXT PRIMARY KEY, total INTEGER NOT NULL ) STRICT `) const increment = storage.database.prepare(` INSERT INTO deposit_totals(account_id, total) VALUES (?, ?) ON CONFLICT(account_id) DO UPDATE SET total = total + excluded.total `) const Totals = projection( 'Totals', { from: Account, on: [Deposited], batch: { size: 100 } }, (batch) => trycatch(() => { // The runtime owns this transaction and commits its checkpoint with these writes. for (const { source, event } of batch) increment.run(source.id, event.amount) }) ) const bank = wire(model('DurableBank', { aggregates: { account: Account }, projections: [Totals] }), { storage }) ``` `deposit` commits and exits without flushing. `drain` calls `bank.flush()` and prints the table. It reuses the `Account` from the [Notifications example](/examples/notifications). See the [SQLite store reference](/docs/api/sqlite) for the recovery table. --- --- url: https://accord-docs-aqu.pages.dev/examples/crm.md --- # CRM and migrations Companies, contacts, and deals. Qualify a contact, open a deal, and move it from discovery through proposal to won. Policies create follow-up and onboarding tasks, a view serves the deal card, and a projection maintains a searchable SQLite pipeline. Then v2 changes the contracts and migrates. Source: [`crm/`](https://github.com/marcelsud/accord-examples/tree/main/crm) ## Run v1 ```sh npm run crm -- /tmp/accord-crm.sqlite seed npm run crm -- /tmp/accord-crm.sqlite drain npm run crm -- /tmp/accord-crm.sqlite drain ``` * `seed` commits the sales flow without flushing consumers. * The first `drain` restores state and delivers pending events: one won pipeline row for 2,400,000 minor BRL units and two task records. * Repeated drains change nothing. `show` reads without draining. ## Run the migrations After the v1 seed and drain: ```sh npm run crm:migrations -- /tmp/accord-crm.sqlite ``` The script opens the database read-only and runs every experiment on SQLite backups. v2 replaces `amount` and `currency` with `money: { minor, currency }` and adds a required `channel`. Migrated records get `LEGACY`; new v2 deals get `INBOUND`. ## Versioned event tags ```ts // v1.ts export const DealChanged = event('DealChanged', schemaV1) // v2.ts export const DealChanged = event('DealChangedV2', schemaV2) ``` The v2 projection subscribes to `[V1.DealChanged, DealChanged]`. ## What it proves | Experiment | Result | | --- | --- | | Change the aggregate contract without migrating snapshots | Stored state keeps the old shape; v2 validation fails | | Change projection code but keep its version | Existing checkpoints prevent replay | | Bump the version while assuming new payloads | Replay fails: old events lack `details.money` | | Change `CREATE TABLE IF NOT EXISTS` | Existing columns don't change | | Subscribe only to the new event tag | Legacy events are filtered out | | Bump a counter version and reuse its destination | Totals double; a fresh destination rebuilds correctly | | Fail halfway through a snapshot migration | Everything rolls back; retry succeeds | | Migrate snapshots and adapt old events during replay | The old deal rebuilds into v2 without repeating tasks | | Restart with mixed history | Two pipeline rows and three tasks stay stable | See [Versioning and migrations](/docs/guides/migrations) for the rules behind this. ## Files | File | What it does | | --- | --- | | [`v1.ts`](https://github.com/marcelsud/accord-examples/tree/main/crm/v1.ts) | Full CRM, SQLite adapters, seed/drain/show CLI | | [`v2.ts`](https://github.com/marcelsud/accord-examples/tree/main/crm/v2.ts) | New contracts, history readers, v2 projection | | [`migrations.ts`](https://github.com/marcelsud/accord-examples/tree/main/crm/migrations.ts) | Backup-based migration and failure experiments | --- --- url: https://accord-docs-aqu.pages.dev/docs.md --- # Accord Accord is a **composable domain modeling** library for TypeScript. *Simplifying DDD for the rest of us.* You define business rules, record what happened, and describe what should happen next. Business failures are plain tagged values. Every operation returns `[value, null]` or `[null, error]`. Effect is optional. ```ts import { aggregate, event, integer, model, ok, refine, wire } from '@marcelsud/accord' const Cents = refine(integer, (amount) => amount > 0, 'positive cents') const Deposited = event('Deposited', { amount: Cents, balance: integer }) class Account extends aggregate('Account', { balance: 0 }) { deposit(amount: number) { const event = Deposited({ amount, balance: this.state.balance + amount }) this.state.balance = event.balance this.emit(event) return ok({ balance: this.state.balance }) } } const Bank = model('Bank', { aggregates: { account: Account } }) const bank = wire(Bank) const [receipt, error] = await bank.account.deposit('account-123', 100) if (error !== null) throw error console.log(receipt.reply) // { balance: 100 } ``` That's it. `wire()` gives you a typed client. Each call loads the stored state, runs your method on an isolated instance, and commits state and events together. ## Quick Start Accord isn't on npm yet. Build a tarball from the repository and install it: ```sh git clone https://github.com/marcelsud/accord.git cd accord npm ci --ignore-scripts npm pack ``` Then follow [Getting Started](/docs/getting-started/basic). ## Features * **Small API**: `aggregate`, `event`, `rejection`, `view`, `policy`, `projection`, `model`, `wire`. That's most of it. * **Typed tuples**: no hidden throws. Rejections are values, and `match()` checks you handled every one. * **Runtime schemas**: `integer`, `string`, `refine`, `union`, and friends validate events, rejections, and inputs at runtime. * **Read your own writes**: `view.get(key, { atLeast: receipt })` catches a view up to your command. * **Durable when you need it**: `@marcelsud/accord/sqlite` persists state, history, views, and checkpoints with Node.js's built-in SQLite. * **Zero runtime dependencies**: Effect is an optional peer, used only by `@marcelsud/accord/effect`. * **Testable**: `story()` runs a scenario against a fresh in-memory runtime and returns a tuple. ## Entry points | Import | What you get | | --- | --- | | `@marcelsud/accord` | The core: schemas, results, definitions, runtime, HTTP transport, stories | | `@marcelsud/accord/sqlite` | The `sqlite()` durable store (Node.js 24 built-in `node:sqlite`) | | `@marcelsud/accord/effect` | `effectify()` for Effect v4.1.2+ | ::: warning Version 0.1 This is the first version. The runtime is in-process, workflows are memory-only, and no npm release has been published yet. See [Limits of v0.1](/docs/guides/limits). ::: ## Next * [Getting Started](/docs/getting-started/basic): install and run your first aggregate. * [Motivation](/docs/concepts/motivation): why Accord looks the way it does. * [Examples](/examples/): bank, notifications, durable bank, and a CRM with migrations. --- --- url: https://accord-docs-aqu.pages.dev/IA.md --- # Accord docs IA (mirrors hono.dev) Top nav: Docs (/docs/) · Examples (/examples/) · Runtime reference (GitHub docs/runtime.md) · GitHub icon Sidebar /docs/ (collapsible groups): * Concepts: motivation, results (Tuples, not throws), delivery (Events and delivery) * Getting Started (expanded): basic, sqlite, effect * API: schema, result, events, value-object, aggregate, model, view, policy, projection, service, external, workflow, stream, http, story, sqlite, effect * Guides: testing, migrations, best-practices, limits, faq, contributing * Coding Agents: /llms.txt, /llms-full.txt Sidebar /examples/: index, bank, notifications, durable-bank, crm, Source on GitHub Example source links use `examples:` and are rewritten from EXAMPLES_BASE in .vitepress/config.ts.