Skip to main content
Glama
README.md
# OpenBook

Open-source appointment booking for small businesses (barbershops, salons, studios), built for
voice. Customers book, reschedule and cancel by talking to Alexa+ or any other MCP client; the
assistant talks to OpenBook through a **Model Context Protocol server**.

> 🧑 "Book a fade with Marcus at Fade Studio Saturday afternoon."
> 🔊 "Marcus has three times available Saturday afternoon: 12:30 PM, 1:15 PM, or 3 PM. Which one
> works for you?"
>
> _(a real reply from the simulated Alexa+: Claude on Amazon Bedrock calling `list_services` and
> `search_availability` on the OpenBook MCP server)_

Built for the Amazon Developer Hackathon (Alexa+ track, AWS Builder and Open Source challenges).

## Status

OpenBook is being built in milestones. This section is kept honest: only what is listed as done
exists and is tested.

| #   | Milestone                                                                       | Status       |
| --- | ------------------------------------------------------------------------------- | ------------ |
| 1   | Foundation: monorepo, Postgres schema with exclusion constraints, seed data, CI | ✅ Done      |
| 2   | Core domain: availability, holds, bookings, reschedule, cancel, idempotency     | ✅ Done      |
| 3   | MCP server (Streamable HTTP, 8 tools, dev auth)                                 | ✅ Done      |
| 4   | Simulated Alexa+ (`/assistant`, Bedrock agent + MCP client)                     | ✅ Done      |
| 5   | Owner dashboard                                                                 | ✅ Done      |
| 6   | Notifications + outbox worker                                                   | ✅ Done      |
| 7   | Cognito OAuth + AWS CDK deployment + connect to real Alexa+ (MCP Toolkit)       | ✅ Deployed² |
| 8   | Stretch: Bedrock Knowledge Base FAQ, Google Calendar                            | ✅ Built¹    |
| 9   | Polish: README, architecture, demo script, friction log, product feedback       | ✅ Done      |
| 10  | Store finder: live "salons near me" search (OpenBook + real-world shops)        | ✅ Done      |

¹ Code and tests done; the Knowledge Base is created on AWS deploy, and Google Calendar needs a
Google OAuth client, so neither has been exercised against the live service yet.

² Deployed to AWS (us-east-1) on 2026-09-24: API health, the MCP OAuth 401 challenge and
metadata, migrations + seed, and the web app verified live. The Alexa+ registration still needs
the project owner's Alexa developer account.

Architecture: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). Every design decision:
[docs/DECISIONS.md](docs/DECISIONS.md).

What works today:

- **End to end by voice:** `docker compose up`, open `http://localhost:3000/assistant`, hold the
  button and talk. The simulated Alexa+ (Claude Haiku 4.5 on **Amazon Bedrock**, Converse API
  with tool use) books, reschedules and cancels through the OpenBook MCP server, answers questions
  from the business's policies, and shows every MCP tool call with its arguments and result.
  Verified live: book → parking question → reschedule → list → cancel with the $10 late fee
  quoted first.
- **Owner dashboard** at `http://localhost:3000/dashboard` (local "Dev login" as Fade Studio's
  owner): day/week calendar by staff member with cancel and no-show, services, staff with
  services and weekly hours, time off, policies and FAQ, settings with the MCP endpoint URL and a
  copy button. Backed by an owner REST API (`/api/v1`, same server as MCP) that authorises every
  request against `business_owners`. Changes reach the voice assistant immediately. A Playwright
  test covers "owner creates a service → it appears in `list_services`".
- **Store finder (live):** choose "Any shop near me" on `/assistant` and ask "find me a barber
  near Williamsburg that can do a shave today". OpenBook shops come back with distance, open now and
  the next free slot (bookable right there), plus real-world shops from **Amazon Location Service**
  (name, distance, hours, phone; not bookable). Served by the directory MCP endpoint `/mcp`.
- **Notifications:** every booking, reschedule and cancellation queues a message in the outbox;
  the worker sends a confirmation SMS and email (Amazon SES / AWS End User Messaging in AWS, logged
  locally), retrying with backoff and never sending a channel twice.
