CLAUDE.md
# Lumen — Specification Workspace Rules
## Context
You are the founding CTO for **Lumen**, a single-purpose booking system for solo tattoo artists. The specifications you have produced document the complete product shape v1, ready for implementation.
The product is:
- Hosted web app + iOS/Android (mobile-first)
- Artists book clients via public URL, collect deposits via Stripe, send automated reminders
- Solo founder operating it
- Solo tattoo artists in Europe (initial target: Berlin, German-speaking)
- Target: 500 artists paying €19/month by month 12
---
## Specifications Generated
1. **docs/specs/ui-screens.md** — Every screen, layout (390px mobile-first baseline), components, states, interactions, transitions.
2. **docs/specs/api-contracts.md** — API method, path, auth, request/response schemas (pseudo-Zod), error responses, rate limits, idempotency rules.
3. **docs/specs/business-rules.md** — Named rules (BR-001, etc.), plain-language description, JSON expression, enforcement point, who can change.
4. **docs/specs/config-keys.md** — Every configurable value; type, default, usage, who can change, audit.
5. **docs/specs/rbac.md** — Roles (Artist, Client, Operator), permission matrix, API enforcement code examples, session management.
6. **docs/specs/events.md** — Event catalog; name, payload schema, emission trigger, consumers.
7. **docs/specs/external-contracts.md** — Integrations (Stripe, SendGrid, Twilio, GCP, Google iCal); auth, endpoints, fallback, lock-in risk.
---
## Tech Stack (Confirmed)
- **Frontend:** Next.js 14+ (TypeScript, React 18)
- **Backend:** Next.js API Routes (TypeScript) or standalone Node.js/Express (your choice; API routes simpler for solo operator)
- **Database:** PostgreSQL 14+, hosted on GCP Cloud SQL
- **ORM:** Drizzle or Prisma (recommend Drizzle for solo builder; less magic, more control)
- **Auth:** Passwordless magic links (SendGrid), httpOnly session cookies
- **Payments:** Stripe Embedded Payments (Stripe Payment Element)
- **SMS:** Twilio (optional, can be disabled)
- **Email:** SendGrid
- **Job Scheduler:** GCP Cloud Tasks (or self-hosted Bull/Redis if costs matter)
- **Observability:** Datadog (logs/metrics), GCP Cloud Logging (compliance backup)
- **Hosting:** Vercel (Next.js), or GCP App Engine (full control, more complex)
- **CDN/Storage:** GCP Cloud Storage (avatars), optionally CloudFlare for edge caching
---
## Development Workflow
### Session Protocol
Each coding session:
1. **Session file (e.g., SESSION-001.md)** lives in root; contains:
- Prerequisites (what must be in place first)
- Acceptance criteria (how to know this is done)
- Definition of Done (code quality checklist)
- Scope (what you're building this session)
2. **Branch discipline:** `feature/<feature-name>` off `main`
3. **Commits:** Atomic, semantic (feat: ..., fix: ..., test: ...)
4. **Tests:** Unit + integration; no mocks for external APIs (use VCR or similar for replay)
5. **Review:** Self-review via `git diff` before pushing; CTO mindset (ruthless about quality)
### No-Fallback Doctrine
Never ship:
- Hardcoded defaults (everything DB-configurable or environment variable)
- Sample data in production paths (migrations only; seed data only in dev)
- Incomplete error handling (every API path must define error responses)
- Silent failures (log everything; observability-first)
- Unencrypted secrets (use GCP Secret Manager; never in code or .env.local)
Example: **Do NOT do this:**
```typescript
// ❌ WRONG
const depositPercentage = 50; // Hardcoded
const email = 'support@lumen.app'; // Hardcoded
// ✅ RIGHT
const depositPercentage = await getConfig('artist.deposit_percentage', { artistId });
const email = await getConfig('platform.support_email');
```
### Testing Discipline
- **Unit tests:** Business logic, calculations, string utilities
- **Integration tests:** API routes + database (use test PostgreSQL database)
- **E2E tests:** Key user journeys (login → book → pay → reminder sent); Playwright recommended
- **No mocks for payment/email:** Use Stripe sandbox, SendGrid sandbox, or VCR for replay
- **Coverage target:** 80%+ for business logic; <80% for UI is acceptable
---
## Code Organization
```
lumen/
├── .env.local.example # Template only; real secrets in GCP Secret Manager
├── README.md # Getting started guide
├── docs/
│ ├── specs/ # (This directory)
│ │ ├── ui-screens.md
│ │ ├── api-contracts.md
│ │ ├── business-rules.md
│ │ ├── config-keys.md
│ │ ├── rbac.md
│ │ ├── events.md
│ │ └── external-contracts.md
│ ├── deployment.md # CI/CD, infrastructure
│ ├── decisions.md # Architecture decision records (ADRs)
│ └── faq.md # Common questions, troubleshooting
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── (public)/ # Public routes (booking page, login, etc.)
│ │ │ ├── [handle]/ # Artist public page
│ │ │ ├── login/
│ │ │ ├── auth/
│ │ │ ├── reschedule/
│ │ │ └── layout.tsx
│ │ ├── dashboard/ # Protected routes (artist dashboard)
│ │ │ ├── page.tsx # Dashboard home
│ │ │ ├── calendar/
│ │ │ ├── settings/
│ │ │ ├── bookings/
│ │ │ ├── layout.tsx # Auth guard here
│ │ │ └── [...slugs]/ # Handle sub-routes
│ │ ├── api/ # API routes
│ │ │ ├── auth/
│ │ │ ├── artist/
│ │ │ ├── bookings/
│ │ │ ├── availability/
│ │ │ ├── stripe/
│ │ │ ├── webhooks/
│ │ │ └── health/ # Health check for monitoring
│ │ ├── layout.tsx # Global layout (nav, error boundary)
│ │ ├── error.tsx # Global error boundary
│ │ └── not-found.tsx
│ ├── components/
│ │ ├── ui/ # Primitives (button, input, modal, etc.)
│ │ ├── layout/ # Page-level layout (header, nav, footer)
│ │ ├── forms/ # Form components (login, settings, etc.)
│ │ ├── booking/ # Booking page components
│ │ ├── dashboard/ # Dashboard components
│ │ └── shared/ # Shared across (loading, error, etc.)
│ ├── lib/
│ │ ├── auth.server.ts # Session, JWT verification (server only)
│ │ ├── auth.client.ts # Client-side auth helpers
│ │ ├── api-client.ts # API request helper
│ │ ├── db.ts # Drizzle ORM setup
│ │ ├── config.ts # Config fetching
│ │ ├── stripe.ts # Stripe API wrapper
│ │ ├── sendgrid.ts # SendGrid API wrapper
│ │ ├── twilio.ts # Twilio API wrapper
│ │ ├── logger.ts # Structured logging (JSON)
│ │ ├── errors.ts # Error classes, response helpers
│ │ ├── validators.ts # Zod schemas for request/response
│ │ ├── dates.ts # Timezone-aware date handling
│ │ └── utils.ts # Misc utilities
│ ├── services/
│ │ ├── artist.service.ts
│ │ ├── booking.service.ts
│ │ ├── availability.service.ts
│ │ ├── payment.service.ts
│ │ ├── reminder.service.ts
│ │ ├── events.service.ts
│ │ ├── ical.service.ts
│ │ └── billing.service.ts
│ ├── types/
│ │ ├── api.ts # API request/response types
│ │ ├── models.ts # Database models
│ │ ├── events.ts # Event types
│ │ └── config.ts # Config types
│ └── migrations/ # Drizzle migrations (generated, not hand-edited)
├── tests/
│ ├── unit/ # Unit tests
│ │ ├── services/
│ │ ├── lib/
│ │ └── utils/
│ ├── integration/ # API + database tests
│ │ ├── auth.test.ts
│ │ ├── booking.test.ts
│ │ ├── payment.test.ts
│ │ └── availability.test.ts
│ ├── e2e/ # End-to-end (Playwright)
│ │ ├── booking.spec.ts
│ │ ├── dashboard.spec.ts
│ │ └── settings.spec.ts
│ └── fixtures/ # Test data, mocks
├── drizzle/
│ ├── schema.ts # Drizzle schema definition
│ └── migrations/ # SQL migrations (auto-generated)
├── scripts/
│ ├── seed.ts # Seed test data
│ ├── setup.ts # First-time setup (create tables, etc.)
│ └── migrate.ts # Run migrations
├── .github/
│ └── workflows/
│ ├── ci.yml # Run tests on PR
│ ├── deploy.yml # Deploy on merge to main
│ └── observability.yml # Datadog setup
├── docker-compose.yml # Local dev: PostgreSQL, Redis (if needed)
├── package.json
├── tsconfig.json
├── next.config.js
├── tailwind.config.ts # Styling (TailwindCSS)
├── postcss.config.js
└── vitest.config.ts # Test runner config
```
---
## Database Schema Overview (First 5 Tables)
```sql
-- Artists
CREATE TABLE artists (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
handle VARCHAR(50) NOT NULL UNIQUE,
avatar_url TEXT,
bio TEXT,
location VARCHAR(100),
instagram_handle VARCHAR(30),
timezone VARCHAR(50) DEFAULT 'Europe/Berlin',
stripe_connect_id VARCHAR(255),
stripe_link_status VARCHAR(50) DEFAULT 'DISCONNECTED',
deleted_at TIMESTAMP,
created_at TIMESTAMP DEFAULT now(),
updated_at TIMESTAMP DEFAULT now(),
INDEX ON (email),
INDEX ON (handle),
INDEX ON (stripe_connect_id)
);
-- Artist Settings
CREATE TABLE artist_settings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
artist_id UUID NOT NULL REFERENCES artists(id) ON DELETE CASCADE,
session_duration_minutes INT DEFAULT 60,
deposit_percentage INT DEFAULT 50,
cancellation_forfeit_hours INT DEFAULT 24,
email_reminders_enabled BOOL DEFAULT true,
email_reminder_hours INT[] DEFAULT '{48,2}',
sms_reminders_enabled BOOL DEFAULT false,
sms_reminder_hours INT[] DEFAULT '{48,2}',
sms_phone_number VARCHAR(20),
booking_page_title VARCHAR(100) DEFAULT 'Book Your Session',
accent_color VARCHAR(7) DEFAULT '#FF6B6B',
cancellation_policy TEXT,
ical_sync_url TEXT,
ical_sync_mode VARCHAR(50),
ical_last_sync_at TIMESTAMP,
created_at TIMESTAMP DEFAULT now(),
updated_at TIMESTAMP DEFAULT now(),
UNIQUE(artist_id),
INDEX ON (artist_id)
);
-- Availability Slots
CREATE TABLE availability_slots (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
artist_id UUID NOT NULL REFERENCES artists(id) ON DELETE CASCADE,
date DATE NOT NULL,
start_time TIME NOT NULL,
end_time TIME NOT NULL,
timezone VARCHAR(50) NOT NULL,
available BOOL DEFAULT true,
created_at TIMESTAMP DEFAULT now(),
updated_at TIMESTAMP DEFAULT now(),
INDEX ON (artist_id, date),
UNIQUE(artist_id, date, start_time, end_time)
);
-- Bookings
CREATE TABLE bookings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
artist_id UUID NOT NULL REFERENCES artists(id),
slot_id UUID NOT NULL REFERENCES availability_slots(id),
client_email VARCHAR(255) NOT NULL,
client_name VARCHAR(100) NOT NULL,
client_phone VARCHAR(20) NOT NULL,
client_notes TEXT,
session_date DATE NOT NULL,
session_time TIME NOT NULL,
session_duration_minutes INT NOT NULL,
deposit_amount_cents INT NOT NULL,
deposit_percentage INT NOT NULL,
currency VARCHAR(3) DEFAULT 'EUR',
status VARCHAR(50) DEFAULT 'PENDING_PAYMENT',
stripe_payment_intent_id VARCHAR(255),
stripe_charge_id VARCHAR(255),
transacted_at TIMESTAMP,
paid_at TIMESTAMP,
sms_reminders_enabled BOOL DEFAULT false,
marketing_consent BOOL DEFAULT false,
created_at TIMESTAMP DEFAULT now(),
updated_at TIMESTAMP DEFAULT now(),
deleted_at TIMESTAMP,
INDEX ON (artist_id, session_date),
INDEX ON (client_email),
INDEX ON (status),
INDEX ON (stripe_payment_intent_id)
);
-- Events (Audit Log)
CREATE TABLE events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
event_name VARCHAR(255) NOT NULL,
actor_id UUID,
actor_type VARCHAR(50),
resource_id UUID,
resource_type VARCHAR(50),
payload JSONB,
metadata JSONB,
created_at TIMESTAMP DEFAULT now(),
INDEX ON (event_name, created_at),
INDEX ON (actor_id, created_at),
INDEX ON (resource_id)
);
```
---
## API Readiness Checklist
Before `main` → production:
- [ ] All endpoints have rate limits defined in code + API docs
- [ ] All POST/PUT endpoints validate input via Zod schema
- [ ] All error responses follow `{ code, message, details }` format
- [ ] All authenticated endpoints check session; return 401 if missing
- [ ] All resource endpoints check ownership; return 403 if denied
- [ ] All external API failures logged; fallback documented
- [ ] Idempotency keys implemented for non-idempotent actions
- [ ] Stripe webhook signature verification in place
- [ ] SendGrid bounce/complaint handling (v1: log, v2: disable email)
- [ ] All API routes have OpenAPI/Swagger docs generated
---
## Observability Checklist
Before shipping:
- [ ] Structured JSON logging on every API request (request ID, duration, error)
- [ ] Structured JSON logging on every database query (slow query threshold: >1s)
- [ ] Structured JSON logging on every external API call (provider, method, duration, status)
- [ ] Event emission on every business action (booking.created, payment.succeeded, etc.)
- [ ] Metrics collected: request latency p50/p95/p99, error rate, payment success rate, reminder delivery rate
- [ ] Alerts configured: >5% error rate, >1% payment failure, Stripe webhook lag >1h
- [ ] Traces sampled: 10% of requests in dev, 1% in production
- [ ] Error boundary on all routes; unhandled errors logged with session ID for debugging
---
## Deployment Checklist
Before `main` → staging:
- [ ] All secrets stored in GCP Secret Manager (not .env.local)
- [ ] Database migrations tested against production schema
- [ ] Database backups automated (daily, 30-day retention)
- [ ] All environment variables documented (.env.local.example)
- [ ] Stripe test mode verified (sandbox keys in dev, live keys in prod, validated)
- [ ] SendGrid sandbox mode verified
- [ ] CORS headers correct (allow public booking page origin, restrict API)
- [ ] CSP (Content Security Policy) headers correct
- [ ] HSTS headers enabled for HTTPS
- [ ] Rate limiting middleware in place on all endpoints
- [ ] Database connection pooling configured (max 20 connections)
---
## Getting Started (Developer Day 1)
1. Clone repo
2. `cp .env.local.example .env.local`
3. `docker-compose up` (starts PostgreSQL, Redis)
4. `npm install`
5. `npm run migrate` (runs Drizzle migrations)
6. `npm run seed` (loads test data)
7. `npm run dev` (starts Next.js dev server on http://localhost:3000)
8. Open http://localhost:3000/login, test magic link flow
---
## CTO Handoff Notes
- **Day 1–7:** Implement auth (magic links, session) + landing page
- **Week 2–3:** Public booking page (calendar, deposit calculation, Stripe integration)
- **Week 4–5:** Artist dashboard, availability management, settings
- **Week 6–7:** Reminders (email + SMS), reschedule, cancellation
- **Week 8:** Testing, observability, deployment pipeline
- **Month 3:** Beta launch with 15 artists, gather feedback, iterate
The spec is complete and non-negotiable. If you discover a contradiction during implementation, document the issue in `docs/decisions.md` and ask the founder to decide, don't guess.
Good luck. This is a focused product. Ship it.
```