24 KiB
Last Updated: February 2026 Status: Work in Progress
Project Checklist
Backend (Go / Chi / Postgres)
Authentication & Authorization
- JWT authentication (login, refresh, role verification)
- User registration with input validation
- Names: 1-50 chars, unicode letters/spaces/hyphen/apostrophe/dot
- Phone: UK format → E.164 (+44...)
- Email: standard format
- Age: Must be 16+ years
- Password hashing with bcrypt
- Middleware for auth/roles (
mw.RequireAuth,mw.RequireAdmin) - DB connection pooling
- Refresh token endpoint - Wired to
POST /api/refresh-token, auto-refresh in frontend - Login rate limiting (1 attempt per 5 seconds)
- Global rate limiting middleware (per-endpoint: 120/min public, 10/min register, 60/min filters, none admin)
- Security headers (X-Content-Type-Options, X-Frame-Options, X-XSS-Protection)
- Strict-Transport-Security (HSTS) - Tell browsers to only access via HTTPS, prevents downgrade attacks. Add after HTTPS is working in prod.
- Referrer-Policy - Track referrer sources for analytics (social media tracking). Use
strict-origin-when-cross-originto send origin but not full URLs. - Rate limiter + Cloudflare - Currently doesn't read CF-Connecting-IP header, so behind Cloudflare all users share one rate limit bucket.
- Account creation spam - Registration endpoint (10/min) could benefit from additional bot protection beyond rate limiting.
- Input validation on all endpoints:
- Registration: name (1-50), email (255), phone (20), password (72), age 16+
- Services: name (100), price (>0), duration (1-480), patch test (0-168), age (0-100)
- Portfolio: tags/filters (256 char max), filter category validation, image ID pattern security
Booking System
/api/bookings- Full CRUD for authenticated users/api/admin/bookings- List, search, create for user, progress, confirm, cancel/api/admin/bookings/search- Search functionality/api/admin/bookings/user/{user_id}- User-specific bookings/api/admin/bookings/{id}/progress- Progress booking status/api/admin/bookings/{id}/confirm- Confirm booking/api/admin/bookings/{id}/cancel- Cancel booking- In-progress auto-infer - Status should auto-set based on time
- Begin button on Today - Manual start for early arrivals (gray out if >3hrs away)
Admin Endpoints
/api/admin/services- Create, delete, list, toggle/api/admin/users- List, view with booking history/api/admin/today- Current/next appointment, today's appointments, pending approvals/api/admin/notifications- GET/acknowledge endpoint wired, but:- Pending notification acknowledgment - Acknowledged on confirm/cancel (not deleted)
- Cancelled booking notification - Created when cancelling non-pending bookings
- Frontend UI to display notifications
- Push mechanism (currently only pull-based)
- User notifications (only admin notifications exist)
- User notification preferences (DB table ready)
Scheduling System
/api/scheduling/default-hours- GET public, PUT admin/api/scheduling/exceptional-groups- CRUD for holiday/special hours/api/scheduling/working-hours- Merged default + exceptional hours/api/scheduling/available-hours- Available slots accounting for bookings
User Endpoints
/api/user/profile- GET, PUT/api/user/account- DELETE (GDPR compliant)/api/user/loyalty- GET loyalty stamps- GDPR data export -
export_all_user_data()exists but not wired to endpoint - Tax data export - Admin endpoint for tax-software-compatible format
Not Yet Wired
- Social auth (
handlers/auth/social.goexists, not imported) - Analytics (
handlers/admin/analytics.goexists, not imported) - Portfolio/images - NOW WIRED:
/api/portfolio/images,/api/portfolio/tags,/api/portfolio/filters,/api/portfolio/images/{id} - Guest user endpoint (
/api/users/guest- needed for walk-in bookings)
Unit Tests & CI/CD
- Unit tests
- CI/CD pipeline
Frontend (SvelteKit / Tailwind / shadcn)
Core Pages
- Home (
/) - Prices (
/prices) - Contact (
/contact) - Book (
/book) - Full wizard with service selection, date/time, customer details - Portfolio (
/portfolio) - S3/R2 storage with tag filtering, category filters, pagination, ?img= featured image, admin upload - Today (
/today) - Admin only, real-time schedule view - Account (
/account) - Login (
/login) - Manage (
/manage)
Admin Dashboard (/admin)
- Auth guard with role check
- ImageUpload component
- UsersCard + UserModal
- BookingsCard + BookingModal
- HolidayHours (exceptional hours management)
- WeeklySchedule (default hours management)
- ServicesManagement
- BookingCreateModal (call-in/admin booking creation)
- WalkInBooking + WalkInCreateModal
- CallInBooking
- ApprovalModal
Booking Flow
- Service selection with pricing/duration
- Calendar with availability detection
- Time slot generation with gap logic
- Customer details form (guest or authenticated)
- Auth store with token refresh logic
- Customer booking submit -
submitBooking()only logs, needsPOST /api/bookings - Payment integration (Square placeholder)
API Integration
- Services fetch from
/api/services - Working hours fetch from
/api/scheduling/working-hours - Available hours fetch from
/api/scheduling/available-hours - Admin bookings use
/api/admin/bookings - Guest user creation (
/api/users/guestnot implemented)
Infrastructure
.envconfig- Docker Compose (postgres, backend, sabredav, nginx)
- Static frontend build served via nginx
- nginx reverse proxy - config under review
- Monitoring/logging - no stack configured
- CI/CD pipeline (Gitea) - not yet defined
- Prometheus metrics integration
Integrations
- CardDAV sync for contacts (SabreDAV)
- CalDAV ready
- Email/SMS reminders - not yet implemented
- Square payment - placeholder only
- S3/R2 image hosting - Rustfs for dev, Cloudflare R2 for prod via build tags
Current Architecture
Overview Diagram
flowchart TD
User([Customer])
Admin([Admin])
subgraph Docker[Docker Compose Stack]
subgraph NGINX[Nginx :80/:443]
Static[Static Frontend Build]
Proxy[API Proxy → Backend:8080]
DAVProxy[DAV Proxy → SabreDAV]
end
subgraph Backend[Go + Chi :8080]
Router[chi Router]
Auth[JWT Middleware]
Handlers[API Handlers]
end
subgraph Database[PostgreSQL :5432]
DB[(Users / Bookings<br/>Payments / Services<br/>Scheduling)]
end
subgraph DAV[SabreDAV :9000]
CardDAV[(vCard Contacts)]
CalDAV[(Calendar Events)]
end
end
subgraph External[External Services - TODO]
Gmail[Gmail SMTP]
SquareAPI[Square API]
S3[S3/R2 Storage]
end
User -->|HTTPS| NGINX
Admin -->|HTTPS| NGINX
Static --> User
Proxy --> Router
Router --> Auth
Auth --> Handlers
Handlers --> DB
Handlers --> DAV
Handlers -.->|TODO| Gmail
Handlers -.->|TODO| SquareAPI
Handlers -.->|TODO| S3
API Reference
Public Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /api/services |
List active services |
| POST | /api/register |
Create new user account |
| POST | /api/login |
Authenticate and receive JWT |
| GET | /api/scheduling/default-hours |
Get weekly default hours |
| GET | /api/scheduling/exceptional-groups |
List holiday/special hour groups |
| GET | /api/scheduling/working-hours |
Get merged working hours for date range |
| GET | /api/scheduling/available-hours |
Get available booking slots |
Authenticated User Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /api/user/profile |
Get current user profile |
| PUT | /api/user/profile |
Update profile |
| DELETE | /api/user/account |
Delete account (GDPR) |
| GET | /api/user/loyalty |
Get loyalty stamp count |
| GET | /api/bookings |
List user's bookings |
| POST | /api/bookings |
Create booking |
| GET | /api/bookings/{id} |
Get specific booking |
| PUT | /api/bookings/{id} |
Update booking |
| DELETE | /api/bookings/{id} |
Cancel booking |
Admin Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /api/admin/services |
List all services |
| POST | /api/admin/services |
Create service |
| DELETE | /api/admin/services/{id} |
Delete service |
| PUT | /api/admin/services/{id}/toggle |
Toggle active status |
| GET | /api/admin/bookings |
List all bookings |
| POST | /api/admin/bookings |
Create booking for user |
| GET | /api/admin/bookings/search |
Search bookings |
| GET | /api/admin/bookings/user/{user_id} |
User's bookings |
| PUT | /api/admin/bookings/{id}/progress |
Progress status |
| POST | /api/admin/bookings/{id}/confirm |
Confirm booking |
| POST | /api/admin/bookings/{id}/cancel |
Cancel booking |
| GET | /api/admin/users |
List users |
| GET | /api/admin/users/{id} |
Get user details |
| GET | /api/admin/today/current-next |
Current and next appointment |
| GET | /api/admin/today/appointments |
Today's appointments |
| GET | /api/admin/today/pending-approvals |
Pending approval queue |
| GET | /api/admin/notifications |
List notifications |
| POST | /api/admin/notifications/{id}/acknowledge |
Acknowledge |
| PUT | /api/scheduling/default-hours |
Update weekly hours |
| POST | /api/scheduling/exceptional-groups |
Create exception group |
| DELETE | /api/scheduling/exceptional-groups |
Delete exception group |
| PUT | /api/scheduling/exceptional-applications |
Apply exceptions to dates |
Portfolio Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /api/portfolio/images |
List images with pagination, filter by tag/tags, filter by category:value |
| GET | /api/portfolio/tags |
List all tags for autocomplete (public) |
| GET | /api/portfolio/filters |
Get filter categories with counts. Supports tag filtering. Unselected categories show counts reduced by other filters. |
| GET | /api/portfolio/images/{id} |
Get single image by UUID or timestamp (fallback to URL pattern match) |
| POST | /api/portfolio/images |
Upload new image with thumbnail and tags (admin only) |
| DELETE | /api/portfolio/images/{id} |
Delete image (admin only) |
Portfolio Frontend Features:
/portfoliopage with tag-based filtering and category filters- Category filters:
?filter[color]=red&filter[season]=summer - Tag search:
?tag=tobyor?tags=toby,summer - Featured image:
?img=<id|timestamp>- loads image directly, bypasses filters - Filter UI: scrollable dropdowns, keyboard navigation, mobile-optimized
- Admin upload: ImageUpload component with live tag suggestions, keyboard nav, confirmation modal
Database Schema
Enums
account_role: unverified_email | verified_email | admin | guest | affiliate
account_type: email | google | microsoft | facebook | guest
booking_status: pending | confirmed | in_progress | completed | client_cancelled | we_cancelled | re-schedule | no_show
payment_type: deposit | full | tip | balance | partial
payment_method: online_square | in_person_card | cash | giftcard | discount
payment_status: pending | completed | failed | refunded
admin_notification_reason: pending_booking | cancelled_booking | rescheduled_booking | 1_week_no_pay | 1_month_no_pay | affiliate_claim
Suggested additional admin_notification_reason values:
| Reason | Purpose |
|---|---|
payment_failed |
Payment processing failed |
patch_test_due |
Customer needs patch test before appointment |
first_time_customer |
New customer's first booking |
inactive_customer |
Regular hasn't booked in X months - 5% discount (non stacking) |
birthday_this_week |
Customer birthday - 5% discount (stacking) |
schedule_conflict |
Potential double-booking detected - admin alert, 'second customer' alert |
Core Tables
| Table | Purpose |
|---|---|
users |
User accounts with profile data |
user_social_logins |
Social auth provider links |
services |
Service offerings |
user_service_patch_tests |
Patch test tracking |
bookings |
Appointment records |
booking_services |
Services per booking |
user_referrals |
Referral tracking |
working_hours |
Default weekly schedule |
exceptional_working_hours_groups |
Holiday/special hour groups |
exceptional_working_hours |
Hours for exception groups |
exceptional_group_applications |
Apply exceptions to date ranges |
payments |
Payment transactions |
business_settings |
Business configuration |
admin_notifications |
Admin notification queue |
images |
Portfolio gallery images with tags |
tags |
Image tag autocomplete |
user_notification_preferences |
User notification preferences (email/sms/push) |
Key Functions
| Function | Purpose |
|---|---|
generate_short_id() |
Generate 12-char IDs |
anonymize_user() |
GDPR data removal |
delete_guest_user() |
Clean up guest accounts |
export_all_user_data() |
GDPR subject access |
get_monthly_business_summary() |
Analytics |
get_vat_return_data() |
VAT reporting |
calculate_vat() |
VAT calculation |
get_receipt_data() |
Receipt generation |
Remaining Work
High Priority
| Task | Description | Files Affected |
|---|---|---|
| Customer booking submit | submitBooking() at line 600 only logs, needs POST /api/bookings |
frontend/src/lib/components/booking/BookingFlow.svelte |
| Remove console.logs | Debug logs left in: BookingFlow.svelte:600, BookingCreateModal.svelte:224 |
Frontend components |
| Guest user endpoint | Create /api/users/guest for walk-in bookings |
backend/handlers/user/ (new file) |
| In-progress auto-infer | Auto-set in_progress status based on time |
Backend booking logic |
| Begin button (Today) | Manual start for early arrivals, gray out if >3hrs away | CurrentAppointment.svelte + backend |
| One-off custom services | Admin creates custom service for single booking without adding to main list | Backend + frontend booking modals |
| One-off exceptional hours | Single-day exceptions (dentist, afternoon off) - not yearly/weekly | Backend scheduling + frontend HolidayHours |
| Auto lunch protection | Block bookings that remove lunch break (1h customer, 30min admin with warning) | Backend available-hours logic |
| Walk-in slot blocking | Properly block next available slot during walk-in intake | WalkInCreateModal.svelte |
| Square payment integration | Full Square SDK integration | Backend payment handlers + frontend payment step |
| GDPR data export | User button for "give me my data" using export_all_user_data() |
Backend endpoint + account page |
| Tax data export | Admin button for tax-software-compatible format | Backend endpoint + admin page |
Medium Priority
| Task | Description |
|---|---|
| Notifications UI | Frontend panel to display admin notifications |
| Notifications push | Real-time notification mechanism (WebSocket/polling) |
| User notification preferences | [x] DB table ready, waiting on user notification system |
| User notifications | Notification system for regular users (booking confirmations, reminders) |
| Remove debug logs | console.log in BookingFlow.svelte:600 and BookingCreateModal.svelte:224 |
| Loyalty display component | Show stamps in account/bookings |
| Email/SMS reminders | Scheduled notification jobs |
| Prometheus metrics | Monitoring integration |
Low Priority
| Task | Description |
|---|---|
| Social auth | Wire handlers/auth/social.go |
| Analytics | Wire handlers/admin/analytics.go |
| nginx config review | Finalize production config |
| CI/CD pipeline | Gitea Actions workflow |
| And many more |
Environment Variables
| Variable | Purpose | Required |
|---|---|---|
JWT_SECRET_KEY |
Secret for JWT signing | Yes |
DATABASE_URL |
PostgreSQL connection string | Yes |
POSTGRES_USER |
Database username | Docker |
POSTGRES_PASSWORD |
Database password | Docker |
POSTGRES_DB |
Database name | Docker |
Docker Compose
services:
postgres:
image: postgres:17
volumes:
- pgdata:/var/lib/postgresql/data
- ./init-scripts/init-script.sql:/docker-entrypoint-initdb.d/init-script.sql:ro
backend:
build: ./backend
depends_on: [postgres]
sabredav:
image: php:8.2-fpm
volumes:
- ./sabredav:/var/www/dav
depends_on: [postgres]
nginx:
image: nginx:stable
ports: ["80:80", "443:443"]
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./frontend/build:/usr/share/nginx/html
depends_on: [backend, sabredav]
Frontend Component Structure
src/lib/components/
├── admin/
│ ├── ApprovalModal.svelte
│ ├── BookingCreateModal.svelte
│ ├── BookingModal.svelte
│ ├── BookingsCard.svelte
│ ├── CallInBooking.svelte
│ ├── HolidayHours.svelte
│ ├── ImageUpload.svelte
│ ├── ServicesManagement.svelte
│ ├── UserModal.svelte
│ ├── UsersCard.svelte
│ ├── WalkInBooking.svelte
│ └── WalkInCreateModal.svelte
├── booking/
│ ├── BookingActions.svelte
│ ├── BookingFlow.svelte
│ ├── BookingSummary.svelte
│ ├── DatePicker.svelte
│ ├── ServiceCard.svelte
│ ├── ServiceSelector.svelte
│ ├── StepIndicator.svelte
│ └── TimeSlotPicker.svelte
└── today/
├── CurrentAppointment.svelte
├── PendingApprovals.svelte
└── TodayCalendar.svelte
Notes
- Holiday hours ARE integrated into all 3 booking flows (customer, call-in, walk-in) via
/api/scheduling/working-hoursand/api/scheduling/available-hours - The backend merges default hours with applied exceptional hours automatically
- Static frontend is built and served by nginx; API calls go directly to Go backend in production
- Local dev uses SvelteKit's API proxy for CORS avoidance
- GDPR functions exist in SQL (
anonymize_user,export_all_user_data,delete_guest_user) but onlyDELETE /api/user/accountis wired - need user data export and admin tax export endpoints - Debug console.logs in
BookingFlow.svelte:600andBookingCreateModal.svelte:224should be removed before production - Notifications are pull-based only (no push/WebSocket). Admin endpoint exists but no frontend UI. No user-facing notification system yet.
- Notification acknowledgment: When a booking is confirmed, any pending notification is acknowledged. When cancelled, pending is acknowledged and cancelled_booking notification is only created if the booking was not in pending status (e.g. was confirmed or in_progress).
- User notification preferences:
user_notification_preferencestable exists with email/sms/push enabled flags, waiting on user notification system to be implemented.
Development Workflow
Quick Start
./local-dev-2.sh # Creates tmux session 'crussell-dev'
Creates 3 panes:
- Pane 0: psql interactive shell
- Pane 1: Backend (
go run -tags dev ./main.go) - Pane 2: Frontend (
npm run dev -- --host)
Dev Build Tag
Backend uses -tags dev - check for dev-specific behavior in code with build constraints.
Admin User Setup
No API endpoint exists for role promotion. Admin users are created via direct SQL:
UPDATE users SET account_role = 'admin' WHERE email = 'admin@example.com';
Seed Data
Running local-dev-2.sh creates:
| Resource | Count | Details |
|---|---|---|
| Users | 18 | 1 admin, 17 regular users |
| Services | 6 | Classic Manicure, Gel Manicure (BIAB), Luxury Pedicure, Express Mani & Pedi, Gel Removal, Nail Art Add-on |
| Bookings | 45 | 8 past, 3 today, 4 tomorrow, 30 future (spread over 15 days) |
| Confirmed | ~50% | Random selection of upcoming bookings auto-confirmed |
| Exceptional | 2 | November Break (closed), Christmas Holiday (reduced hours) |
API JSON Examples
Create Booking:
{
"start_time": "2025-01-15T10:00:00+00:00",
"service_ids": ["abc123def456"],
"notes": "Optional notes"
}
Create Service:
{
"name": "Classic Manicure",
"description": "Nail shaping, cuticle care, hand massage, and polish.",
"price": 25.00,
"duration_minutes": 45,
"patch_test_duration_hours": 0,
"minimum_age_required": 0
}
Confirm Booking:
POST /api/admin/bookings/{id}/confirm
Body: {"serviceOverrides": []}
Create Exceptional Group:
{
"name": "Christmas Holiday Period",
"description": "Reduced hours for Christmas and New Year",
"hours": [
{"weekday": 0, "startTime": "00:00:00", "endTime": "00:00:00", "isOpen": false},
{"weekday": 1, "startTime": "10:00:00", "endTime": "15:00:00", "isOpen": true}
],
"weekStarts": ["2025-12-22", "2025-12-29"]
}
Time Format
All timestamps use ISO 8601 with timezone: YYYY-MM-DDTHH:MM:SS±HH:MM (e.g., 2025-01-15T10:00:00+00:00)
Timezone is always Europe/London (handles BST automatically).
Code Patterns
Transaction Pattern
Used throughout for atomic operations:
tx, err := db.DB.Begin(r.Context())
if err != nil { /* handle error */ }
defer tx.Rollback(r.Context())
// Use tx instead of db.DB for queries
err = tx.QueryRow(r.Context(), `INSERT INTO...`)
if err := tx.Commit(r.Context()); err != nil { /* handle error */ }
CardDAV Synchronization
- On registration: Creates vCard in SabreDAV via
dav.Service.CreateContact() - On profile update: Updates existing vCard via
updateCardDAV()helper - Uses internal HTTP calls to DAV server
Role Change Detection
RefreshTokenHandler verifies user's current role hasn't changed since token was issued. If role changed, forces re-login with 401 Unauthorized.
Build Tags
db_dev.go- Used with-tags devfor local development (localhost connection)db.go- Production build (uses env var for host)internal/dav/service_dev.go/service_prod.go- Same pattern for DAV service