- SQL migrations: users, collectives, collective_members, collective_invitations,
full RLS policies, is_active_member/is_member/is_admin helpers,
accept_invitation SECURITY DEFINER function, admin auto-promote trigger,
avatars storage bucket
- Auth refactor: replace keycloak-js with Supabase OAuth (GoTrue OIDC proxy,
Option B). signInWithOAuth({ provider: 'keycloak' }) + PKCE flow
- GoTrue config: GOTRUE_EXTERNAL_KEYCLOAK_URL + GOTRUE_EXTERNAL_KEYCLOAK_REDIRECT_URI
added to docker-compose.dev.yml; Keycloak realm updated with GoTrue callback URI
- New routes: /auth/callback, /invitation/[token], /(app)/collective/manage
- Functional onboarding: create collective or join via invite link
- Full settings page: display name (debounced), avatar (initials/emoji/upload),
language switcher, Keycloak account console link
- Components: Avatar.svelte (initials/emoji/upload with fallback),
ImageCropper.svelte (cropperjs, 1:1 crop, lazy-loaded)
- i18n: added all Fase 1 message keys (en + es); renamed 'delete' → 'action_delete'
(JS reserved word); fixed plugin-m-function-matcher major-version URL format
- Database types: manually seeded packages/types/src/database.ts from migrations
- .env.development: committed public dev defaults for SvelteKit type checking
- CLAUDE.md: documented /etc/hosts requirement for keycloak hostname
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
8.5 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
Fase 0 complete. Fase 1 in progress.
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)
Local Dev: Required /etc/hosts Entry
GoTrue (Supabase Auth) and the browser both must resolve keycloak to reach the Keycloak container. Add this line to /etc/hosts on the development machine:
127.0.0.1 keycloak
Without this, the OAuth redirect URL (http://keycloak:8080/...) built by Keycloak's discovery document is unreachable from the browser.
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 → GoTrue → Supabase)
Strategy: Option B — GoTrue as OIDC proxy.
The app authenticates with Keycloak (PKCE), then exchanges the Keycloak token with GoTrue for a Supabase JWT. Supabase RLS uses auth.uid() which resolves to the GoTrue user UUID (mapped from Keycloak sub). GoTrue maintains auth.users.
Key decisions:
- Self-registration enabled in Keycloak — users can create accounts on the Keycloak login screen.
- Invitations are link-only — no email sent. Admin generates a link and shares it manually.
- Multiple collectives per user — a user can belong to several collectives and switch between them in the sidebar.
Logout must invalidate both sides: call keycloak.logout() (invalidates Keycloak session) AND clear the Supabase client session 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.