Files
touchbase/docs/progress/2026-04-30-bootstrap.md
2026-05-01 18:24:09 -04:00

9.1 KiB
Raw Blame History

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 test7/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_overlapEXCLUDE 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:

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 fileParallelfileParallelism

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 45).

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

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