# Build a booking API

Run the examples/bookings app end to end: migrate, seed, serve, start the worker, and book a space over HTTP.

You will run `examples/bookings`, a space marketplace where hosts list spaces and guests book them. It takes about 15 minutes. You need Go 1.27+, the `zever` CLI, and a repo clone.

All commands run from `examples/bookings/` so `.env` and the relative `data/` path resolve.

```sh
cd examples/bookings
```

## Read the schema

The whole domain lives in one file: the `User`, `Space`, `Booking`, and `Review` entities, the `BookingService` RPCs, two jobs (`SendConfirmation`, `SendReminder`), and a `Reminder` cron schedule. See [Zen syntax](https://zenta-dev.github.io/zever/reference/zen-syntax/) to extend it.

```sh
cat schema/bookings.zen
```

## Migrate the database

Generated code is committed under `generated/`; never hand-edit it, regenerate with `zever compile`. The migration creates the sqlite database:

```sh
zever db migrate --adapter=sqlite --dsn=data/bookings.db schema/bookings.zen
```

Checkpoint:

```text
OK applied 8 statement(s) via sqlite
```

If you run `zever doctor` from another directory, the db check reports a path miss, so run it from here.

## Seed demo data

The seed is idempotent:

```sh
go run ./db/seed
```

Checkpoint:

```text
{"time":"...","level":"INFO","msg":"seed complete"}
```

It creates `host@example.com` and `guest@example.com`, two spaces, and one confirmed booking.

## Set a JWT secret and serve

The app fails closed when `AUTH_JWT_SECRET` is missing or under 32 bytes ([Configuration](https://zenta-dev.github.io/zever/guides/operate/configuration/)). Export one (never commit a real one):

```sh
export AUTH_JWT_SECRET=dev-only-secret-at-least-32-bytes-long
go run ./cmd/server
```

Checkpoint (`-addr` changes the port):

```text
{"time":"...","level":"INFO","msg":"http server listening","addr":":8080"}
```

With a missing or short secret, the process exits at startup with an error naming the field instead of serving insecurely.

## Start the worker

In a second terminal, export the same secret and start the worker (jobs plus scheduler):

```sh
export AUTH_JWT_SECRET=dev-only-secret-at-least-32-bytes-long
go run ./cmd/worker
```

Checkpoint:

```text
{"time":"...","level":"INFO","msg":"worker started","jobs":2,"schedules":1}
```

The worker consumes the `default` and `low` queues and fires the `Reminder` cron (`0 9 * * *`). `SendConfirmation` sends a mail receipt (`mailer/log`), a guest push (`notification/log`), and a `booking.created` webhook to any configured target (skipped quietly when none; the app README lists `BOOKINGS_WEBHOOK_*`). `SendReminder` notifies guests of confirmed bookings starting within 7 days.

## Call the API

Register a guest:

```sh
curl -s -X POST localhost:8080/api/register \
  -H 'Content-Type: application/json' \
  -d '{"email":"you@example.com","password":"correct horse battery staple"}'
```

```text
{"id":"01J00000000000000000000000","email":"you@example.com"}
```

Log in. The response holds a JWT for every other route:

```sh
curl -s -X POST localhost:8080/api/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"you@example.com","password":"correct horse battery staple"}'
```

```text
{"token":"eyJhbGciOi..."}
```

List spaces, then book one:

```sh
TOKEN=... # from the login response

curl -s localhost:8080/api/spaces -H "Authorization: Bearer $TOKEN"
```

```text
[{"id":"...","host_id":"...","title":"...","price_cents":...},{"id":"...","host_id":"...","title":"..."}]
```

```sh
curl -s -X POST localhost:8080/api/bookings \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"space_id":"...","start_date":"2026-01-01","end_date":"2026-01-03"}'
```

```text
{"booking":{"id":"...","space_id":"...","status":"confirmed",...},"message":"...","payment_id":"..."}
```

The response is HTTP 201. Repeat the same request and you get the first result back with HTTP 200 (idempotent replay).

## Trace one booking through the stack

A successful booking request passes, in order, through:

1. `auth`: the bearer JWT is verified and attached to the context.
2. `ratelimit`: the per-guest quota is checked; over quota returns 429.
3. `idempotency`: a retried POST with the same `Idempotency-Key` header (or `idempotency_key` body field, or the same guest, space, and dates) replays the first result.
4. `lock`: the space date range is held so overlapping bookings cannot both confirm (409 `dates already booked`).
5. `payment`: a stub charge (no real funds), then the booking row is saved as `confirmed`.
6. `queue`: a `bookings.confirm` message is pushed. The `memory` queue is per process, so the separate worker does not receive it; set `queue` to a shared adapter such as `redis` in both processes to hand work across.

## Understand the doctor failures

```sh
zever doctor
```

Expect 30 OK and 4 FAIL: `ai` (no API key), `auth` (no JWT secret), `geo` (missing fixture data), and `i18n` (missing embed FS). These are deliberate zero-infra gaps, not framework bugs; the app `README.md` lists them.

## What you built

One HTTP request touched auth, rate limiting, idempotency, locking, payment, the ORM, and the queue, while the worker ran the job and schedule handlers.

<details>
<summary>Troubleshooting</summary>

**The server exits immediately naming `AUTH_JWT_SECRET`.** The secret is missing or under 32 bytes. Export a longer dev-only value.

**`curl` returns 401 on `/api/spaces`.** The `Authorization: Bearer $TOKEN` header is missing, or the token came from a different secret. Log in again in the terminal where you exported the secret.

**A booking returns 429 or 409.** 429 means the rate limit bit; wait out the window. 409 means the dates overlap an existing booking, or the same booking is still in progress.
</details>

## Next steps

- [Explore the showcase app](https://zenta-dev.github.io/zever/tutorials/explore-the-showcase-app/): Every DSL feature in one schema, over HTTP and gRPC.
- [Providers and adapters](https://zenta-dev.github.io/zever/concepts/providers-adapters/): Why swapping payment: stub for payment: stripe is the only change a real deployment needs.
