CustomerBookingView now carries tenantSlug/serviceId/staffId/ classSessionId so the manage page can query live availability; class bookings resolve their session title as the display label. Reschedule rejects waitlisted/completed bookings and session-bound class bookings (400), collapsing a dead branch in the appointment path. The manage page reschedule dialog replaces free-form date/time inputs with a date → availability-slot picker filtered to appointment mode, so customers can only move to a genuinely free slot with the correct duration; 409 surfaces "slot just taken". Actions are status-gated: reschedule for confirmed/pending appointments, cancel for live bookings, explanatory note for class bookings. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
31 KiB
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_subjectinstead 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)
- authenticated users without membership now see a real workspace creation flow in
[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/servicesroutes, OpenAPI schemas, dashboard services page - staff CRUD end-to-end with active toggle, optional location assignment, and
MaxStaffentitlement enforcement; dashboard staff page with upgrade prompt - dashboard summary now resolves
staffNameon bookings and computessetupCompletionfrom 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
- service CRUD end-to-end: repository (pgx + in-memory), catalog service with validation and tenant isolation, protected
[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}/catalogexposes bookable services and active staff for the booking page- availability endpoint accepts
serviceId,staffId, anddatefilters; 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 whendateis 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_rulesand 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-templatesand/v1/catalog/class-sessionsroutes, OpenAPI + regenerated client - sessions derive
endsAtfrom 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_rulestime columns (HH:MM:SS) down to theHH:MMAPI contract in the service layer - remaining: live staging validation
- working-hours repository now accepts a nullable staff scope on list/update (pgx + in-memory); staff-scoped rules live in
[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.PromoteWaitlisthelper drives both cancel paths; waitlist ledger entries are marked resolved on promotion - bookings surface the
waitlistedstatus: stats card, filter option, warning-tone badge, demo seed entry;dashboard.{status}interpolation resolves the new i18n key automatically BookingRecordnow carriescreated_at(needed for fair waitlist ordering);GetBookingByIDadded to the repository surface- waitlist position display landed in Milestone 16; manual promotion controls landed in Milestone 20
- remaining: live staging validation
- businesses can cancel bookings from the dashboard (
[done/partial]Milestone 12: Real dashboard business metricsDashboardMetricsgainedCompletedCount(confirmed bookings already ended within the window) in both pgx and in-memory repositoriesDashboardSummarynow 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)DashboardKPIcarries optionalchange/trend(up|down|neutral); the KPI card already rendered these, so real data now drives the trend arrows instead of the previousneutral/—fallback- KPI labels are localized in the frontend via a
code→ i18n-key map (dashboard.kpi.bookingsThisWeek,cancellations,utilization;completedreuses the existing key); backend labels stay English as a fallback - the booking-trend chart was already bucketing real
allBookingsinto 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 viareminderSchedulefor the new time (no job when moved inside the no-reminder horizon) - dispatch now joins
bookings.statusand skips non-confirmed bookings (skippedjob status,skippedCountin 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 (
validateAppointmentTargetis now a shared package-level helper); class bookings keep session-defined times - new repository methods
CancelPendingReminderJobsForBooking/DeletePendingReminderJobsForBookingin pgx and in-memory implementations - remaining: live staging validation; SMS reminder channel exists in schema (
channelcolumn) but onlyemailis dispatched — SMS reminders are a separate opt-in feature
- reminder jobs are lifecycle-aware: customer and dashboard cancellation mark pending jobs
[done/partial]Milestone 14: Metrics completion + reminder dispatch schedulingDashboardMetricsgainedNewClientsCount— distinct customer emails whose earliest-ever booking (starts_at) lands in the window; a fifth KPInew_clientscompletes the set the demo data promised- KPI grid rebalanced for five cards (
lg:grid-cols-3 xl:grid-cols-5);new_clientsmaps to the existingdashboard.kpi.newClientsi18n key - reminder dispatch now has a real trigger:
.github/workflows/reminder-dispatch.ymlcallsPOST /v1/internal/jobs/reminders/dispatchevery 15 minutes withX-Bookra-Job-Keyfrom 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: smswhen the tenant has SMS enabled (tenant_sms_settings.enabled) and the customer left a phone number; email jobs are unchanged notifications.Servicegained an injectedsmsSenderfunction adapter (wired tosms.Service.SendMessagein the API server, keeping the package dependency-free); dispatch routeschannel: smsthrough it with localized body text and the customer phone as recipientsms.Service.SendMessagestill enforces enabled/plan/monthly-limit and logs usage, so SMS reminders inherit metering and billing automatically; missing provider config or phone marks the jobfailedwith 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
- booking creation arms a second reminder job on
[done/partial]Milestone 16: Waitlist position displayGetWaitlistPositionrepository method (pgx + memory) returns the 1-based queue spot: count of earlier-created waitlisted bookings on the same session + 1CustomerBookingViewcarrieswaitlistPositionfor waitlisted class bookings; the manage page now shows a proper status badge forwaitlisted/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-softdo not exist;--error-subtle/--warning-subtledo) - manual promotion controls landed in Milestone 20
- remaining: live staging validation
[done/partial]Milestone 17: Class template editingPUT /v1/catalog/class-templates/{id}end-to-end:UpdateClassTemplatein both repositories, catalog service method sharingvalidateClassTemplateInputwith 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/Fullwhen empty, and the action switches toNa čekací listinu/Join waitlist(previously a full session still said "Book now" while silently producing a waitlisted booking) - reminder arming extracted into a shared
armReminderJobshelper (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_gistextension plus three partial exclusion constraints onbookingsmirroringresourcesCollidesemantics — same staff, same location, or both-unassigned appointments may not overlap (tstzrange,[)bounds so abutting slots are legal);cancelledrows andbooking_mode='class'are excluded db.IsExclusionViolationmaps SQLSTATE 23P01 toErrBookingConflictat 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:17container: 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 vianpm run db:migrate:upwithBOOKRA_DATABASE_DIRECT_URLpointed at it - remaining: live staging validation on Neon
- migration
[done/partial]Milestone 20: Class roster + manual waitlist promotionGET /v1/catalog/class-sessions/{id}/rosterreturns session metadata plusconfirmedandwaitlistentry lists (booking id, reference, customer name/email, status,waitlistPosition,createdAt);ClassSessionnow carrieswaitlistCountso session rows can badge queue length without an extra requestPOST /v1/catalog/bookings/{id}/promotemanually confirms a waitlisted booking:409when the session is full unless the caller passesallowOverCapacity,400for non-waitlisted bookings,404for missing or foreign bookings; promotion reuses the shared confirm path so waitlist ledger rows resolve and reminders re-arm automaticallyListBookingsBySessionrepository method (pgx + memory) backs the roster and simplifiesPromoteWaitlist;UpcomingBookingcarriesclassSessionId+waitlistPosition, computed per session in the dashboard summary- classes tab session rows expand into a two-column roster panel (attendees vs waitlist with
#Nposition 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,
allowOverCapacitypromoted 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 rebookingCustomerBookingViewnow carriestenantSlug,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 blankRescheduleBookinghardened: rejectswaitlisted/completedbookings and any class booking (session-bound times cannot move to arbitrary windows; switching sessions is cancel-and-rebook); the previously deadClassSessionID == nilbranch 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
CustomerBookingViewschema added to OpenAPI and referenced fromGET /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
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
subas the authenticated principal - backend now syncs
neon_subjectinto app-sideusersrecords 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_URLis configured - deterministic in-memory fallback when Neon is not configured
- Neon-backed pgx access when
- 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/dashboardis the main application entry/book/:tenantSlugstays 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-serviceis 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.
/dashboardnow 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
MaxStaffentitlements and staff location assignment validates tenant ownership. - Dashboard summary computes
setupCompletionfrom 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}/catalogand 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.
- Calendar integrations: ICS feed per tenant/staff and optional Google Calendar sync; high demand for salons, moderate complexity. Start with a read-only
.icssubscription URL — zero third-party auth needed. - 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
notificationstable 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