- Turborepo + pnpm workspaces with apps/web and packages/types - SvelteKit app: Tailwind, Paraglide i18n (en/es), keycloak-js, Supabase client, auth/collective stores, PWA service worker skeleton, all route placeholders - packages/types: domain types (User, Collective, ShoppingList, etc.) and database.ts placeholder for generated types - Justfile with dev, db-*, kc-*, build, backup, restore, deploy recipes - infra/docker-compose.dev.yml: Postgres 15, GoTrue, PostgREST, Realtime v2.83, Storage, Kong (port 8001), Studio, Keycloak 24 - infra/docker-compose.prod.yml, kong.yml, db-init scripts, backup/deploy scripts - keycloak/realm-export.json with colectivo realm and 5 dev test users - supabase/config.toml and seed.sql with sample collective and items - GitHub Actions: ci.yml (lint+typecheck+build) and deploy.yml (GHCR + SSH) - .env.example documenting all required variables - Fixed docker-compose issues: Studio image tag, Kong port conflict (8001), internal role passwords init script, Realtime METRICS_JWT_SECRET/APP_NAME Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
8.0 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Code Language
All code must be written in English. This applies to variable names, function names, type names, database column names, SQL identifiers, comments, and any other code artifact. The UI may display text in any language, but the code layer is always English.
Project Status
This repository is in the planning phase. No application code exists yet. The repo contains:
README.md— full development plan, confirmed tech stack, Justfile reference, and technical warningsanalysis/analisis-funcional.md— complete functional specification (domain model, use cases, data models, business rules)plan/fase-*.md— phase-by-phase implementation plans (Fase 0 through Fase 4)
Start by reading these documents before implementing anything.
Planned Stack
| Layer | Technology |
|---|---|
| Frontend / PWA | SvelteKit 2 + @vite-pwa/sveltekit + Workbox |
| Auth / IdP | Keycloak self-hosted (OIDC/OAuth2) |
| Backend / BaaS | Supabase self-hosted (GoTrue + Realtime + Storage + Kong) |
| Database | PostgreSQL 15 (via Supabase) |
| Offline storage | IndexedDB via idb |
| Monorepo | Turborepo + pnpm workspaces |
| Infra | Docker Compose (dev + prod) + nginx (prod, managed externally) |
| CI/CD | GitHub Actions |
Planned Repository Layout
apps/web/src/lib/
supabase.ts # Supabase singleton client
auth.ts # keycloak-js OIDC client + session helpers
stores/ # Svelte global stores
sync/ # offline queue + conflict resolution
components/ # reusable UI components
apps/web/src/routes/ # SvelteKit file-based routes
packages/types/src/
database.ts # types generated from Supabase schema (never edit manually)
domain.ts # domain types (Collective, ShoppingList, Item, etc.)
supabase/
migrations/ # versioned SQL migrations
seed.sql # dev test data (5 Keycloak users + sample collective)
functions/ # Edge Functions (invitations, etc.)
config.toml # Supabase CLI config + Keycloak OIDC
keycloak/
realm-export.json # "colectivo" realm — imported on dev startup
infra/
docker-compose.dev.yml
docker-compose.prod.yml
scripts/backup.sh / backup-cron.sh / restore.sh / deploy.sh
Development Commands (Justfile)
just dev # start docker-compose.dev.yml + SvelteKit dev server
just dev-stop # stop local environment
just db-reset # drop + migrate + seed (dev)
just db-migrate # apply pending migrations (dev)
just db-seed # apply supabase/seed.sql to the running dev db
just db-types # regenerate TS types from Supabase schema → packages/types
just kc-export # export Keycloak realm → keycloak/realm-export.json
just kc-open # open Keycloak Admin Console (localhost:8080)
just deploy # run infra/scripts/deploy.sh (prod)
just backup supabase # immediate manual backup
just restore supabase <file> # restore a specific dump
just logs # docker compose logs -f (prod)
Local Dev Endpoints
| Service | URL | Notes |
|---|---|---|
| SvelteKit app | http://localhost:5173 | |
| Supabase Studio | http://localhost:54323 | |
| Supabase API (Kong) | http://localhost:8001 | Port 8000 reserved by other services |
| Keycloak Admin | http://localhost:8080/admin | admin / admin (dev only) |
| PostgreSQL | localhost:5432 | postgres / postgres (dev only) |
Domain Model
The central organizing unit is the Collective (household group), not the individual user. All content belongs to the Collective.
Roles: admin (full CRUD + member management) | member (full CRUD on content) | guest (read-only)
Core entities: Collective → CollectiveMember, ShoppingList → ShoppingItem, TaskList → Task, Note, CollectiveInvitation
Key field names (English): collective_id, list_id, is_checked, checked_by, checked_at, created_by, created_at, completed_at, sort_order, display_name, avatar_type, avatar_emoji, avatar_url, language
Key business rules:
- Only one active shopping session per list at a time.
- Completing a list keeps items as history; it can be "reset" (uncheck all → back to active).
- Trash retains deleted items/notes for 7 days before permanent deletion.
- If the sole admin deletes their account, the system auto-promotes the oldest member before allowing deletion.
Auth Architecture (Keycloak → Supabase)
Auth flows through Keycloak as the OIDC IdP. Supabase validates Keycloak-issued JWTs. Supabase RLS uses auth.uid() which reads the sub claim from the JWT (must be a UUID v4 — Keycloak default).
The Keycloak RS256 public key must be set as JWT_SECRET in Supabase. Rotating Keycloak signing keys requires updating Supabase JWT_SECRET simultaneously.
Logout must invalidate both sides: call keycloak.logout() (invalidates Keycloak session) AND clear the Supabase client locally.
Sync Strategy
| Content | Mechanism | Target latency |
|---|---|---|
| Items in active shopping session | WebSocket (Supabase Realtime) | < 1 second |
| Lists, tasks (outside session) | SSE / polling | < 5 seconds |
| Notes | Polling | < 30 seconds |
Offline-first: all operations execute locally first (IndexedDB), then sync in background. Conflict resolution is last-write-wins. Conflicts are logged to a sync_conflicts table for debugging — no CRDT in MVP.
Critical Platform Gotchas
iOS Safari / PWA:
keycloak-jssilent token refresh uses iframes (checkLoginIframe) — Safari blocks third-party cookies in iframes. SetcheckLoginIframe: falseand useupdateToken()manually instead.- Background Sync API is not supported in Safari. Implement a manual fallback using the
onlineevent. - Push notifications require iOS 16.4+ and the PWA must be installed to the home screen.
- Test the shopping session mode on a real iPhone during development, not only in DevTools.
nginx (prod) — WebSocket for Realtime:
- The
/realtime/location block must come before the/block. proxy_read_timeout 3600sis required on the Realtime block — without it, nginx closes WebSocket connections after 60 seconds, forcing continuous reconnects during active shopping sessions.- CSP
connect-srcmust include bothhttps://andwss://for the API domain.
Keycloak behind nginx:
X-Forwarded-HostandX-Forwarded-Portheaders are mandatory. Without them, Keycloak builds redirect URIs with the internal port (8080) instead of 443, breaking the OIDC flow.
Supabase Realtime self-hosted:
- Check
MAX_REPLICATION_SLOTSin PostgreSQL andREALTIME_MAX_CONNECTIONSbefore going to production. - Presence subscriptions are more resource-intensive than Postgres Changes — limit them to lists with an active session.
Dev Test Users
Defined in keycloak/realm-export.json and supabase/seed.sql. All use password test1234.
| User | Role in test collective | |
|---|---|---|
ana |
ana@dev.local | Admin |
borja |
borja@dev.local | Member |
carmen |
carmen@dev.local | Member |
david |
david@dev.local | Guest |
eva |
eva@dev.local | (no collective — for onboarding testing) |
Internationalisation
Library: Paraglide JS (@inlang/paraglide-sveltekit). Compile-time, zero runtime overhead, tree-shaken per language.
- Message files:
messages/en.json(default) andmessages/es.json - Language detection order:
users.language(if logged in) →Accept-Languageheader →en - Language switching: update
users.languagein Supabase + callsetLanguageTag()— no page reload - Never hardcode visible user-facing strings in components. All UI text goes through Paraglide messages.
Supported languages: en, es.
TypeScript Types
packages/types/src/database.ts is generated by just db-types (supabase gen types typescript). Never edit it manually. Domain-level types go in packages/types/src/domain.ts.