Quickstart
Define a contract, react to its events, and make your first durable append in five minutes.
What you'll build
An order that notifies a shop when it arrives: one contract, one consumer, two routes, and a log you can read back.
1. Install
npm install experimental-a2 zod
2. Define a contract
A contract is a name plus the events it understands, each with a schema:
import { z } from 'zod'
import * as a2 from 'experimental-a2'
export const orders = a2.contract({
name: 'orders',
events: {
'created': z.object({ shopId: z.string(), items: z.array(z.string()) }),
'shop.notified': z.object({}),
},
})3. Implement it
A consumer reacts to events, and createServer binds the contract to storage
and its consumers. Same file, below the contract:
import { createServer } from 'experimental-a2/server'
export const ordersServer = createServer({
contract: orders,
consumers: [
{
name: 'notify-shop',
types: ['created'],
run: async ({ event }) => {
// your side effect, e.g. email the shop, idempotent via event.id:
// await sendEmailToShop(event.payload, { idempotencyKey: event.id })
return { type: 'shop.notified', payload: {} }
},
},
],
})nameis the consumer's identity: A2 keeps its place in every session under it.typeslists the events it runs on.- Consumers can run more than once (at-least-once delivery), so make external
calls safe to repeat.
event.idis stable across re-runs: use it as the idempotency key. - The returned event commits atomically with the consumer's success. If
runthrows or the process dies first,shop.notifieddoesn't appear. Return one event or an array.
Development needs no database setup: events go to SQLite at .a2/dev.db and
survive restarts. Tests get an in-memory store. Production needs an explicit
store; see Stores.
4. Append the first event
import { ordersServer } from '@/server/orders'
export async function POST(req: Request) {
const { orderId, shopId, items } = await req.json()
await ordersServer.session(orderId).append({
type: 'created',
payload: { shopId, items },
})
return Response.json({ ok: true })
}session(orderId) is one session, one instance of the
contract. The id is yours, and creating the handle does no I/O.
curl -X POST localhost:3000/api/orders \
-H 'content-type: application/json' \
-d '{ "orderId": "order-42", "shopId": "shop-7", "items": ["flat white"] }'The payload is validated before anything is written; a bad one fails with
INVALID_PAYLOAD and the issues.
When append returns, the event is durably in the log and A2 has started the
notify-shop consumer inline. In production, a queue-backed scheduler wakes pending
work after a failure or process death. See Guarantees.
5. Read the log back
import { ordersServer } from '@/server/orders'
export async function GET(
_req: Request,
{ params }: { params: Promise<{ orderId: string }> },
) {
const { orderId } = await params
const events = await ordersServer.session(orderId).history()
return Response.json(events)
}curl localhost:3000/api/orders/order-42[
{ "index": 1, "type": "created", "payload": { "shopId": "shop-7", "items": ["flat white"] } },
{ "index": 2, "type": "shop.notified", "payload": {} }
]Each event also carries id, sessionId, and createdAt (trimmed here). You
appended one event and the consumer returned the second. Debugging in A2 is
reading the log.
6. Fold state out of the log
UIs usually want a view, not raw history. Define a reducer once:
import { reducer } from 'experimental-a2'
import { orders } from '@/server/orders'
export const ordersReducer = reducer({
contract: orders,
name: 'order-status',
initialState: { status: 'new' },
}).fold((state, event) => {
switch (event.type) {
case 'created':
return { ...state, status: 'created' }
case 'shop.notified':
return { ...state, status: 'notified' }
default:
return state
}
})Then fold wherever you need the current picture:
// anywhere on the server:
import { ordersServer } from '@/server/orders'
import { ordersReducer } from '@/reducer'
const { state } = await ordersServer.session('order-42').state(ordersReducer)
// state → { status: 'notified' }State isn't a second record to update. It's a fold over the log; see State.
That's it
You have a validated write path, a consumer, a log you can read, and a view folded from it. The rest of these docs apply the same two moves, append an event and react to it, to timers, cancellation, live UIs, and production.