# Fase 1 — what has been built

- `supabase/migrations/001_users.sql` — `users` table, `handle_new_user` trigger
- `supabase/migrations/002_collectives.sql` — `collectives`, `collective_members`, `collective_invitations`; auto-promote-oldest-admin trigger
- `supabase/migrations/003_rls.sql` — full RLS policies, `is_active_member`/`is_member`/`is_admin` helpers, `accept_invitation()` function
- `supabase/migrations/004_storage.sql` — `avatars` bucket (private, signed URLs)
- `apps/web/src/lib/supabase.ts` — Supabase singleton with PKCE flow
- `apps/web/src/lib/auth.ts` — `login()` / `logout()` via `supabase.auth.signInWithOAuth`
- `apps/web/src/lib/stores/auth.ts` — `currentUser`, `authLoading`, `isAuthenticated`, `displayName`
- `apps/web/src/lib/stores/collective.ts` — `currentCollective`, `userCollectives`, `collectiveMembers`
- `apps/web/src/lib/components/Avatar.svelte` — initials / emoji / upload with deterministic colour hash
- `apps/web/src/lib/components/ImageCropper.svelte` — lazy-loaded cropperjs, 1:1, 256×256 WebP output
- `apps/web/src/routes/+layout.svelte` — auth state listener, collective bootstrap
- `apps/web/src/routes/(app)/+layout.ts` — SSR disabled (`ssr: false`); no auth check here (see gotcha below)
- `apps/web/src/routes/(app)/+layout.svelte` — sidebar with collective switcher; auth redirect via `$effect`
- `apps/web/src/routes/auth/callback/+page.svelte` — PKCE code exchange
- `apps/web/src/routes/onboarding/+page.svelte` — create collective or join via link
- `apps/web/src/routes/(app)/collective/manage/+page.svelte` — members, roles, invite link generator
- `apps/web/src/routes/invitation/[token]/+page.svelte` — accept invitation (auth-aware)
- `apps/web/src/routes/(app)/settings/+page.svelte` — display name, avatar, language, sign out
- `apps/web/messages/en.json` + `es.json` — all Fase 1 + 2a strings
- `packages/types/src/database.ts` — manually seeded from migrations (update with `just db-types` once CLI is wired)
- `setup.sh` — idempotent local dev setup (prerequisites, .env, Docker, migrations, seed, Paraglide)
- `infra/kong-start.sh` — Kong container entrypoint; substitutes `${VAR}` placeholders in `kong.yml` before Kong starts (Kong 2.8 does not do this natively)

## Test suite — initial scaffolding

- `packages/test-utils/` — shared test infrastructure (Vitest integration tests)
  - `package.json` — `@colectivo/test-utils`; depends on `@supabase/supabase-js`, `jose`, `pg`
  - `vitest.config.ts` — loads root `.env` via `loadEnv`; single-fork mode (no parallelism races)
  - `src/seed-constants.ts` — fixed UUIDs for all 5 seed users + collective + seed list
  - `src/supabase-clients.ts` — `createClientAs(userId)` signs HS256 JWT with `SUPABASE_JWT_SECRET`; `createAdminClient()` uses service role key
  - `src/db-helpers.ts` — `sql()` via raw `pg` Pool for direct DB manipulation (bypasses RLS); used to set `deleted_at = 8 days ago` etc.
  - `tests/rls-isolation.test.ts` — F-series: Eva (non-member) sees zero rows, all writes blocked
  - `tests/rls-collective.test.ts` — B-series: collective read/update/invite by role
  - `tests/rls-lists.test.ts` — C-series: list CRUD, soft-delete, trash window, cross-collective isolation
  - `tests/rls-items.test.ts` — D-series: item CRUD by role, cross-collective item isolation
  - `tests/rls-frequency.test.ts` — E-series: frequency read-only + trigger populates on INSERT
- `supabase/tests/*.sql` each start with `CREATE EXTENSION IF NOT EXISTS pgtap;` so they're idempotent on a fresh DB
  - `001_accept_invitation.sql` — `accept_invitation()` happy path + not_found / expired / already_used; uses `set_config('request.jwt.claim.sub', …)` to set `auth.uid()` for the test session, and looks up by `token` (not `id`)
  - `002_item_frequency_trigger.sql` — trigger normalizes name with `lower(trim(...))` and increments `use_count` on repeat INSERTs. RLS write-blocks are covered by the Vitest suite instead (postgres superuser bypasses RLS here)
  - `003_promote_on_admin_leave.sql` — deletes a synthetic admin from `public.users` to fire the `BEFORE DELETE` trigger and asserts the oldest remaining member is promoted
- `apps/web/playwright.config.ts` — Playwright config; `globalSetup` is a Keycloak health check (not a session cache), `reuseExistingServer: true`
- `apps/web/tests/fixtures/users.ts` — seed user constants (Ana, Borja, Carmen, David, Eva)
- `apps/web/tests/fixtures/login.ts` — `loginAs(page, user)` runs the real OAuth flow (Keycloak → GoTrue PKCE) and waits for the session token to land in localStorage. Each test that needs auth calls this in `beforeEach`; cached `storageState` is NOT used (see the Playwright storageState gotcha in `CLAUDE.md`)
- `apps/web/tests/e2e/auth.test.ts` — A-series: unauthenticated redirect, full login, collective visible in sidebar, sign-out, guest login
- `apps/web/tests/e2e/lists.test.ts` — C-series: create (with reload-persistence check), soft-delete → appears in trash drawer, archive, guest read-only
- `apps/web/tests/e2e/items.test.ts` — D-series: seed list renders, add item (with reload-persistence check), toggle check, desktop-hover delete, frequency suggestions, guest read-only
