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`
|