Files
Bookra/project.md
T
Tomas DvorakandDevin 6c3b2bf6bf feat(calendar): read-only ICS feeds for tenant and staff
GET /v1/public/calendar/{token} serves an RFC 5545 calendar under a
token-as-secret URL (private Google/Outlook link model). Staff feeds
contain only that member's bookings; the tenant feed tags each event
with the staff member. Window is -30d to +180d; cancelled bookings map
to STATUS:CANCELLED, waitlisted/pending to TENTATIVE + TRANSPARENT.
Summaries resolve service or class-session titles, descriptions carry
localized contact labels, locations resolve to names.

Feed tokens live on tenants/staff (migration 00015) and are managed via
GET /v1/calendar/feed plus POST /v1/calendar/feed/rotate — rotating a
scope invalidates the old URL. ICS rendering is hand-rolled: TEXT
escaping, CRLF, 75-octet folding on rune boundaries.

Settings gains a "Kalendář (ICS)" card with copyable URLs and two-step
rotation per scope; demo mode points at the real seeded tokens.

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

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

32 KiB
Raw Blame History

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
    • waitlist position display landed in Milestone 16; manual promotion controls landed in Milestone 20
    • remaining: live staging validation
  • [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
  • [done/partial] Milestone 14: Metrics completion + reminder dispatch scheduling
    • DashboardMetrics gained NewClientsCount — distinct customer emails whose earliest-ever booking (starts_at) lands in the window; a fifth KPI new_clients completes the set the demo data promised
    • KPI grid rebalanced for five cards (lg:grid-cols-3 xl:grid-cols-5); new_clients maps to the existing dashboard.kpi.newClients i18n key
    • reminder dispatch now has a real trigger: .github/workflows/reminder-dispatch.yml calls POST /v1/internal/jobs/reminders/dispatch every 15 minutes with X-Bookra-Job-Key from secrets (BOOKRA_API_URL, BOOKRA_JOB_RUNNER_KEY); jobs are timestamp-based so scheduler lateness delays but never skips
    • remaining: live staging validation; the workflow no-ops gracefully until the two secrets are configured
  • [done/partial] Milestone 15: SMS reminder channel
    • booking creation arms a second reminder job on channel: sms when the tenant has SMS enabled (tenant_sms_settings.enabled) and the customer left a phone number; email jobs are unchanged
    • notifications.Service gained an injected smsSender function adapter (wired to sms.Service.SendMessage in the API server, keeping the package dependency-free); dispatch routes channel: sms through it with localized body text and the customer phone as recipient
    • sms.Service.SendMessage still enforces enabled/plan/monthly-limit and logs usage, so SMS reminders inherit metering and billing automatically; missing provider config or phone marks the job failed with a delivery-log reason
    • reminder lifecycle work from milestone 13 applies per-booking, so cancel/reschedule clears and re-arms both channels
    • remaining: live staging validation against a real SMS Manager key
  • [done/partial] Milestone 16: Waitlist position display
    • GetWaitlistPosition repository method (pgx + memory) returns the 1-based queue spot: count of earlier-created waitlisted bookings on the same session + 1
    • CustomerBookingView carries waitlistPosition for waitlisted class bookings; the manage page now shows a proper status badge for waitlisted/cancelled (previously both rendered as "Pending") plus a notice with the queue position and the auto-promotion explanation, in CS and EN
    • fixed invalid token names along the way (--danger-soft/--warning-soft do not exist; --error-subtle/--warning-subtle do)
    • manual promotion controls landed in Milestone 20
    • remaining: live staging validation
  • [done/partial] Milestone 17: Class template editing
    • PUT /v1/catalog/class-templates/{id} end-to-end: UpdateClassTemplate in both repositories, catalog service method sharing validateClassTemplateInput with create, route, OpenAPI path, regenerated client
    • classes tab template rows gained a pencil edit action that loads the template into the form; the submit button switches to save-changes with a cancel affordance
    • remaining: live staging validation
  • [done/partial] Milestone 18: Public class list polish
    • class sessions on the public booking page are now grouped by day (localized weekday + date headers) with compact timetable-style rows instead of one tall card per session — consistent with the stepped appointment picker above
    • capacity states read honestly: spots-left in success/warning tones, Obsazeno/Full when empty, and the action switches to Na čekací listinu/Join waitlist (previously a full session still said "Book now" while silently producing a waitlisted booking)
    • reminder arming extracted into a shared armReminderJobs helper (email + opt-in SMS) used by booking creation and waitlist promotion — a booking promoted after its reminder already dispatched-as-skipped now gets re-armed instead of going unreminded
    • remaining: live staging validation
  • [done] Milestone 19: Double-booking race hardening (validated on local Postgres)
    • migration 00014_booking_no_overlap.sql: btree_gist extension plus three partial exclusion constraints on bookings mirroring resourcesCollide semantics — same staff, same location, or both-unassigned appointments may not overlap (tstzrange, [) bounds so abutting slots are legal); cancelled rows and booking_mode='class' are excluded
    • db.IsExclusionViolation maps SQLSTATE 23P01 to ErrBookingConflict at all three write paths (public create, customer reschedule, dashboard manual create) — a lost race now surfaces as a proper 409 instead of a silent double-booking
    • validated end-to-end against a local postgres:17 container: SQL-level matrix (staff/location/unassigned overlap blocked, abutting allowed, cancel frees the slot), repo-level 23P01 detection, and a 10-request concurrent burst on one slot producing exactly one 201 + 409s for the rest; down/up migration roundtrip clean
    • local test container: docker run -d --name bookra-pg-local -p 5439:5432 -e POSTGRES_PASSWORD=bookra-local postgres:17-alpine, migrations via npm run db:migrate:up with BOOKRA_DATABASE_DIRECT_URL pointed at it
    • remaining: live staging validation on Neon
  • [done/partial] Milestone 20: Class roster + manual waitlist promotion
    • GET /v1/catalog/class-sessions/{id}/roster returns session metadata plus confirmed and waitlist entry lists (booking id, reference, customer name/email, status, waitlistPosition, createdAt); ClassSession now carries waitlistCount so session rows can badge queue length without an extra request
    • POST /v1/catalog/bookings/{id}/promote manually confirms a waitlisted booking: 409 when the session is full unless the caller passes allowOverCapacity, 400 for non-waitlisted bookings, 404 for missing or foreign bookings; promotion reuses the shared confirm path so waitlist ledger rows resolve and reminders re-arm automatically
    • ListBookingsBySession repository method (pgx + memory) backs the roster and simplifies PromoteWaitlist; UpcomingBooking carries classSessionId + waitlistPosition, computed per session in the dashboard summary
    • classes tab session rows expand into a two-column roster panel (attendees vs waitlist with #N position badges) with per-entry promote buttons; a full session arms an explicit "confirm anyway" over-capacity step instead of silently overriding
    • the booking detail modal shows the waitlist position and offers the same promote action for waitlisted bookings; the bookings list badges waitlisted rows with #N
    • demo mode ships a seeded session with a populated roster; mutation attempts surface the demo notice instead of failing
    • verified live in demo mode: seeded a full session via the public booking API, roster listed 4 confirmed + 1 waitlisted, promote rejected with 409, allowOverCapacity promoted and drained the queue; browser-checked CS/EN rendering with a clean console
    • remaining: live staging validation on Neon + pgx repository path
  • [done/partial] Milestone 21: Customer self-service rebooking
    • CustomerBookingView now carries tenantSlug, serviceId, staffId, classSessionId — everything the manage page needs to query real availability; class bookings resolve the session title as their service label instead of rendering blank
    • RescheduleBooking hardened: rejects waitlisted/completed bookings and any class booking (session-bound times cannot move to arbitrary windows; switching sessions is cancel-and-rebook); the previously dead ClassSessionID == nil branch collapsed into a straight appointment validation
    • manage page reschedule dialog replaced free-form date+time inputs with a real slot picker: pick a date → GET /v1/public/tenants/{slug}/availability (filtered by the booking's service/staff) → time buttons; the slot carries its own duration so the hardcoded 60-minute assumption is gone; 409 surfaces "slot just taken" instead of a generic failure
    • action buttons are now status-gated: reschedule shows only for confirmed/pending appointments, cancel for any live booking, and class bookings get an explanatory note instead of a broken button
    • CustomerBookingView schema added to OpenAPI and referenced from GET /v1/public/bookings/{reference}; the reschedule contract documents the class-booking 400 and slot-conflict 409
    • verified live: booked an appointment via public API, rescheduled through the real manage page in a browser (slot grid → 200 → card shows new time); class booking reschedule returns 400
    • remaining: live staging validation on Neon + pgx repository path
  • [done/partial] Milestone 22: ICS calendar feeds
    • GET /v1/public/calendar/{token} serves an RFC 5545 feed — token-as-secret (Google/Outlook private-link model), .ics suffix tolerated, bookings from −30d to +180d; cancelled → STATUS:CANCELLED, waitlisted/pending → TENTATIVE + TRANSP:TRANSPARENT, waitlist entries marked in the summary
    • per-staff tokens (staff.feed_token) and one tenant-wide token (tenants.feed_token, migration 00015); staff feeds contain only their bookings, tenant feeds tag each event with the staff member; summaries resolve service or class-session titles, descriptions carry localized contact labels (cs/en by tenant locale), events get location names
    • custom ICS renderer — no dependency: TEXT escaping, CRLF, 75-octet folding on rune boundaries
    • GET /v1/calendar/feed lists tenant + staff tokens behind auth; POST /v1/calendar/feed/rotate invalidates a leaked URL (tenant or staff scope, staff verified against the tenant)
    • settings page "Kalendář (ICS)" card: copyable feed URLs per business/staff member, two-step rotate with confirmation, demo-mode placeholders pointing at real seeded tokens
    • verified live: feed info returns tenant+staff tokens, tenant feed renders the seeded booking with staff name in the summary, staff feed omits it, bogus token 404s, rotation kills the old URL and the new one serves the same feed
    • remaining: live staging validation on Neon + pgx repository path; Google Calendar two-way sync stays deferred (the ICS read direction covers the common ask)

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, customers, and class templates/sessions with rosters; 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 per-staff availability rules so staff schedules are editable after onboarding

Future Phases

Shortlist of worthwhile directions once v1 stabilizes — kept deliberately small, in line with the "no feature sprawl" principle:

  • Activity/audit log: a per-tenant feed of who changed what (booking created/cancelled/promoted, settings edits). Natural fit for the notification dropdown and useful for multi-staff accountability.
  • Session-to-session move for classes: M21 covered appointment rebooking; class customers currently must cancel and book another session. A "move to another session of the same template" flow (target session picker + capacity/waitlist handling) would close the gap.
  • Google Calendar sync: M22 shipped read-only ICS feeds; two-way sync (push bookings into Google, pull busy blocks back as availability exceptions) needs OAuth and conflict rules — larger commitment, defer until a concrete request.
  • Recurring class schedules: templates currently schedule sessions one at a time; a weekly recurrence rule with per-session overrides would cut setup time for studios.
  • Booking notes intake: structured intake fields per service (e.g. "first visit?", "injuries?") instead of the single free-text notes field — still schema-light.
  • Persistent notification center: the dropdown currently derives notifications from summary data; a notifications table would enable read/unread state and delivery history in-app.
  • Staff performance view: bookings/completion/utilization per staff member — the metrics primitives already exist; this is a filtered presentation layer.
  • CSV export: bookings and customers export for accounting/backup; cheap to build, frequently requested by small businesses.

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