Files
Bookra/project.md
T
Tomas DvorakandDevin cdad7875e1 fix(reminders): lifecycle-aware jobs and dispatch skip guard
The reminder pipeline fired regardless of what happened to the booking
after creation: cancelled customers got reminded about bookings that
weren't happening, reschedules kept the old reminder time, and
waitlisted bookings armed jobs too. Pending jobs are now cancelled on
both cancel paths, deleted and re-armed on reschedule, and dispatch
joins bookings.status to skip anything not confirmed. Reschedule also
gained the same availability-window and blocked-day validation as
booking creation, closing a 3am-reschedule hole via the manage URL.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-18 12:47:07 +02:00

373 lines
21 KiB
Markdown

# Bookra
## Delivery Tracker
- `[done]` Milestone 1: Remote-first monorepo scaffold completed
- SolidJS frontend shell
- Go + Gin backend shell
- OpenAPI-driven TS client generation
- Neon Auth adapter boundary
- remote-first env layout for Vercel, Railway, and Neon
- `[done]` Milestone 2: Repository-backed booking baseline completed
- DB/in-memory repository boundary added
- public availability now generated through backend service logic
- public booking creation now goes through real service validation
- appointment conflict detection implemented
- class capacity and waitlist fallback implemented
- backend tests added for booking and slot behavior
- frontend public booking page can submit a demo booking
- `[done/partial]` Milestone 3: Tenant bootstrap, dashboard hydration, and Neon-authenticated API flows
- tenant bootstrap endpoint now resolves through repository-backed membership lookup
- dashboard summary endpoint now resolves through repository-backed metrics lookup
- frontend dashboard now hydrates from API when a Neon Auth JWT is available
- frontend dashboard degrades to contract-shaped preview mode when auth is unavailable
- Neon Auth token acquisition remains adapter-isolated and still needs production validation against live Neon Auth
- `[done/partial]` Milestone 4: Stripe subscription sync and entitlement baseline completed
- normalized billing snapshot model added to backend and OpenAPI contract
- backend-owned subscription state now syncs through one overwrite path
- Stripe checkout launch endpoint added
- Stripe webhook endpoint added with signature verification and event filtering
- billing snapshot refresh endpoint added
- customer portal endpoint added
- plan entitlements now derive from normalized plan state
- dashboard now surfaces billing status, entitlements, and checkout actions
- mock/no-key fallback still works for local development
- `[done/partial]` Milestone 5: Reminder jobs, notification delivery abstractions, and remote job-runner baseline completed
- booking creation now schedules reminder jobs for qualifying upcoming bookings
- notification service now dispatches due jobs through replaceable provider interfaces
- localized reminder copy now renders with tenant locale/timezone context
- notification delivery log persistence added
- internal reminder dispatch endpoint added for Railway-style scheduled execution
- shared job-runner key added for remote job execution
- current providers are noop-safe abstractions; real email/SMS vendors still need production integration
- `[done/partial]` Milestone 6: Production auth completion and tenant identity sync baseline completed
- verified Neon JWTs now sync app-side users automatically on protected requests
- app-side membership resolution now maps by `users.neon_subject` instead of assuming JWT subject equals DB primary key
- frontend auth provider now reads JWT from the Neon session contract instead of a fallback token hook
- bootstrap payload now returns user identity fields needed for authenticated shells
- Neon subject schema migration added for production databases
- live Neon Auth / staging validation still needs to be completed against real infrastructure
- `[done/partial]` Milestone 7: First-tenant onboarding flow added to the dashboard
- authenticated users without membership now see a real workspace creation flow in `/dashboard`
- protected tenant onboarding endpoint added with backend validation for slug, preset, locale, and timezone
- onboarding now creates the tenant, owner membership, and first default location
- dashboard can transition from empty bootstrap shell to a provisioned tenant state
- post-onboarding entity screens for locations, services, staff, blocked days, working hours, and class templates/sessions now exist (Milestones 8 and 10)
- `[done/partial]` Milestone 8: Catalog management shipped; live staging validation and operational hardening remain
- service CRUD end-to-end: repository (pgx + in-memory), catalog service with validation and tenant isolation, protected `/v1/catalog/services` routes, OpenAPI schemas, dashboard services page
- staff CRUD end-to-end with active toggle, optional location assignment, and `MaxStaff` entitlement enforcement; dashboard staff page with upgrade prompt
- dashboard summary now resolves `staffName` on bookings and computes `setupCompletion` from real state (brand, location, service, staff, schedule)
- booking modal service field is a select bound to the tenant's real services; blocked-day creation supports optional per-staff scoping
- deleting a service or staff member preserves bookings (FK `SET NULL`) and removes the member's availability rules (`CASCADE`), mirrored in the in-memory repository
- live staging validation, real Neon Auth/Stripe/SMS provider checks, and operational hardening are still open
- `[done/partial]` Milestone 9: Public booking flow with real service/staff selection
- public booking page is now a stepped flow: pick service → optional staff preference → pick day → pick time; class sessions still listed below
- `GET /v1/public/tenants/{slug}/catalog` exposes bookable services and active staff for the booking page
- availability endpoint accepts `serviceId`, `staffId`, and `date` filters; default response is unchanged for the embeddable widget
- slots now carry `staffName`, respect tenant-wide and per-staff blocked days, and are generated per-day without the preview cap when `date` is given
- booking creation hardened: appointment requests must fit inside a matching availability rule, not overlap a blocked day, and staff/location IDs must belong to the tenant with staff active
- remaining: live staging validation
- `[done/partial]` Milestone 10: Per-staff availability and class/session management
- working-hours repository now accepts a nullable staff scope on list/update (pgx + in-memory); staff-scoped rules live in `availability_rules` and directly drive public slot generation
- protected routes added: `GET/PUT /v1/catalog/staff/{id}/working-hours[/{day}]` with tenant-ownership checks
- class template + session CRUD end-to-end: repository (`classes.go`), catalog service with validation, protected `/v1/catalog/class-templates` and `/v1/catalog/class-sessions` routes, OpenAPI + regenerated client
- sessions derive `endsAt` from template duration, default capacity from the template, validate future start, tenant-owned template and optional location; list resolves template title, location name, and live booked count
- deletion semantics mirror the schema: deleting a template cascades its sessions; deleting a session preserves bookings via `class_session_id SET NULL` (mirrored in the in-memory repo)
- dashboard zones page gained a staff-scope selector on the working-hours tab (business-wide vs individual schedule) and a Classes tab with template create/delete and session scheduling
- normalized `availability_rules` time columns (`HH:MM:SS`) down to the `HH:MM` API contract in the service layer
- remaining: live staging validation
- `[done/partial]` Milestone 11: Booking lifecycle — dashboard cancellation and waitlist promotion
- businesses can cancel bookings from the dashboard (`POST /v1/catalog/bookings/{id}/cancel`), tenant-checked, with a two-step confirm in the booking detail modal
- cancelling a class booking (dashboard or customer self-service) now promotes the earliest waitlisted bookings while free capacity remains, ordered by booking `created_at`; promoted customers receive a confirmation email, cancelled customers a cancellation email
- shared `bookings.PromoteWaitlist` helper drives both cancel paths; waitlist ledger entries are marked resolved on promotion
- bookings surface the `waitlisted` status: stats card, filter option, warning-tone badge, demo seed entry; `dashboard.{status}` interpolation resolves the new i18n key automatically
- `BookingRecord` now carries `created_at` (needed for fair waitlist ordering); `GetBookingByID` added to the repository surface
- remaining: live staging validation; waitlist position display and manual promotion controls are not yet exposed
- `[done/partial]` Milestone 12: Real dashboard business metrics
- `DashboardMetrics` gained `CompletedCount` (confirmed bookings already ended within the window) in both pgx and in-memory repositories
- `DashboardSummary` now queries three trailing windows (next 7d, last 7d, 7-14d ago) and emits four KPIs with real trend deltas: `bookings_this_week`, `completed`, `cancellations`, `utilization` (delta in percentage points)
- `DashboardKPI` carries optional `change`/`trend` (`up|down|neutral`); the KPI card already rendered these, so real data now drives the trend arrows instead of the previous `neutral`/`—` fallback
- KPI labels are localized in the frontend via a `code` → i18n-key map (`dashboard.kpi.bookingsThisWeek`, `cancellations`, `utilization`; `completed` reuses the existing key); backend labels stay English as a fallback
- the booking-trend chart was already bucketing real `allBookings` into the current week — confirmed, no change needed
- demo mode intentionally keeps curated static KPI values; real tenants get the computed set
- remaining: live staging validation; "new clients" KPI still has no backend metric (customer entity lacks first-booking tracking)
- `[done/partial]` Milestone 13: Reminder pipeline reliability
- reminder jobs are lifecycle-aware: customer and dashboard cancellation mark pending jobs `cancelled`; reschedule deletes pending jobs and re-arms one via `reminderSchedule` for the new time (no job when moved inside the no-reminder horizon)
- dispatch now joins `bookings.status` and skips non-confirmed bookings (`skipped` job status, `skippedCount` in the response contract) — cancelled/waitlisted customers no longer receive reminders for bookings that are not happening
- reschedule hardened with the same validation as booking creation: appointment reschedules must fit an availability rule and avoid blocked days (`validateAppointmentTarget` is now a shared package-level helper); class bookings keep session-defined times
- new repository methods `CancelPendingReminderJobsForBooking` / `DeletePendingReminderJobsForBooking` in pgx and in-memory implementations
- remaining: live staging validation; SMS reminder channel exists in schema (`channel` column) but only `email` is dispatched — SMS reminders are a separate opt-in feature
## Overview
Bookra is a Czech-first booking SaaS for small local service businesses. It targets salons, massage therapists, small clinics, repair shops, and small studios/clubs that need a simple public booking flow, internal schedule management, reminders, and basic business reporting without becoming a full ERP.
The product is multi-tenant, calm in scope, and optimized for production quality rather than a fast prototype.
## Product Direction
Bookra uses one unified booking core with tenant presets.
This means:
- one platform for appointments and simple classes
- different business presets for salon, clinic, repair, massage, sports/studio
- shared scheduling, reminders, dashboard, and billing logic
- no industry-specific forked products in v1
## Launch Scope
### Market
- Czech Republic first
- English included at launch
- EU-friendly hosting and data handling defaults
### Supported booking models
- Appointments
- Capacity-based classes
### Explicitly out of scope for v1
- Customer payments for bookings
- Deposits or prepaid services
- Memberships, passes, or package accounting
- Regulated healthcare workflows
- Medical records or sensitive health data
- Marketplace/discovery layer
## Customer Experience
- Public booking page per business
- Guest booking supported by default
- Optional customer accounts for history and faster rebooking
- Confirmation, cancellation, and reschedule flow
- Czech and English UI from day one
- Timezone-aware booking and reminders
## Business Experience
Each business can manage:
- multiple locations
- multiple staff members
- services
- class sessions
- business hours and exceptions
- buffer times
- blackout periods
- reminder settings
- branding basics
- dashboard metrics
Owner dashboard should show:
- bookings this week
- cancellations
- utilization
- simple revenue forecast based on scheduled services
## Core Scheduling Rules
The booking engine must support:
- multi-tenant isolation
- staff and location assignment
- conflict detection
- buffer times
- timezone-safe date handling
- waitlists
- class capacity limits
- exception-based availability rules
Appointments and classes should share the same scheduling foundation where possible.
## Notifications
v1 reminder strategy:
- email is first-class
- SMS is optional and treated as a paid add-on
- SMS must be abstracted behind a provider interface
- Czech-first provider support is preferred, with room for generic fallback providers later
## Billing
Bookra uses Stripe for B2B SaaS subscription billing. (Migrated from Paddle; the Paddle integration was removed.)
Stripe scope in v1:
- tenant subscription plans
- plan upgrades/downgrades
- add-ons such as SMS or expanded limits
- webhook-based subscription state sync
- customer self-serve billing portal
Stripe is not used for end-customer booking checkout in v1.
## Remote-First Architecture
Bookra follows the `tdvorak-fullstack` profile with remote deployment overrides.
### Stack
- Frontend: SolidJS + Vite + TypeScript + Tailwind
- Backend: Go + Gin
- Database: Neon Postgres + goose (handwritten pgx repositories; sqlc removed)
- Auth: Neon Auth
- API contract: OpenAPI as source of truth
- Frontend deployment: Vercel
- Backend deployment: Railway
- Local development: direct app processes against remote services
### Structure
- modular monolith first
- monorepo layout
- generated TypeScript API client from OpenAPI
- strong tenant boundaries
- background jobs for reminders and waitlist handling
- no Docker Compose and no self-hosted infra path in v1
Suggested layout:
- `/apps/frontend`
- `/apps/backend`
- `/packages/api-client`
- `/packages/shared-types`
## Auth And Identity
- Neon Auth handles sign-up, sign-in, sessions, and browser auth state
- frontend owns browser auth flows
- Go backend verifies Neon Auth JWTs through JWKS
- backend treats Neon `sub` as the authenticated principal
- backend now syncs `neon_subject` into app-side `users` records on authenticated requests
- JWT usage is isolated to frontend-to-backend API calls across domains
- Neon-specific auth code must stay behind replaceable adapters because Neon Auth is beta
## Database And Environments
- Neon is the only database platform
- pooled connections for application traffic
- direct connections for migrations and admin tasks
- staging and production only in v1
- no per-PR backend preview environments in v1
## Main Domain Model
Key entities:
- tenants
- tenant users
- customer accounts
- locations
- staff
- services
- class templates
- class sessions
- availability rules
- availability exceptions
- bookings
- waitlist entries
- reminder jobs
- subscription accounts
- subscription events
## v1 Delivery Phases
### Phase 1
- monorepo setup
- Neon Auth integration shell
- tenant foundation
- locations, staff, services
- i18n foundation
- OpenAPI baseline
- status: completed
### Phase 2
- appointment booking flow
- availability rules
- conflict detection
- cancellation and rescheduling
- timezone-safe scheduling
- status: in progress
- current implementation:
- public availability endpoint returns generated appointment and class slots
- public booking creation validates tenant existence, timestamps, conflicts, and class capacity
- frontend public route can trigger booking creation from live slots
- repository pattern supports Neon-backed persistence with memory fallback
### Phase 3
- class sessions
- capacity limits
- waitlists
- email reminders
- dashboard metrics
- status: materially implemented
- current implementation:
- class sessions appear in public availability
- class capacity and waitlist status are enforced at booking time
- dashboard metrics endpoint is repository-backed but still minimal in scope
- booking creation now schedules reminder jobs
- due reminder jobs can now be dispatched through the notifications service
- delivery logging now exists for reminder execution
### Phase 4
- Stripe subscriptions
- SMS add-on support
- plan enforcement
- hardening, logging, and operational tooling
- status: in progress
- current implementation:
- billing snapshot persistence added
- Stripe checkout route added (Checkout Sessions)
- Stripe webhook route added with signature verification
- Stripe portal route added (customer portal sessions)
- subscription refresh route added
- entitlement mapping is now available to the frontend
- API edge now includes security headers and public route rate limiting
- remote job-runner endpoint added for reminder execution
- goose commands are wired into repeatable npm scripts
## Current Implementation Notes
- Backend is no longer static-demo-only for booking flows.
- The repository layer supports:
- Neon-backed pgx access when `BOOKRA_DATABASE_URL` is configured
- deterministic in-memory fallback when Neon is not configured
- OpenAPI remains the frontend/backend contract source.
- Frontend public booking flow is now interactive, not just presentational.
- Route structure is now explicit:
- `/` is the primary marketing landing page
- `/dashboard` is the main application entry
- `/book/:tenantSlug` stays public and customer-facing
- Neon Auth remains intentionally isolated behind adapters because the managed product is still beta.
- Billing now follows a backend-owned snapshot model rather than trusting incremental webhook payloads.
- `apps/auth-service` is now retired from active runtime and docs.
- Reminder delivery now has a working runtime path instead of just a queued schema.
- goose migration execution is runnable through workspace scripts.
- Protected backend requests now perform automatic app-side identity sync before tenant membership resolution.
- `/dashboard` now includes first-run onboarding instead of only a passive empty state for unassigned authenticated users.
- Catalog management now covers services and staff end-to-end; staff creation enforces `MaxStaff` entitlements and staff location assignment validates tenant ownership.
- Dashboard summary computes `setupCompletion` from real configured state (brand, location, service, staff, availability rules) rather than a static value.
- Public booking flow offers real service and staff selection via a stepped picker backed by `GET /v1/public/tenants/{slug}/catalog` and filtered availability (`serviceId`, `staffId`, `date`).
- Appointment creation validates that requested times fit inside availability rules, skip blocked days, and reference tenant-owned staff and locations.
## Current Gaps
- Neon Auth token extraction is wired structurally, but still needs live browser validation against a real Neon Auth environment.
- Tenant membership bootstrap now syncs identities structurally, but staging still needs seeded membership records and onboarding flows for first-time tenants.
- Post-onboarding CRUD now covers services, staff, locations, blocked days, working hours, and customers; class/session template management and per-staff availability rules still have no management UI.
- Stripe lifecycle sync is implemented; still needs live environment validation against real products, price IDs, and webhook signatures.
- Real email/SMS provider integrations are not implemented yet; current delivery providers are safe noops.
- The internal reminder dispatch route exists, but Railway scheduling and secret rotation still need production setup.
## Next Build Targets
- replace preview-mode dashboard hydration with fully validated Neon Auth JWT flow
- sync Neon Auth identities into tenant membership records and onboarding bootstrap
- validate Stripe checkout/webhooks/portal and reminder dispatch against live staging infrastructure
- replace noop notification providers with the first production email transport and a gated SMS transport
- add management UI for class/session templates and per-staff availability rules so service duration, capacity, and staff schedules are editable after onboarding
## Product Principles
- calm, premium, structured UX
- production-grade architecture
- no generic SaaS clutter
- deterministic scheduling behavior
- minimal operational complexity in v1
- build for maintainability, not feature sprawl
## Success Criteria For v1
Bookra is successful when a small business can:
- create its business profile
- configure staff, locations, and services
- publish a booking page
- accept bookings without double-booking
- send reminders reliably
- manage cancellations and waitlists
- view simple business health metrics
- subscribe to a paid plan through Stripe
## Final Positioning
Bookra is not an ERP, not a medical system, and not a marketplace.
It is a focused booking operating layer for small local service businesses that need:
- simple setup
- reliable scheduling
- clear reminders
- lightweight reporting
- clean SaaS billing