Skip to content
Web SaaS

Tattoo studio booking with deposits and aftercare

Solo artists collect deposits, schedule sessions, and follow up — without a salon-receptionist tool.

Generate your own scaffold
Files
52
Generator
Anthropic — Claude Haiku 4.5

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.
```