docs: Update architecture for supabase
test: Add tests for auth workflow
This commit is contained in:
190
docs/context/sdlc-architecture.md
Normal file
190
docs/context/sdlc-architecture.md
Normal file
@@ -0,0 +1,190 @@
|
||||
# SDLC Architecture — oysqn.app
|
||||
|
||||
**Date:** 2026-04-12
|
||||
**Status:** Decided — no open items
|
||||
|
||||
## Project Topology
|
||||
|
||||
| Repo | Purpose | Status |
|
||||
|------|---------|--------|
|
||||
| `oysqn.app` | Nuxt PWA + Supabase schema/migrations + all tests | Active |
|
||||
| `bab-backend-ansible` | Infra provisioning, deployment orchestration, day2 ops | Needs rewrite (Appwrite → Supabase) |
|
||||
|
||||
## Lifecycle Phases
|
||||
|
||||
1. **Local Dev** — `yarn dev` + `npx supabase start` (Podman)
|
||||
2. **Dev server** — `bab1.mgmt.toal.ca` — static site via nginx; backend = supabase.com
|
||||
3. **Production** — static site on AWS S3; backend = supabase.com
|
||||
|
||||
## Backend Hosting
|
||||
|
||||
**supabase.com** (free tier initially; may self-host if free plan limits are exceeded).
|
||||
- supabase.com provides direct Postgres access (connection string) on all tiers including free.
|
||||
- Backups: `pg_dump` via Postgres connection string → compressed → stored to `bab1.mgmt.toal.ca` via SSH.
|
||||
- Migrations: `supabase db push` against remote project (Supabase CLI).
|
||||
- Rollback strategy: pre-migration `pg_dump` backup + rollback SQL scripts (see Down-Migration Convention).
|
||||
|
||||
## Supabase Projects
|
||||
|
||||
Two separate supabase.com projects — isolated credentials and migration state:
|
||||
|
||||
| Project | Purpose |
|
||||
|---------|---------|
|
||||
| `oysqn-dev` | Development + staging |
|
||||
| `oysqn-prod` | Production |
|
||||
|
||||
Migrations promoted dev → prod only after E2E validation on dev.
|
||||
|
||||
## Down-Migration Convention
|
||||
|
||||
Supabase CLI only runs forward migrations. Rollback scripts are separate files executed by AAP on failure.
|
||||
|
||||
```
|
||||
supabase/
|
||||
migrations/
|
||||
20260325000000_initial_schema.sql ← forward (applied by supabase db push)
|
||||
20260412120000_add_boats_table.sql
|
||||
rollback/
|
||||
20260412120000_add_boats_table.sql ← reverse SQL, same filename, separate dir
|
||||
```
|
||||
|
||||
Rules:
|
||||
- Every forward migration **must** have a corresponding rollback file before the PR merges
|
||||
- Rollback files are plain SQL executed by AAP via `psql` on rollback
|
||||
- If a migration is irreversible (e.g., data-destroying DROP), document this explicitly at the top of the rollback file — AAP alerts and halts rather than executing
|
||||
|
||||
## Secrets Management
|
||||
|
||||
**All secrets in HashiCorp Vault:** `http://nas.lan.toal.ca:8200` — KV path prefix: `kv/oys/`
|
||||
|
||||
Format: `kv/oys/(dev|prod|shared)/(supabase|app|infra)/<secretname>`
|
||||
|
||||
| Secret | Vault path | Consumers |
|
||||
|--------|-----------|-----------|
|
||||
| Supabase dev API URL | `kv/oys/dev/supabase/url` | Gitea Actions (ENV_FILE), AAP |
|
||||
| Supabase dev anon key | `kv/oys/dev/supabase/anon_key` | Gitea Actions (ENV_FILE), AAP |
|
||||
| Supabase dev service role key | `kv/oys/dev/supabase/service_role_key` | AAP (migrations) |
|
||||
| Supabase prod API URL | `kv/oys/prod/supabase/url` | Gitea Actions (ENV_FILE), AAP |
|
||||
| Supabase prod anon key | `kv/oys/prod/supabase/anon_key` | Gitea Actions (ENV_FILE), AAP |
|
||||
| Supabase prod service role key | `kv/oys/prod/supabase/service_role_key` | AAP (migrations, pg_dump) |
|
||||
| Supabase prod Postgres conn string | `kv/oys/prod/supabase/postgres_url` | AAP (pg_dump) |
|
||||
| AWS access key ID | `kv/oys/prod/app/aws_access_key_id` | AAP (S3 deploy) |
|
||||
| AWS secret access key | `kv/oys/prod/app/aws_secret_access_key` | AAP (S3 deploy) |
|
||||
| AWS S3 bucket name | `kv/oys/prod/app/aws_s3_bucket` | AAP (S3 deploy) |
|
||||
| SSH private key (bab1) | `kv/oys/shared/infra/ssh_private_key` | AAP (backup, nginx deploy) |
|
||||
| Gitea API token | `kv/oys/shared/infra/gitea_token` | AAP (fetch artifacts, sync secrets) |
|
||||
|
||||
**Local dev:** Secrets in `.env` (git-ignored). Do not put real values in `.env.example`.
|
||||
**AAP:** Vault lookup plugin as a credential type.
|
||||
**Gitea Actions:** Variable `ENV_FILE` (per branch) populated by AAP sync playbook (see below).
|
||||
|
||||
### Gitea Actions Secret Injection
|
||||
|
||||
Follows the same `ENV_FILE` pattern as `bab-app`. AAP runs a `sync-gitea-secrets` playbook:
|
||||
- **Trigger:** Scheduled daily + on-demand job template
|
||||
- **Action:** Reads `url` + `anon_key` from Vault, constructs `.env` content, updates Gitea repo variable via API (`PUT /api/v1/repos/{owner}/{repo}/actions/variables/ENV_FILE_DEV` and `ENV_FILE_PROD`)
|
||||
- **In workflow:** `echo "${{ vars.ENV_FILE_DEV }}" > .env` (dev branch) / `ENV_FILE_PROD` (main branch)
|
||||
|
||||
## CI/CD Toolchain
|
||||
|
||||
- **SCM:** Gitea
|
||||
- **CI:** Gitea Actions — unit tests + build + semantic-release → Gitea Release artifact
|
||||
- **CD + ops:** Ansible + EDA — triggered by Gitea webhook; backup, migrate, deploy, smoke test, rollback
|
||||
- **Branch strategy:** `dev` → dev server; `main` → production (manual approval gate in AAP)
|
||||
|
||||
### Pipeline Architecture
|
||||
|
||||
```
|
||||
Gitea push (dev or main)
|
||||
│
|
||||
▼
|
||||
Gitea Actions (.gitea/workflows/build.yaml)
|
||||
├── yarn test (unit tests — no external deps)
|
||||
├── echo $ENV_FILE_DEV > .env (or ENV_FILE_PROD for main)
|
||||
├── yarn semantic-release (bumps version, builds tarball, publishes Gitea Release)
|
||||
│ └── prepareCmd: yarn generate → tar release-<version>.tar.gz
|
||||
│ └── publishCmd: attaches tarball to Gitea Release, sets VERSION output
|
||||
└── webhook → EDA (artifact_url, branch, version)
|
||||
│
|
||||
▼
|
||||
EDA rulebook receives webhook
|
||||
│
|
||||
▼
|
||||
AAP workflow template
|
||||
├── pre-deploy: pg_dump → bab1.mgmt.toal.ca (pre-migration snapshot)
|
||||
├── migrate: supabase db push → if fails, run rollback SQL + abort
|
||||
├── deploy: fetch artifact → S3 sync (prod) or nginx swap (dev)
|
||||
├── post-deploy: yarn test:e2e BASE_URL=<deployed-url>
|
||||
├── on failure: psql rollback script, redeploy previous artifact, notify
|
||||
└── on success: notify
|
||||
```
|
||||
|
||||
### Artifact Pattern (Matches bab-app)
|
||||
|
||||
- `semantic-release` + `@saithodev/semantic-release-gitea`
|
||||
- Tarball: `release-<version>.tar.gz` of `.output/public/`
|
||||
- Attached to Gitea Release
|
||||
- Webhook payload: `{ "artifact_url": "...", "version": "...", "branch": "..." }`
|
||||
|
||||
## Backup Policy
|
||||
|
||||
**Scope:** Production only. Dev database is ephemeral — no backups.
|
||||
**Location:** `bab1.mgmt.toal.ca:/var/backups/oysqn/` (confirm path before first production backup)
|
||||
|
||||
| Type | Retention | Max count |
|
||||
|------|-----------|-----------|
|
||||
| Regular (daily + pre-migration) | 90 days | 30 |
|
||||
| Monthly | 12 months | 12 |
|
||||
|
||||
Monthly backups taken on the 1st of each month. AAP rotation playbook enforces limits after each backup run.
|
||||
|
||||
Filename convention:
|
||||
- Regular: `oysqn-prod-<YYYYMMDD-HHMMSS>.sql.gz`
|
||||
- Monthly: `oysqn-prod-<YYYY-MM>-monthly.sql.gz`
|
||||
|
||||
## Test Strategy
|
||||
|
||||
### Test Tiers
|
||||
|
||||
| Tier | Tool | Runs in | Requires |
|
||||
|------|------|---------|---------|
|
||||
| Unit | Vitest | Gitea Actions + local | Nothing |
|
||||
| Integration | Vitest (node) | Local only | Local Supabase + `SUPABASE_SERVICE_ROLE_KEY` |
|
||||
| E2E | Playwright | Local + AAP post-deploy | Running app + Supabase |
|
||||
|
||||
### Unit Test Scope
|
||||
|
||||
- **Test:** pure business logic, auth middleware, Pinia store actions, utility functions
|
||||
- **Do NOT unit test:** Vue components that primarily compose Ionic/PrimeVue — E2E covers these
|
||||
- **Reason:** Mocking Nuxt auto-imports (`#imports`) creates brittle tests that test mocks, not code
|
||||
|
||||
### Integration Test Scope
|
||||
|
||||
- Supabase RLS policy correctness (one suite per role)
|
||||
- Auth flows and session creation
|
||||
- Run locally: `SUPABASE_SERVICE_ROLE_KEY=<key> yarn test:integration`
|
||||
|
||||
### E2E Test Strategy
|
||||
|
||||
- **Lives in:** `oysqn.app/tests/e2e/` — versioned with app code, tool: Playwright
|
||||
- **Parameterized by:** `BASE_URL` env var
|
||||
- **Local:** `BASE_URL=http://localhost:3000 yarn test:e2e`
|
||||
- **Post-deploy:** AAP calls `yarn test:e2e` with deployed URL
|
||||
- **Not in Gitea CI** — runner has no Docker/Podman for local Supabase
|
||||
|
||||
## bab-backend-ansible Rewrite Scope
|
||||
|
||||
New responsibilities (all Appwrite playbooks retired):
|
||||
1. **Infra provisioning** — dev server nginx setup, cert management, monitoring
|
||||
2. **Supabase migrations** — `supabase db push` against supabase.com; rollback on failure
|
||||
3. **Backup** — prod only; scheduled daily + pre-migration `pg_dump` → `bab1.mgmt.toal.ca`; rotation enforcing retention policy
|
||||
4. **Frontend deployment** — S3 sync (prod), nginx artifact swap (dev)
|
||||
5. **Day2 ops** — cert renewal, log rotation, health checks
|
||||
6. **Secret sync** — `sync-gitea-secrets` playbook populates Gitea `ENV_FILE_DEV` / `ENV_FILE_PROD` variables from Vault
|
||||
7. **EDA rulebooks** — Gitea push webhook → trigger AAP workflow template
|
||||
|
||||
## Assumptions
|
||||
|
||||
- `main` branch → production requires manual approval gate in AAP before deploy
|
||||
- Gitea Actions runner is `ubuntu-latest` (same as bab-app)
|
||||
- Backup path on bab1.mgmt.toal.ca: `/var/backups/oysqn/` (confirm before first production backup)
|
||||
78
docs/summaries/handoff-2026-04-12-splash-and-login.md
Normal file
78
docs/summaries/handoff-2026-04-12-splash-and-login.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Session Handoff: Splash Page & Magic Link Login
|
||||
**Date:** 2026-04-12
|
||||
**Session Duration:** ~30 minutes
|
||||
**Session Focus:** Implement unauthenticated splash page with logo and login button; implement magic link login page
|
||||
**Context Usage at Handoff:** Low
|
||||
|
||||
## What Was Accomplished
|
||||
|
||||
1. **Splash page implemented** → `app/pages/index.vue` — unauthenticated users see centered logo + "Log In" button; authenticated users see home content with header/menu
|
||||
2. **Login page implemented** → `app/pages/login.vue` — email input, sends magic link via `auth.sendMagicLink()`, shows confirmation state with mail icon after send
|
||||
3. **Auth middleware updated** → `app/middleware/auth.ts` — added `/` to public routes; unauthenticated users redirected to `/` (not `/login`)
|
||||
4. **Supabase redirect config updated** → `nuxt.config.ts` — `login` redirect changed from `/login` to `/`; `/` added to `exclude` list
|
||||
|
||||
## Exact State of Work in Progress
|
||||
|
||||
- Splash + login pages coded but not yet tested end-to-end against local Supabase
|
||||
- `auth/callback.vue` still a skeleton — magic link redirect will land there; not yet implemented
|
||||
|
||||
## Decisions Made This Session
|
||||
|
||||
- USE `/` as the unauthenticated landing route INSTEAD OF `/login` BECAUSE the splash/logo page IS the unauthenticated entry point; `/login` is a detail page reached from it — STATUS: confirmed
|
||||
- MAGIC LINK ONLY on login page (not OTP) BECAUSE user stated "sends a magic link" — STATUS: confirmed; OTP flow (`sendOtp` + `verifyOtp`) exists in auth store but unused by login page
|
||||
|
||||
## Key Numbers Generated or Discovered This Session
|
||||
|
||||
- Logo file: `public/oysqn_logo.png` (confirmed present)
|
||||
- Max logo display width: 280px (CSS)
|
||||
- Max login form width: 400px (CSS)
|
||||
|
||||
## Files Created or Modified
|
||||
|
||||
| File Path | Action | Description |
|
||||
|-----------|--------|-------------|
|
||||
| `app/pages/index.vue` | Modified | Splash (unauthenticated) + home (authenticated) in single page, toggled by `useSupabaseUser()` |
|
||||
| `app/pages/login.vue` | Modified | Email input → `sendMagicLink()` → confirmation state; uses `IonBackButton` to return to splash |
|
||||
| `app/middleware/auth.ts` | Modified | Added `/` to public routes; redirects unauthenticated to `/` |
|
||||
| `nuxt.config.ts` | Modified | `supabase.redirectOptions.login` = `/`; added `/` to `exclude` |
|
||||
|
||||
## What the NEXT Session Should Do
|
||||
|
||||
1. **First**: Test the auth flow end-to-end: load app → confirm splash shows logo + Login button → tap Login → enter email → check Mailpit (http://127.0.0.1:54324) for magic link → click link → confirm redirect to `/auth/callback` → confirm redirect to `/` (home state)
|
||||
2. **Then**: Implement `app/pages/auth/callback.vue` — handle magic link redirect (Supabase sets session from URL hash; page should show a spinner, then navigate to `/`)
|
||||
3. **Then**: Verify `useAuthStore` is auto-imported by Pinia Nuxt module (currently explicitly imported in `login.vue` with `import { useAuthStore } from '~/stores/auth'` — check if explicit import is still needed or if auto-import covers it)
|
||||
4. **Then**: Run `npx supabase gen types typescript --local > app/types/supabase.ts` to replace placeholder types
|
||||
5. **Then**: Implement `app/pages/index.vue` home content (authenticated state) — boat list or welcome state
|
||||
|
||||
## Open Questions Requiring User Input
|
||||
|
||||
- [ ] Should the login page also support OTP (enter token from email) as a fallback, or magic link only? — `sendOtp` + `verifyOtp` exist in auth store but unused — impacts login page UI
|
||||
- [ ] Should `auth/callback.vue` redirect to a specific page after login (e.g., boats list) or always to `/`? — impacts callback implementation
|
||||
|
||||
## Assumptions That Need Validation
|
||||
|
||||
- ASSUMED: `@nuxtjs/ionic` + `@pinia/nuxt` auto-imports `useAuthStore` — currently explicitly imported; validate by removing explicit import and testing
|
||||
- ASSUMED: `sendMagicLink()` in auth store correctly sets `emailRedirectTo` to `/auth/callback` — validate by clicking the email link and checking where it lands
|
||||
- ASSUMED: `IonRouterOutlet` handles `router-link` on `IonButton` correctly — validate by tapping Login button in browser
|
||||
|
||||
## Files to Load Next Session
|
||||
|
||||
- `app/pages/auth/callback.vue` — primary implementation target
|
||||
- `app/pages/index.vue` — may need home content (authenticated state) built out
|
||||
- `app/stores/auth.ts` — reference for `sendMagicLink` signature and redirect URL
|
||||
|
||||
## Dev Environment Reference
|
||||
|
||||
```
|
||||
# Start local Supabase (if not running):
|
||||
DOCKER_HOST=unix:///run/user/1000/podman/podman.sock npx supabase start
|
||||
|
||||
# Start app:
|
||||
yarn dev
|
||||
|
||||
# Mailpit (view magic link emails):
|
||||
http://127.0.0.1:54324
|
||||
|
||||
# Supabase Studio:
|
||||
http://127.0.0.1:54323
|
||||
```
|
||||
Reference in New Issue
Block a user