- `docker compose up` starts Postgres 16, applies migrations, seeds two demo businesses, and runs
  the MCP server (:3001) and web app (:3000).
- The database itself rejects double bookings, overlapping holds, bookings inside another
  appointment's buffer, and cross-tenant references (see [Data integrity](#data-integrity)).
- **MCP server** at `http://localhost:3001/mcp/{businessSlug}`: Streamable HTTP, stateless, JSON
  responses, built on the official MCP TypeScript SDK v2. It serves MCP 2026-07-28 and the
  2025-era protocol (including 2025-11-25). All 8 tools are listed in
  [docs/MCP_TOOLS.md](docs/MCP_TOOLS.md). Tools return speakable text plus structured data, and
  stable error codes.
- OAuth 2.1 resource-server behaviour: `401` + RFC 9728 challenge, JWT validation against a JWKS
  (issuer, expiry, audience/client id), per-business protected-resource metadata. Locally,
  `DEV_AUTH` signs in as the demo customer Jen. Real Cognito sign-in is milestone 7.
- Verified with **MCP Inspector 2.8.0** (tools/list, tools/call) against the Docker Compose stack.
- `packages/core` holds every booking rule: availability (working hours, buffers, time off,
  notice, horizon, DST-safe), 5-minute holds, booking, rescheduling, cancellation with the late
  fee, idempotent writes, a transactional outbox, service/staff matching for spoken requests, and
  policy-based FAQ answers.
- 165 tests, including 20 concurrent holds for one slot (exactly one wins), concurrent retries with
  one idempotency key (one booking), tenant isolation, a full book → reschedule → cancel flow
  driven by the SDK client over Streamable HTTP in both protocol eras, and the Bedrock ↔ MCP agent
  bridge against the real server (with a scripted model, so CI needs no AWS account).

Not yet: notifications are queued in the outbox but not sent (milestone 6), the owner dashboard
(milestone 5), and AWS deployment with Cognito sign-in (milestone 7). The assistant signs in as
the demo customer Jen with the development token.

## Quick start

Requirements: Docker. For the voice assistant, AWS credentials with Amazon Bedrock access to a
Claude model (your `~/.aws` profile is mounted read-only, or export `AWS_PROFILE` /
`AWS_ACCESS_KEY_ID` etc.). For development also Node.js 24 and pnpm 10.

```sh
docker compose up
```

Then open **http://localhost:3000/assistant** (Chrome or Edge for speech recognition; any browser
works with the text box), hold the button and say "Book a fade with Marcus Saturday afternoon."

This starts Postgres on `localhost:54329`, runs migrations, seeds demo data (only the first time;
your bookings survive restarts), and starts the MCP server on `localhost:3001` and the web app on
`localhost:3000`. The default model is `us.anthropic.claude-haiku-4-5-20251001-v1:0` in
`us-east-1`; set `BEDROCK_MODEL_ID` / `AWS_REGION` to change it. Without Bedrock access the page
still loads and says why it can't answer.

The MCP server works on its own too:

```sh
curl localhost:3001/healthz
npx @modelcontextprotocol/inspector --cli http://localhost:3001/mcp/fade-studio --transport http \
  --header "Authorization: Bearer dev-token-jen-0123456789" --method tools/list
```

To wipe and reseed:

```sh
SEED_RESET=true docker compose up
```

### Demo data

| Business    | Slug          | Time zone           | Staff        | Services                                                                               |
| ----------- | ------------- | ------------------- | ------------ | -------------------------------------------------------------------------------------- |
| Fade Studio | `fade-studio` | America/New_York    | Marcus, Dana | Fade (45 min, $35), Beard Trim (20 min, $15), Cut & Beard (60 min + 5 min buffer, $45) |
| Glow Salon  | `glow-salon`  | America/Los_Angeles | Priya        | Blowout (45 min, $40), Haircut (60 min + 10 min buffer, $65)                           |

Fade Studio is open Tuesday–Saturday, 9 AM–6 PM, has a 24-hour cancellation window with a $10
late fee, welcomes walk-ins, and has free street parking on Elm St. The upcoming Saturday is
partly booked so availability looks realistic. Demo customer **Jen** (OAuth subject `dev|jen`)
exists in both businesses as separate customer records.

