Files
touchbase/docs/progress/2026-04-30-bootstrap.md

169 lines
9.1 KiB
Markdown
Raw Normal View History

2026-05-01 18:24:09 -04:00
# 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 45).
## 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`