# 2026-04-30 — Bootstrap & Exclusion-Constraint Spike > Snapshot at `2026-04-30T17:12Z`. Companion to `Initial.md` (planning) at `/Users/noise/Documents/obsidian/Massage/Initial.md`. ## Milestone The five bootstrap tasks from the kickoff plan are complete. The repository is initialized, Postgres is running under docker-compose, the schema-managed migration includes the two `EXCLUDE USING gist` constraints that prevent therapist and room double-booking at the database level, and a Vitest suite of seven tests proves the constraints behave as specified. This is the foundation everything else builds on per `Initial.md` §9 step 1. ## What's verified - `pnpm install` — clean - `pnpm lint` — clean (0 warnings, 0 errors) - `pnpm exec tsc --noEmit` — clean - `pnpm test` — **7/7 tests green** - `docker-compose up -d postgres mailpit` — healthy on first or second try (see "Gotchas") - `pnpm exec prisma migrate dev` — applies cleanly; constraints visible via `\d "Booking"` ## Repo layout ``` touchbase/ ├── caddy/Caddyfile # prod reverse proxy (profile: prod) ├── compose.yaml # postgres + mailpit always; app + caddy under prod profile ├── db/init/01-extensions-and-test-db.sql # creates touchbase_test, installs btree_gist + pgcrypto ├── docs/progress/ # this dir — milestone snapshots ├── prisma/ │ ├── migrations/20260430170142_booking_spike/migration.sql │ ├── schema.prisma # Booking model only; full schema lands later │ └── ... ├── scripts/db-bootstrap.sh # idempotent fallback for the bind-mount race ├── src/ │ ├── app/ # Next.js App Router (untouched starter for now) │ ├── generated/prisma/ # generated, gitignored │ └── lib/db.ts # singleton PrismaClient with PrismaPg adapter ├── test/ │ ├── booking-exclusion.test.ts # 7 tests │ └── setup.ts # loads .env.test and refuses non-test DBs ├── .env, .env.test, .env.example ├── compose.yaml, Caddyfile (above) ├── package.json, pnpm-workspace.yaml, .nvmrc ├── prisma.config.ts, vitest.config.ts └── eslint.config.mjs, tsconfig.json, next.config.ts (defaults) ``` ## Decisions ratified this session | Decision | Resolution | |---|---| | Project name | **TouchBase** | | Code path | `/Users/noise/Documents/code/touchbase/` (outside Obsidian vault) | | Email transport | SMTP via `nodemailer`. Dev: Mailpit (`localhost:1025`). Prod: Resend | | Email provider chosen between Resend and Postmark | **Resend** (assistant's call, not user-overridden) | | Node engines | `>=22`; `.nvmrc` pinned to `22` | | Package manager | pnpm 10 | | Currency | Assumed USD (`priceCents`) — pending user confirmation | | `APP_TZ` placeholder | `America/Los_Angeles` — pending user confirmation | ## Stack snapshot (resolved versions) - Next.js 16.2.4 (App Router) + React 19.2.4 + Tailwind 4.2.4 - Prisma 7.8.0 with the new `prisma-client` generator (TS-native client at `src/generated/prisma/`) - `@prisma/adapter-pg` 7.8.0 + `pg` 8.20.0 — required by Prisma 7 (no built-in connection) - Vitest 4.1.5 + dotenv-cli 9.0.0 + zod 4.3.6 - Postgres 16-alpine with `btree_gist` and `pgcrypto` extensions - Mailpit (latest) for dev SMTP - Caddy 2-alpine for prod TLS/proxy ## Database state Two databases on the single Postgres container: - `touchbase_dev` — used by `pnpm dev` and `prisma migrate dev` - `touchbase_test` — used by `pnpm test`; reset between test files Both have `btree_gist` and `pgcrypto` extensions installed. Migrations are applied to both via separate flows (`prisma migrate dev` for dev, `dotenv-cli -e .env.test -- prisma migrate deploy` for test). Migration `20260430170142_booking_spike` creates: - `BookingStatus` enum: `HOLD | CONFIRMED | COMPLETED | NO_SHOW | CANCELLED` - `Booking` table (no FKs yet — opaque text IDs for `customerId`, `therapistId`, `roomId`, `serviceId`) - 4 btree indexes for query patterns we expect - **`Booking_no_therapist_overlap`** — `EXCLUDE USING gist` on `(therapistId, tstzrange(startsAt, endsAt))`, scoped to active statuses - **`Booking_no_room_overlap`** — same shape but on `(roomId, tstzrange(startsAt, roomReleasedAt))` so the post-service buffer is enforced `roomReleasedAt = endsAt + service.bufferAfterMin`. Stored on the row so the constraint is single-table without a join. ## Test cases (all passing) In `test/booking-exclusion.test.ts`: 1. Non-overlapping bookings on the same therapist succeed 2. Overlapping bookings on the same therapist are rejected by the DB (raw SQLSTATE 23P01) 3. Two therapists, same time slot, different rooms — both succeed 4. Room buffer is enforced — second booking inside the buffer window is rejected 5. A booking starting exactly when the room is released is allowed (range is `[)`) 6. CANCELLED bookings do not block new bookings on the same slot 7. HOLD blocks CONFIRMED in the same slot — race-condition safety net ## Gotchas hit and how they were resolved ### 1. Docker Desktop bind-mount race on first Postgres start The `/docker-entrypoint-initdb.d/*` glob expanded to the literal pattern (zero matches) the first time the container started, so init scripts were skipped — `touchbase_test` wasn't created and extensions weren't installed. **Fix:** `docker-compose down -v` and re-up resolved it. As a permanent safety net, `scripts/db-bootstrap.sh` is idempotent and re-applies the same setup. Wired up as `pnpm db:bootstrap`. The README of the spike directory (this doc, until we have a real README) is the place this lives. ### 2. Prisma 7 dropped `datasourceUrl` and `datasources` constructor options `new PrismaClient({ datasourceUrl: ... })` — the pattern that worked through Prisma 6 — now throws `PrismaClientConstructorValidationError`. Prisma 7 requires a driver adapter. **Fix:** added `@prisma/adapter-pg` and `pg`, and `PrismaClient` is now constructed as: ```ts new PrismaClient({ adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }) }) ``` Encapsulated in `src/lib/db.ts`. Worth noting that all training-data examples for Prisma will be wrong on this point — assume Prisma 7 docs are authoritative going forward. ### 3. Prisma 7 error shape doesn't surface SQLSTATE at top level `PrismaClientKnownRequestError` for an exclusion violation didn't expose `error.code === '23P01'` in any obvious place. The constraint name *is* present in `error.meta.constraint`. **Fix:** `findPgCode` in `test/booking-exclusion.test.ts:47` walks the error and falls back to constraint-name presence. Loose — a foreign-key or unique violation would also be classified as 23P01 by this fallback. Acceptable for the spike; tighten before this helper is used outside tests. ### 4. Vitest 4 renamed `fileParallel` → `fileParallelism` Quiet runtime-OK but caught by `tsc --noEmit`. **Fix:** updated `vitest.config.ts`. ### 5. Docker Compose plugin not installed The `docker compose` (space) form failed; `docker-compose` (hyphenated, v5.1.2 from Homebrew) works. All scripts and docs use the hyphenated form. ## Open questions for the user These are non-blocking but matter before the public booking page lands: 1. **Practice timezone.** `APP_TZ` is a placeholder. The system clock observed is EDT during this session — but that may just be the developer location. What's the practice's local timezone? 2. **Public-facing name.** Is "TouchBase" the brand customers will see, or just the codebase name? Affects email templates, page titles, etc. 3. **Currency.** USD assumed. Confirm. 4. **Stripe account ownership.** Personal vs. LLC vs. existing entity — flagged for when billing wiring lands (~week 4–5). ## Next step (recommended) Build out the **full schema** per `Initial.md` §4 in one migration: User + Customer + Therapist + Room + Service + WorkingHours + AvailabilityOverride + tagging tables + RoomBlock + Notification + AuditLog + the FK retrofit for Booking. Then a seed script with 10 therapists, 10 rooms, ~5 services, ~10 tags. Then start on the **availability algorithm** as a pure-function module with a fixture-driven test suite — the load-bearing piece per `Initial.md` §5. Auth.js and the customer-facing UI come after the algorithm is provably correct against fixtures. ## How to resume in a new session ```bash cd /Users/noise/Documents/code/touchbase docker-compose up -d postgres mailpit # If touchbase_test is missing or extensions not installed: pnpm db:bootstrap pnpm install pnpm test # should be 7/7 green pnpm dev # starts Next.js on http://localhost:3000 (still the create-next-app starter) ``` Mailpit web UI: http://localhost:8025 ## Files of interest for cross-reference - Planning: `/Users/noise/Documents/obsidian/Massage/Initial.md` - Schema: `prisma/schema.prisma` - Constraint SQL: `prisma/migrations/20260430170142_booking_spike/migration.sql` - Tests: `test/booking-exclusion.test.ts` - DB singleton: `src/lib/db.ts` - Compose: `compose.yaml` - Bootstrap script: `scripts/db-bootstrap.sh`