## Architecture

```mermaid
flowchart LR
  subgraph Browser
    A["/assistant<br/>push-to-talk (Web Speech API)<br/>speech synthesis · Tool calls panel"]
  end
  subgraph web["apps/web (Next.js)"]
    R["POST /api/assistant<br/>agent loop"]
  end
  subgraph AWS
    B["Amazon Bedrock<br/>Claude · Converse API + tool use"]
  end
  subgraph mcp["apps/mcp-server (Hono + MCP SDK v2)"]
    M["POST /mcp/{businessSlug}<br/>Streamable HTTP · stateless · JSON<br/>OAuth 2.1 resource server"]
    T["8 tools<br/>get_business_info · list_services · search_availability · hold_slot<br/>create_booking · find_my_bookings · reschedule_booking · cancel_booking"]
  end
  C["packages/core<br/>availability · holds · bookings<br/>idempotency · policies"]
  D[("PostgreSQL 16<br/>exclusion constraints<br/>outbox")]
  X["Any MCP client<br/>(Alexa+, MCP Inspector, desktop assistants)"]

  A -- "what the customer said" --> R
  R -- "Converse (messages + tool specs)" --> B
  B -- "toolUse" --> R
  R -- "MCP tools/list, tools/call<br/>Bearer token" --> M
  X -- "MCP over Streamable HTTP" --> M
  M --> T --> C --> D
```

Each spoken turn: the page sends the transcript to `/api/assistant`, which asks Bedrock what to
do. The model's tool calls go to the MCP server exactly as any MCP client's would, and the reply
is spoken back. The panel under each reply lists those MCP calls.

## Deploy to AWS

`infra/` is an AWS CDK app that deploys everything as one stack: API Gateway + Lambda (MCP
server and owner API), a Lambda container for the web app, RDS PostgreSQL, Cognito, the outbox
worker on an EventBridge schedule, a Bedrock Knowledge Base, and migrations that run on every
deploy. Architecture and trade-offs: [docs/DECISIONS.md](docs/DECISIONS.md) (D39).

```sh
pnpm install
cd infra
npx cdk bootstrap                      # once per account/region
npx cdk deploy \
  -c ownerBootstrap=you@example.com:fade-studio \
  -c sesFromEmail=you@example.com       # optional: SES sender (verify the email when asked)
# add -c sesIdentityExists=true if that email is already a verified SES identity
```

Requirements: AWS credentials, Docker (to build the web image), Bedrock access to Claude Haiku
4.5 in the region (default `us-east-1`). Outputs: `WebUrl` (dashboard + `/assistant`),
`McpEndpoint`, `McpDirectoryEndpoint` (register this one with the Alexa+ MCP Toolkit),
`CognitoDomain`, `AlexaClientId`. Sign up on the web URL with the `ownerBootstrap` email to manage
Fade Studio. `npx cdk destroy` removes it all (the database leaves a final snapshot).

### What it costs (us-east-1, on-demand, from the AWS Price List API, September 2026)

Always-on baseline, before any traffic:

| Resource                               | Price                                           | Per month    |
| -------------------------------------- | ----------------------------------------------- | ------------ |
| RDS PostgreSQL db.t4g.micro, single-AZ | $0.016/hour                                     | $11.68       |
| RDS gp3 storage, 20 GB                 | $0.115/GB-month                                 | $2.30        |
| NAT instance t4g.micro                 | $0.0084/hour                                    | $6.13        |
| Public IPv4 address (NAT instance)     | $0.005/hour                                     | $3.65        |
| Secrets Manager, 2 secrets             | $0.40/secret                                    | $0.80        |
| Bedrock Knowledge Base index storage   | $5.00/GB-month (policy documents are kilobytes) | ~$0.00       |
| CloudWatch Logs, ECR image, S3, SSM    | small                                           | < $1         |
| **Baseline**                           |                                                 | **≈ $25–26** |

Per use:

