---
title: Quickstart
description: 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 handler, two
routes, and a log you can read back.

## 1. Install

```package-install
npm i experimental-a2 zod
```

## 2. Define a contract

A contract is a name plus the events it understands, each with a schema:

```ts server/orders.ts
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

`createServer` binds the contract to storage and handlers. Same file, below
the contract:

```ts server/orders.ts
import { createServer } from 'experimental-a2/server'

export const ordersServer = createServer({
  contract: orders,
  handlers: {
    created: 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: {} }
    },
  },
})
```

- Handlers can run more than once (at-least-once delivery), so make external
  calls safe to repeat. `event.id` is stable across re-runs: use it as the
  idempotency key.
- The returned event commits atomically with handler success. If the handler
  throws or the process dies first, `shop.notified` doesn'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](/stores/overview).

## 4. Append the first event

```ts app/api/orders/route.ts
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](/contract/sessions), one instance of the
contract. The id is yours, and creating the handle does no I/O.

```bash
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
`created` handler inline. In production, a queue-backed scheduler wakes pending
work after a failure or process death. See [Guarantees](/server/guarantees).

## 5. Read the log back

```ts app/api/orders/[orderId]/route.ts
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)
}
```

```bash
curl localhost:3000/api/orders/order-42
```

```json
[
  { "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 handler 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:

```ts reducer.ts
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:

```ts
// 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](/contract/state).

## That's it

You have a validated write path, a handler, 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.

<CardGroup cols={3}>
  <Card title="Handlers" href="/server/handlers" icon="file">
    Context, chaining, and re-runs.
  </Card>
  <Card title="Stores" href="/stores/overview" icon="rocket">
    Where sessions live in production.
  </Card>
  <Card title="Schedulers" href="/schedulers/overview" icon="clock">
    Retries and timers on a queue.
  </Card>
</CardGroup>
