From 9c93e7431853fb91ef8afedb2f2828d37ebec99c Mon Sep 17 00:00:00 2001 From: ordinarthur <@arthurbarre.js@gmail.com> Date: Thu, 16 Apr 2026 10:15:34 +0200 Subject: [PATCH] replace Supabase with Postgres + Drizzle ORM - Drop @supabase/supabase-js entirely; add drizzle-orm + postgres (porsager) driver - New packages/db: schema (pgSchema ordinarthur_os), client factory, migrate runner, drizzle-kit config - SQL migrations: 0000_init (pgcrypto + schema), 0001_jobs (jobs + job_search_criteria, no RLS) - Rewrite apps/api db module with DI symbols DB/DB_HANDLE + @InjectDb() decorator - Rewrite jobs.service.ts with Drizzle queries (upsert via onConflictDoUpdate, arrayOverlaps for stack filter) - Replace SUPABASE_* env vars with DATABASE_URL in env config + .env.example - Add docker-compose.yml (Postgres 16-alpine, dev only) - Add deploy/k8s/postgres.yaml (StatefulSet + PVC), migrate.job.yaml, updated secrets.template.yaml - Update all docs (README, PLAN, ARCHITECTURE, CLAUDE.md, AGENTS.md, packages/db/README.md) Co-Authored-By: Claude Opus 4.6 (1M context) --- AGENTS.md | 6 +- ARCHITECTURE.md | 47 +- CLAUDE.md | 6 +- PLAN.md | 12 +- README.md | 2 +- apps/api/.env.example | 7 +- apps/api/package.json | 4 +- apps/api/src/config/env.ts | 8 +- apps/api/src/db/db.module.ts | 36 +- apps/api/src/modules/jobs/jobs.service.ts | 250 +++++--- apps/pwa/src/routes/index.tsx | 2 +- deploy/k8s/README.md | 13 + deploy/k8s/backup.cronjob.yaml | 7 +- deploy/k8s/migrate.job.yaml | 27 + deploy/k8s/postgres.yaml | 66 ++ deploy/k8s/secrets.template.yaml | 37 +- docker-compose.yml | 35 + packages/db/README.md | 51 +- packages/db/drizzle.config.ts | 22 + packages/db/migrations/0000_init.sql | 10 + packages/db/migrations/0001_jobs.sql | 51 ++ packages/db/migrations/0001_schema.sql | 20 - packages/db/migrations/0002_jobs.sql | 54 -- packages/db/migrations/meta/_journal.json | 20 + packages/db/package.json | 22 +- packages/db/src/client.ts | 31 + packages/db/src/index.ts | 3 + packages/db/src/migrate.ts | 29 + packages/db/src/schema/_schema.ts | 8 + packages/db/src/schema/index.ts | 2 + packages/db/src/schema/jobs.ts | 67 ++ packages/db/tsconfig.json | 12 + pnpm-lock.yaml | 746 +++++++++++++++++++--- 33 files changed, 1380 insertions(+), 333 deletions(-) create mode 100644 deploy/k8s/migrate.job.yaml create mode 100644 deploy/k8s/postgres.yaml create mode 100644 docker-compose.yml create mode 100644 packages/db/drizzle.config.ts create mode 100644 packages/db/migrations/0000_init.sql create mode 100644 packages/db/migrations/0001_jobs.sql delete mode 100644 packages/db/migrations/0001_schema.sql delete mode 100644 packages/db/migrations/0002_jobs.sql create mode 100644 packages/db/migrations/meta/_journal.json create mode 100644 packages/db/src/client.ts create mode 100644 packages/db/src/index.ts create mode 100644 packages/db/src/migrate.ts create mode 100644 packages/db/src/schema/_schema.ts create mode 100644 packages/db/src/schema/index.ts create mode 100644 packages/db/src/schema/jobs.ts create mode 100644 packages/db/tsconfig.json diff --git a/AGENTS.md b/AGENTS.md index 9dcfff0..a7be4e0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,7 +13,7 @@ Avant toute action, lire dans cet ordre : - **Single-user.** Bearer token statique, pas de multi-tenant. - **Design = portfolio arthurbarre.fr** (cream `#F5F1EA`, ink `#0F0F0F`, accent orange `#FF4A1C`, mono labels, bordures, italique = orange). PAS le violet/cyan du HTML jobs. - **FR only** côté IA (prompts + STT lang=fr). -- **Schéma Postgres dédié `ordinarthur_os`** dans Supabase self-hosted (`supabase.arthurbarre.fr`). +- **Postgres standalone dans le k3s** (plus de Supabase), schéma dédié `ordinarthur_os`. ORM = **Drizzle** (via `packages/db`). ## Phases @@ -23,7 +23,7 @@ Voir `PLAN.md`. Implémentation séquentielle Phase 0 → 7. Phase 8 (finance) r - Monorepo pnpm + Turborepo - `apps/pwa` Vite + React + TanStack Router/Query + Tailwind + shadcn -- `apps/api` NestJS (modules par domaine), `@supabase/supabase-js` (pas d'ORM) +- `apps/api` NestJS (modules par domaine), Drizzle ORM via `@ordinarthur-os/db` - `packages/shared` types + zod DTOs partagés PWA ↔ API -- `packages/db/migrations` SQL versionné, appliqué manuellement sur Supabase pour l'instant +- `packages/db` schéma Drizzle + `migrations/` SQL versionnées (runner `pnpm --filter @ordinarthur-os/db migrate`) - Pas de fichier `.env` commité, juste `.env.example` diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 42637ff..b143299 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -12,15 +12,15 @@ ┌──────────────┬──────────────────┼────────────────┬──────────────┐ ▼ ▼ ▼ ▼ ▼ ┌────────────┐ ┌─────────────┐ ┌──────────────┐ ┌────────────┐ ┌────────────┐ - │ Supabase │ │ Mistral AI │ │ Groq Whisper │ │ Google Cal │ │ Telegram │ - │ self-host │ │ (small) │ │ (STT FR) │ │ OAuth │ │ Bot API │ + │ Postgres │ │ Mistral AI │ │ Groq Whisper │ │ Google Cal │ │ Telegram │ + │ (k3s STS) │ │ (small) │ │ (STT FR) │ │ OAuth │ │ Bot API │ │ schema │ │ function- │ │ │ │ │ │ │ │ ordinarthur│ │ calling │ │ │ │ │ │ │ │ _os │ │ │ │ │ │ │ │ │ └────────────┘ └─────────────┘ └──────────────┘ └────────────┘ └────────────┘ ``` -La PWA ne parle **jamais** directement à Supabase ni aux APIs externes. Tout passe par le BFF NestJS, protégé par bearer token. +La PWA ne parle **jamais** directement à Postgres ni aux APIs externes. Tout passe par le BFF NestJS, protégé par bearer token. Le service Postgres est en ClusterIP interne au k3s, jamais exposé. ## 2. Monorepo @@ -52,20 +52,28 @@ ordinarthur-os/ │ │ ├── ai/ # command, voice, function dispatcher │ │ ├── telegram/ │ │ └── sync/ # replay client_mutations, dedup -│ ├── db/ # supabase client factory +│ ├── db/ # Drizzle client factory (@ordinarthur-os/db) │ ├── config/ # env schema (zod) │ └── main.ts │ ├── packages/ │ ├── shared/ # Types + zod DTOs partagés pwa/api -│ └── db/ -│ └── migrations/ # 0001_schema.sql, 0002_jobs.sql, … +│ └── db/ # Drizzle ORM : schema TS + migrations SQL +│ ├── drizzle.config.ts +│ ├── src/ +│ │ ├── schema/ # Définitions tables (pgSchema ordinarthur_os) +│ │ ├── client.ts # createDb(connectionString) +│ │ └── migrate.ts # runner DATABASE_URL-driven +│ └── migrations/ # 0000_init.sql, 0001_jobs.sql, … + meta/_journal.json │ ├── deploy/ │ └── k8s/ # Manifests à aligner sur conf Gitea d'Arthur +│ ├── namespace.yaml +│ ├── postgres.yaml # StatefulSet Postgres 16 + PVC │ ├── api.deployment.yaml │ ├── pwa.deployment.yaml │ ├── ingress.yaml +│ ├── migrate.job.yaml # Job one-shot drizzle migrate │ ├── secrets.template.yaml │ └── backup.cronjob.yaml │ @@ -124,6 +132,8 @@ theme: { ## 4. Schéma Postgres `ordinarthur_os` +Source de vérité : les définitions Drizzle dans [`packages/db/src/schema/`](./packages/db/src/schema/). Le SQL ci-dessous est l'équivalent dénormalisé, à titre de référence — les migrations réelles vivent dans `packages/db/migrations/`. + ```sql create schema if not exists ordinarthur_os; set search_path to ordinarthur_os; @@ -283,7 +293,7 @@ create table client_mutations ( ); ``` -**RLS** : activée partout avec policy unique `using (auth.role() = 'service_role')`. Seul le Nest (avec la service key) peut lire/écrire. La PWA est derrière le bearer Nest. +**Pas de RLS.** La base n'est jamais exposée : le seul client SQL est le backend NestJS (ClusterIP interne au k3s, credentials Postgres classiques). La PWA est derrière le bearer Nest. ## 5. API NestJS — routes @@ -427,10 +437,9 @@ spec: ### Secrets k8s attendus -`ordinarthur-os-secrets` : +`ordinarthur-os-secrets` (consommé par l'API) : - `API_BEARER_TOKEN` -- `SUPABASE_URL` (`https://supabase.arthurbarre.fr`) -- `SUPABASE_SERVICE_ROLE_KEY` +- `DATABASE_URL` (`postgres://:@postgres.ordinarthur-os.svc.cluster.local:5432/ordinarthur_os`) - `MISTRAL_API_KEY` - `GROQ_API_KEY` - `GOOGLE_OAUTH_CLIENT_ID` @@ -440,6 +449,13 @@ spec: - `TELEGRAM_WEBHOOK_SECRET` - `ICAL_FEED_SECRET` +`ordinarthur-os-db-secrets` (consommé par le StatefulSet Postgres) : +- `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` + +`ordinarthur-os-backup-secrets` (CronJob) : +- `PGURL` (même valeur que `DATABASE_URL`) +- `RCLONE_REMOTE` (ex. `b2:ordinarthur-os-backups`) + ### CronJob backup ```yaml @@ -462,10 +478,11 @@ spec: args: - | pg_dump "$PGURL" --schema=ordinarthur_os --format=c | gzip > /tmp/dump.gz - # upload vers S3/B2 via rclone (à confirmer avec Arthur) - rclone copy /tmp/dump.gz remote:ordinarthur-os-backups/$(date +%F).gz + rclone copy /tmp/dump.gz "$RCLONE_REMOTE/$(date +%F).gz" ``` +> `PGURL` = même chaîne `postgres://…` que `DATABASE_URL`. Le schéma reste dédié (`--schema=ordinarthur_os`) pour faciliter d'éventuelles restaurations sélectives. + ### Pipeline Gitea (à aligner avec le skill `/deploy` d'Arthur) ```yaml @@ -500,7 +517,7 @@ jobs: - Bearer token Nest : stocké seulement dans les secrets k8s, jamais dans le bundle PWA → la PWA demande le token à l'utilisateur (écran d'onboarding) et le stocke dans `localStorage` (accès par Arthur uniquement sur son device) - Webhook Telegram : vérification de `X-Telegram-Bot-Api-Secret-Token` - Feed iCal : path contient un secret rotable (`ICAL_FEED_SECRET`) -- Supabase : schema dédié + RLS service-role-only +- Postgres : service ClusterIP interne au k3s, jamais exposé ; credentials via Secret k8s - Backups : quotidien, chiffrés au repos côté bucket, rétention 30 jours ## 10. Observabilité (phase ultérieure) @@ -519,9 +536,7 @@ jobs: NODE_ENV=production PORT=3000 API_BEARER_TOKEN= -SUPABASE_URL=https://supabase.arthurbarre.fr -SUPABASE_SERVICE_ROLE_KEY= -SUPABASE_SCHEMA=ordinarthur_os +DATABASE_URL=postgres://ordinarthur:changeme@postgres.ordinarthur-os.svc.cluster.local:5432/ordinarthur_os MISTRAL_API_KEY= MISTRAL_MODEL=mistral-small-latest GROQ_API_KEY= diff --git a/CLAUDE.md b/CLAUDE.md index 6fc219e..17b0fe9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,7 +13,7 @@ Avant toute action, lire dans cet ordre : - **Single-user.** Bearer token statique, pas de multi-tenant. - **Design = portfolio arthurbarre.fr** (cream `#F5F1EA`, ink `#0F0F0F`, accent orange `#FF4A1C`, mono labels, bordures, italique = orange). PAS le violet/cyan du HTML jobs. - **FR only** côté IA (prompts + STT lang=fr). -- **Schéma Postgres dédié `ordinarthur_os`** dans Supabase self-hosted (`supabase.arthurbarre.fr`). +- **Postgres standalone dans le k3s** (plus de Supabase), schéma dédié `ordinarthur_os`. ORM = **Drizzle** (via `packages/db`). ## Phases @@ -23,7 +23,7 @@ Voir `PLAN.md`. Implémentation séquentielle Phase 0 → 7. Phase 8 (finance) r - Monorepo pnpm + Turborepo - `apps/pwa` Vite + React + TanStack Router/Query + Tailwind + shadcn -- `apps/api` NestJS (modules par domaine), `@supabase/supabase-js` (pas d'ORM) +- `apps/api` NestJS (modules par domaine), Drizzle ORM via `@ordinarthur-os/db` - `packages/shared` types + zod DTOs partagés PWA ↔ API -- `packages/db/migrations` SQL versionné, appliqué manuellement sur Supabase pour l'instant +- `packages/db` schéma Drizzle + `migrations/` SQL versionnées (runner `pnpm --filter @ordinarthur-os/db migrate`) - Pas de fichier `.env` commité, juste `.env.example` diff --git a/PLAN.md b/PLAN.md index 8798669..18d5426 100644 --- a/PLAN.md +++ b/PLAN.md @@ -16,7 +16,7 @@ Un assistant personnel qui aide Arthur à s'organiser **sans le déresponsabilis 1. **Self-hosted, open-source**. Pas de Vercel, pas de Next.js. Tout tourne sur le k3s d'Arthur. 2. **Single-user**. Pas de multi-tenant, pas d'invitations, pas de partage. Bearer token unique pour protéger l'API. 3. **PWA installable iOS**. Vite + React, pas de SSR. Service worker + mutation queue pour l'offline. -4. **BFF unique**. La PWA ne parle qu'au NestJS. Le Nest parle à Supabase, Mistral, Groq, Google Calendar, Telegram. +4. **BFF unique**. La PWA ne parle qu'au NestJS. Le Nest parle à Postgres, Mistral, Groq, Google Calendar, Telegram. 5. **Design éditorial / Swiss-brutalist** — mirror du portfolio arthurbarre.fr (cream, ink, orange, borders, mono labels). ## Stack verrouillée @@ -25,8 +25,8 @@ Un assistant personnel qui aide Arthur à s'organiser **sans le déresponsabilis | --- | --- | | Monorepo | pnpm workspaces + Turborepo | | Frontend | Vite + React 18 + TanStack Router + TanStack Query + Tailwind + shadcn/ui | -| Backend | NestJS + `@supabase/supabase-js` (pas d'ORM) | -| DB | Postgres via Supabase self-hosted, schéma dédié `ordinarthur_os` | +| Backend | NestJS + Drizzle ORM (driver `postgres`) | +| DB | Postgres 16 standalone (k3s StatefulSet + PVC), schéma dédié `ordinarthur_os` | | Auth | Bearer token statique (single-user), middleware Nest | | IA LLM | Mistral `mistral-small-latest` (low-cost) via API | | STT | Groq `whisper-large-v3-turbo` | @@ -41,9 +41,9 @@ Un assistant personnel qui aide Arthur à s'organiser **sans le déresponsabilis ### Phase 0 — Scaffold (prio immédiate) - Monorepo `pnpm-workspace.yaml`, `turbo.json` - `apps/pwa` : Vite + React + Tailwind + shadcn + manifest PWA + service worker placeholder + routing TanStack -- `apps/api` : NestJS + module `health` + middleware bearer + client supabase initialisé +- `apps/api` : NestJS + module `health` + middleware bearer + client Drizzle initialisé - `packages/shared` : types et zod schemas partagés -- `packages/db` : premier fichier de migration SQL créant le schéma `ordinarthur_os` +- `packages/db` : schéma Drizzle + premières migrations SQL (`0000_init` crée le schéma `ordinarthur_os`) - `deploy/k8s` : manifests génériques (à adapter ensuite à la conf Gitea/Traefik d'Arthur) - Design system : composants primitifs (`