| What                                          | Price                                                                                   | Example                                                                                                        |
| --------------------------------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| Claude Haiku 4.5 on Bedrock (US cross-region) | $1.10 / M input tokens, $5.50 / M output; cache reads $0.11 / M                         | One spoken turn ≈ 2–3 model calls ≈ 5–14k input tokens → **≈ $0.01–0.02**; a full booking conversation ≈ $0.05 |
| Amazon Location Service                       | Geocode (Core) $0.50 / 1,000; Search Text with contact details (Advanced) $1.50 / 1,000 | One "barber near Williamsburg" search ≈ $0.002                                                                 |
| Knowledge Base retrieval                      | $1.00 / 1,000 Retrieve calls                                                            | per FAQ question ≈ $0.001                                                                                      |
| API Gateway HTTP API                          | $1.00 / million requests                                                                | negligible                                                                                                     |
| Lambda (arm64)                                | $0.20 / million requests + $0.0000133 / GB-second                                       | negligible at demo scale                                                                                       |
| Cognito Lite                                  | first 10,000 monthly active users free, then $0.0055 / MAU                              | $0 for a demo                                                                                                  |
| SES email                                     | $0.16 / 1,000 emails (plan-dependent)                                                   | negligible                                                                                                     |

A hackathon month with a few hundred test conversations comes to **roughly $25–35**. Deleting the
stack stops all charges except the final database snapshot (~$0.095/GB-month). The Knowledge Base
prices are from the Bedrock pricing page; everything else is from the AWS Price List API.

## Development

```sh
npm install -g pnpm@10      # or: corepack enable (needs admin rights on Windows)
pnpm install
cp .env.example .env
docker compose up -d        # Postgres + migrations + seed

pnpm test                   # unit + integration tests (starts its own Postgres via Testcontainers)
pnpm lint && pnpm typecheck && pnpm format:check
pnpm db:migrate             # apply migrations to DATABASE_URL
pnpm db:seed                # seed DATABASE_URL (add -- --reset to reseed)
```

Integration tests need Docker. To use an existing Postgres instead, set `TEST_DATABASE_URL` to
an admin connection string (the user must be allowed to `CREATE DATABASE`).

## Repository layout

```
apps/
  mcp-server/         # MCP server (+ owner REST API): Hono, MCP SDK v2, OAuth resource server
  web/                # Next.js: /assistant (simulated Alexa+) and /dashboard (owners)
  worker/             # Outbox delivery (email, SMS, Google Calendar) and maintenance sweeps
packages/
  db/                 # SQL migrations, migration runner, Kysely types, seed, test helpers
  core/               # Domain logic: availability, holds, bookings, policies, store finder
  notifications/      # SES / End User Messaging / Google Calendar clients, templates
  shared/             # Zod schemas and types shared across apps
infra/                # AWS CDK stack
docs/
  ARCHITECTURE.md     # Components, request flows, data integrity, deployment
  MCP_TOOLS.md        # Every tool: description, inputs, outputs, examples, errors
  DEMO_SCRIPT.md      # The 3-minute demo, line by line
  PRODUCT_FEEDBACK.md # Feedback on Bedrock and the other AWS services used
  DECISIONS.md        # Architecture decisions and deviations from the brief
  FRICTION_LOG.md     # Every tool/SDK/doc problem hit while building
```

## Data integrity

All instants are stored as `timestamptz` in UTC. Each business has an IANA time zone, validated
by a trigger, and working hours are local wall-clock times in that zone.

| Guarantee                                                   | Enforced by                                                                                |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| No two confirmed bookings for the same staff member overlap | `EXCLUDE USING gist (staff_id WITH =, blocked_range WITH &&) WHERE (status = 'confirmed')` |
| No two active holds for the same staff member overlap       | Same, on `holds`, `WHERE (status = 'active')`                                              |
| Buffers after an appointment are respected                  | Constraints run on `blocked_range` = appointment + buffer                                  |
| A hold becomes at most one booking                          | `UNIQUE (hold_id)` on `bookings`                                                           |
| Idempotency keys are unique per business                    | `UNIQUE (business_id, key)`                                                                |
| No cross-tenant references                                  | Composite foreign keys on `(business_id, id)`                                              |

Tested directly: 20 concurrent inserts for the same slot result in exactly one booking.

## License

[MIT](LICENSE)