169 lines
9.1 KiB
Markdown
169 lines
9.1 KiB
Markdown
# 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`
|