Work in progress. Internal tool for short-term rental ops: sync bookings from a channel manager, assign each stay to a physical unit, avoid double-booking, and coordinate cleaning and maintenance. Not for guests.
Stack direction: Next.js, PostgreSQL / Prisma, Redis for jobs — see docs/architecture/CONVENTIONS.md.
Code review: use docs/architecture/CONVENTIONS.md as the checklist for module boundaries, API errors, and data access.
- Node 20+
- pnpm (version pinned in root
package.jsonaspackageManager)
| Script | Description |
|---|---|
pnpm dev |
Builds workspace packages, then starts the Next.js app (apps/web) |
pnpm build |
Builds all workspace packages (pnpm -r run build) |
pnpm lint |
Runs ESLint in all packages (pnpm -r run lint) |
pnpm test |
Runs tests in all packages (pnpm -r run test) |
apps/web— Next.js (App Router) UI and route handlerspackages/db— Prisma schema and data accesspackages/shared— Shared types, env validation (Zod), and helperspackages/worker— Background jobs (BullMQ) entrypointpackages/sync— Hosthub client, webhooks queue, booking ingest (Hosthub API docs, docs/vendor/hosthub-api.md)
- Next.js App Router app (
apps/web) - TypeScript path alias in the web app:
@/*→apps/web/src/*(seeapps/web/tsconfig.json) - Environment validation in
packages/shared/src/env.ts - Prisma schema/migrations + seed harness in
packages/db/prisma/* - Local data stack via
docker-compose.yml(Postgres + Redis)
- Auth endpoints
POST /api/auth/loginPOST /api/auth/logoutGET /api/auth/me
- Session/cookie behavior
- Cookie name:
stay_ops_session - TTL: 24 hours
- Signed/verified with
SESSION_SECRET - Cookie attributes:
httpOnly,SameSite=lax,securein production
- Cookie name:
- Route protection
apps/web/src/middleware.tsdenies by default and allows an explicit public allowlist- API unauthorized responses return JSON with
error.code = "UNAUTHORIZED" - App routes redirect unauthenticated users to
/login?next=... - Corrupt/expired cookies are cleared
- Persistence/bootstrap
packages/db/prisma/schema.prismaincludes aUsermodel mapped to theuserstablepackages/db/prisma/seed.tssupports idempotent admin upsert viaBOOTSTRAP_ADMIN_EMAILandBOOTSTRAP_ADMIN_PASSWORD
- Recovery documentation:
docs/runbooks/auth-recovery.md
- API (admin session required)
POST /api/assignments— assign an unassigned booking to a roomPATCH /api/assignments/[id]/reassign— move an assignment to another roomPOST /api/assignments/[id]/unassign— return a booking to the unassigned queueGET /api/bookings/unassigned— list bookings not yet assigned to a room (optionalmeta.total)
- Domain logic lives under
apps/web/src/modules/allocation/(service + error types). Database overlap and uniqueness are enforced in Postgres; concurrent writes map to stable API errors such asCONFLICT_ASSIGNMENTandBOOKING_ALREADY_ASSIGNED. - Inactive rooms:
Room.isActiveis honored; assigning or reassigning to an inactive room returnsROOM_INACTIVE(see docs/phases/phase-04-allocation.md).
- API (admin session required)
POST /api/blocks— create a manual block on a room for a date rangePATCH /api/blocks/[id]/DELETE /api/blocks/[id]— update or remove a block
- Service:
apps/web/src/modules/blocks/service.ts(facade over shared calendar rules). Overlaps with assignments or other blocks surface asCONFLICT_ASSIGNMENT/CONFLICT_BLOCKas appropriate.
- API (admin session required)
GET /api/cleaning/tasks/POST /api/cleaning/tasks— list and create service cleaning tasksPATCH /api/cleaning/tasks/[id]/schedule— adjust planned window (validates against booking turnover rules)PATCH /api/cleaning/tasks/[id]/status— status transitions (in_progress,done)
- Engine modules under
apps/web/src/modules/cleaning/(scheduling, state machine, turnover generation). Spec and invariants: docs/phases/phase-05-cleaning-engine.md.
POST /api/sync/hosthub/webhook— ingest webhook events (signature/HMAC when configured)GET /api/sync/runs— list recent sync run records (authenticated)GET /api/health— deployment health probe (DB connectivity + process uptime)- Client and pipeline:
packages/sync; vendor notes: docs/vendor/hosthub-api.md, phase outline docs/phases/phase-03-hosthub-sync.md.
- JSON error shape:
{ error: { code, message, details? } }(see docs/architecture/CONVENTIONS.md). - Route handlers compose
jsonError/ module-specific envelopes so auth, validation, and domain codes stay consistent.
- Unit / package tests:
packages/shared,packages/sync(Vitest). - Web integration tests:
apps/web/tests/integration/(Vitest;apps/web/vitest.config.tsuses projects for integration + jsdom unit tests). They hit real Postgres and Redis — startdocker composebeforepnpm --filter @stay-ops/web test. - Web component tests (Phase 6 UI):
apps/web/tests/unit/(Vitest + Testing Library + jsdom) — calendar cards/lanes/grid, block modal, unassigned drawer, cleaning board. Run withpnpm --filter @stay-ops/web test(same command as integration). - Browser E2E (Playwright):
apps/web/tests/e2e/— desktop Chromium and mobile viewport (390×844). Easiest local run:pnpm e2e:local(Docker Postgres/Redis → migrate → seed →seed:e2e→ Playwright on port 3005 so it does not clash withpnpm devon 3000; same disposable test admin as CI). Otherwise install browsers once (pnpm --filter @stay-ops/web test:e2e:install), seed the DB, setE2E_ADMIN_*to matchBOOTSTRAP_ADMIN_*, thenpnpm --filter @stay-ops/web test:e2e. Skipping seed or env vars causes login 401. CI:.github/workflows/e2e.yml. See docs/runbooks/local-dev.md. - Coverage includes auth, allocation (including races and inactive rooms), blocks, cleaning flows, DB constraints, and sync webhook behavior.
- Target stack: Vercel + Neon + Upstash.
- Deployment runbook: docs/runbooks/production-deploy.md.
- Incident runbooks:
- Start local services:
docker compose up -d(ordocker compose up --buildthe first time) - Copy env:
.env.example→.env/apps/web/.env.localas needed (never commit secrets) - Apply DB + bootstrap admin:
pnpm --filter @stay-ops/db migrate:deploythenpnpm --filter @stay-ops/db seed(withBOOTSTRAP_ADMIN_EMAIL/BOOTSTRAP_ADMIN_PASSWORDset). For Playwright, also runpnpm --filter @stay-ops/db seed:e2eand alignE2E_ADMIN_*with the bootstrap user — or runpnpm e2e:localonce Chromium is installed to do Docker + migrate + both seeds + E2E with CI-aligned defaults. - Run the app:
pnpm --filter @stay-ops/web dev - Run the full check from repo root:
pnpm lint,pnpm build,pnpm test
- Phased execution and acceptance criteria: docs/phases/README.md and individual phase files (calendar UX, suggestions, production readiness, etc. are specified there even when not yet built in code).
- Do not commit operational secrets (for example
.env,SESSION_SECRET, provider tokens, or database URLs with credentials). Use.env.exampleand local environment injection instead.