Crussell
Crussell is a full‑stack application that powers a nail‑bar / salon booking service. The repository is split into a Go backend and a SvelteKit front‑end, both of which are containerised with Docker. A lightweight SabreDAV instance is also exposed so that the salon can offer WebDAV access to clients.
📦 Project Structure
Crussell/
├─ backend/ # Go 1.25 + chi router API
├─ frontend/ # SvelteKit 5 SPA (static build)
├─ sabredav/ # PHP + Composer for DAV
├─ nginx/ # Nginx reverse‑proxy for HTTP & HTTPS
├─ init-scripts/ # PostgreSQL init SQL
├─ compose.yml # Docker‑Compose definition
├─ local-dev-2.sh # Development helper using tmux (seeded with 8 services incl. 2 with patch tests)
└─ README.md
⚙️ Prerequisites
| Tool | Version |
|---|---|
| Docker & Docker‑Compose | ≥ 20.10 |
| Go | ≥ 1.22 |
| Node | ≥ 18 (npm) |
| tmux | ≥ 3.0 |
Tip
: If you already have Docker Desktop or Docker Engine installed, you are good to go.
📥 Getting Started
# Clone the repository
git clone http://git.popertots.com/popertots/Crussell.git
cd Crussell
# Copy the example environment file and edit it
cp .env.example .env
# Open .env and provide values for POSTGRES_*, JWT_SECRET_KEY, etc.
Docker‑Compose
The simplest way to bring the whole stack up is with Docker‑Compose.
docker compose up --build -d
postgres– PostgreSQL 17backend– Go API (exposed on:8080)sabredav– PHP‑based WebDAV (served by Nginx)nginx– Reverse‑proxy (HTTP on:80and HTTPS on:443)
After the containers are running, the front‑end is reachable at http://localhost. The API is available at http://localhost/api. SabreDAV can be accessed via http://localhost/dav.
Development with local-dev-2.sh
For a more interactive dev experience the repository ships a small helper script that launches Docker, starts a tmux session with four panes (PostgreSQL console, Go dev server, Svelte dev server, Rustfs logs) and seeds the database with an admin, regular users, and sample services including patch test services.
chmod +x local-dev-2.sh
./local-dev-2.sh
The script performs the following steps:
- Docker checks – starts Docker if it isn't already running.
- PostgreSQL reset – removes the old volume and starts a fresh container.
- tmux session – creates
crussell-devwith panes:psqlconsole- Go server (
go run -tags dev ./main.go) - Svelte dev server (
npm run dev -- --host) - Rustfs logs
- Seeding – creates admin (
admin@example.com), regular users (user@example.com), 8 services (6 standard + 2 requiring patch tests), bookings, exceptional hours.
Note
: The script uses a temporary shell script to perform the HTTP calls, so no external tooling like
jqis required.
🔧 Building & Testing
Backend
cd backend
# Build the binary
go build -o bin/backend ./main.go
The binary is then copied into the Docker image via the Dockerfile.
Frontend
cd frontend
npm ci
npm run build # Production build (static)
npm run dev # Development server
SabreDAV
SabreDAV is bundled with PHP‑FPM and Composer. The Docker image installs dependencies automatically during the container start‑up.
📂 Environment Variables
| Variable | Purpose | Example |
|---|---|---|
POSTGRES_USER |
DB username | myuser |
POSTGRES_PASSWORD |
DB password | mysecret |
POSTGRES_DB |
DB name | mydb |
JWT_SECRET_KEY |
HMAC key for JWT (HS256) | supersecret |
SABRE_DAV_* |
Optional SabreDAV overrides | – |
Create a .env file in the project root based on the provided .env.example.
Note
: JWT tokens expire after 30 days. Login is rate-limited to 1 attempt per 5 seconds.
📊 Seeding Data
The local-dev-2.sh script automatically seeds:
- Admin user (
admin@example.com/password) - Regular user (
user@example.com/password) - 8 services:
- 6 standard (no patch test)
- 2 with patch test requirement (48h) - Gel Polish Full Set, Luxury Gel Manicure
The enhanced local-dev-2.sh script provides additional test data including:
- Multiple users with different roles and account types
- Working hours configurations
- Exceptional working hours groups (holiday schedules)
- Sample bookings with various statuses
- Payment records
- Admin notifications
- User referrals
If you want to seed manually, use the provided init-scripts/init-script.sql and your favourite Postgres client.
📌 Useful Commands
# Show Docker containers
docker ps
# Rebuild the Go binary and restart containers
make build-backend
docker compose up -d backend
# Tail logs
docker compose logs -f
# Open a shell inside the backend container
docker compose exec backend sh
🏗️ Implementation Status
✅ Complete
| Feature | Backend | Frontend | Notes |
|---|---|---|---|
| JWT Authentication | ✅ | ✅ | Login, register, refresh, middleware; HS256, 30-day expiry, auto-refresh |
| Booking CRUD | ✅ | ⚠️ | Backend complete; frontend submit is stub (logs only) |
| Admin Endpoints | ✅ | ✅ | Services, users, today view, notifications |
| Scheduling System | ✅ | ✅ | Default hours, exceptional hours, working/available hours |
| Holiday Hours | ✅ | ✅ | Integrated in all 3 booking flows |
| Loyalty Backend | ✅ | ❌ | DB schema ready, no frontend component |
| VAT System | ✅ | ❌ | get_vat_return_data(), calculate_vat() functions exist |
| User Referrals | ✅ | ❌ | user_referrals table, backend logic exists |
| Token Refresh | ✅ | ✅ | POST /api/refresh-token, auto-refresh in auth store |
| Portfolio System | ✅ | ✅ | S3/R2 storage abstraction, tag-based filtering, category filters, admin upload, ?img= featured image param |
| Service Eligibility | ✅ | ✅ | Age + patch test filtering; /api/services/eligible-for/{user_id} for admin booking flows |
| Image Metadata Stripping | ✅ | ❌ | EXIF/GPS stripped on upload via imaging library |
| Profile Pictures | ✅ | ✅ | Upload to separate bucket, cropper, circular display, CalDAV sync |
| Auto-Booking Status | ✅ | ✅ | Auto-transition: confirmed → in_progress → completed based on time |
| Simplified Deposits | ✅ | ❌ | deposits_required INT on users table (3 default), reduces on payment |
| Contact Page | ✅ | ✅ | Dynamic data from first admin user via /api/contact endpoint |
⚠️ Partially Complete
| Feature | Status | Details |
|---|---|---|
| Notifications | Backend only | Admin notifications wired; acknowledgment on confirm/cancel; no frontend panel, no push (email/SMS), no regular user notifications |
| User Notification Preferences | DB ready | Table user_notification_preferences exists; waiting on user notification system |
| GDPR Compliance | DELETE only | anonymize_user(), export_all_user_data() exist but only DELETE is wired to endpoint |
| Analytics | Handler exists | handlers/admin/analytics.go exists but NOT wired in router |
❌ Not Wired (Handlers Exist)
| Handler | File | Notes |
|---|---|---|
| Social Auth | handlers/auth/social.go |
OAuth integration placeholder |
| Analytics | handlers/admin/analytics.go |
Statistics/dashboard endpoint |
🚧 Critical TODOs
| Location | Issue | Priority |
|---|---|---|
BookingFlow.svelte:600 |
submitBooking() only logs, needs POST implementation |
High |
/api/users/guest |
Guest endpoint for walk-ins not implemented | Medium |
| GDPR Export | Need endpoint for export_all_user_data() |
Medium |
| Tax Export | Endpoint for VAT return data export | Medium |
📋 Feature Requests
| Feature | Description |
|---|---|
| One-off Custom Services | Allow creating single-use services not in regular catalog |
| One-off Exceptional Hours | Single-day overrides without creating a group |
🔄 Booking Flows
Crussell supports three distinct booking flows:
| Flow | User | Entry Point | Status |
|---|---|---|---|
| Self-Service | Customer | BookingFlow.svelte → /api/bookings |
⚠️ Stub submit |
| Walk-In | Admin | Admin panel → walk-in modal | ✅ |
| Call/Message-In | Admin | Admin panel → booking modal | ✅ |
All three flows integrate with the holiday/exceptional hours system to prevent bookings during closed periods.
🔍 Helpful Greps
All Three Booking Flows
Backend - All Booking Creation Endpoints:
grep -rn "func.*Create.*Booking\|func.*WalkIn\|func.*Walk.*In\|POST.*booking" backend/handlers/ --include="*.go"
Frontend - All Booking Flow Components:
grep -rln "BookingFlow\|WalkIn\|walk-in\|call.*in\|message.*in" frontend/src/lib/components/ --include="*.svelte"
Combined - Single View of All Booking Flows:
grep -rn "CreateBooking\|CreateWalkIn\|BookingFlow\|submitBooking" backend/ frontend/ --include="*.go" --include="*.svelte"
Database Schema
All Enums:
grep -n "CREATE TYPE" init-scripts/init-script.sql
All Tables:
grep -n "CREATE TABLE" init-scripts/init-script.sql
All Functions:
grep -n "CREATE OR REPLACE FUNCTION" init-scripts/init-script.sql
Router Endpoints
All Wired Routes:
grep -n "r\.\(Get\|Post\|Put\|Delete\|Patch\)" backend/main.go
Middleware Chain:
- RequestID, RealIP, Logger, Recoverer, Timeout(15s)
- Security headers (X-Content-Type-Options, X-Frame-Options, X-XSS-Protection)
- Rate limiting (per-endpoint):
- Public read-only: 120/min
- Registration: 10/min
- Portfolio filters: 60/min
- Portfolio single image: 120/min
- Portfolio admin: 60/min
- Authenticated users: 120/min
- Admin search: 60/min
- Admin-only routes: none (trusted)
- ⚠️ Gap: Rate limiter doesn't read CF-Connecting-IP - behind Cloudflare all users share one bucket
- ⚠️ Gap: No HSTS header - add when HTTPS working
- ⚠️ Gap: No Referrer-Policy - for analytics tracking
- ✅ Image metadata stripping - EXIF/GPS stripped on upload (security improvement)
Input Validation:
- Backend validates all inputs against DB schema constraints
- Frontend adds maxlength attributes matching DB limits
- Registration: name (1-50), email (255), phone (20), password (72)
- Services: name (100), price (>0), duration (1-480), patch test (0-168), age (0-100)
- Portfolio: tags/filters (256 char max)
grep -n "r\.Use\|r\.Group" backend/main.go
Authentication
JWT Middleware:
grep -rn "JWTMiddleware\|VerifyToken\|ParseToken" backend/ --include="*.go"
Protected Routes:
grep -n "r\.Group.*Auth" backend/main.go
Holiday/Exceptional Hours
Usage Across All Flows:
grep -rn "exceptional.*hours\|holiday.*hours\|getExceptional\|getHoliday" backend/handlers/ frontend/src/ --include="*.go" --include="*.svelte"
Notifications
Admin Notifications:
grep -rn "admin_notifications\|AdminNotification" backend/ --include="*.go"
Notification Reasons (Enum Values):
grep -A10 "admin_notification_reason" init-scripts/init-script.sql
GDPR Functions
Data Export/Anonymization:
grep -rn "anonymize_user\|export_all_user_data\|delete_guest_user" backend/ --include="*.go" init-scripts/
Frontend Debug Artifacts
Console Logs to Remove:
grep -rn "console\.log" frontend/src/ --include="*.svelte" --include="*.ts"
🗄️ Database Schema Overview
Enums
| Enum | Values |
|---|---|
account_role |
user, admin |
account_type |
standard, vip, guest |
booking_status |
pending, confirmed, in_progress, completed, cancelled, no_show |
payment_type |
deposit, full, refund |
payment_method |
cash, card, bank_transfer, square |
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 |
Key Tables
| Table | Purpose |
|---|---|
users |
Customer and admin accounts |
services |
Salon service catalog |
bookings |
Appointment records |
booking_services |
Services per booking (many-to-many) |
payments |
Payment transactions |
working_hours |
Default weekly schedule |
exceptional_working_hours_groups |
Holiday/special schedules |
admin_notifications |
Admin alerts |
user_notification_preferences |
User notification preferences (email/sms/push) |
user_referrals |
Referral tracking |
Validation Rules
| Field | Rules |
|---|---|
| Names | 1-50 chars, unicode letters/spaces/hyphen/apostrophe/dot |
| Phone | UK format → E.164 (+44...) |
| Age | Must be 16+ years |
| Login Rate Limit | 1 attempt per 5 seconds |
Happy coding!