Le copilote admin des ostéopathes libéraux. Dicte 60 secondes après la consultation, retrouve une note structurée (motif / anamnèse / examen / traitement / conseils) et archivée par patient. Conforme à l'obligation de conservation 20 ans.
Stack Next.js 14 (App Router, TS strict) · Supabase (Auth + Postgres + Storage EU) · Mistral AI (Voxtral transcription + Mistral Large structuration, Paris/UE) · Stripe (billing) · PostHog EU (analytics) · @react-pdf/renderer (export) · IndexedDB + Service Worker (offline) · Tailwind + shadcn/ui · Vercel cdg1 · Vitest + Playwright + GitHub Actions.
- Prérequis
- Setup en 10 minutes (1re fois)
- Setup sans Docker (alternative)
- Problèmes courants
- Scripts
- Arborescence
- Pipeline IA
- Tests
- Observabilité
- Sécurité & RGPD
- Variables d'env
- Déploiement
- Conventions
| Outil | Version | Comment vérifier |
|---|---|---|
| Node.js | ≥ 20 (LTS, idéalement 22) | node --version |
| npm (livré avec Node) ou pnpm | npm ≥ 10, pnpm ≥ 9 | npm --version ou pnpm --version |
| Docker Desktop | dernière | Lancé, icône baleine 🐳 verte |
| Clé Mistral AI | avec accès Voxtral + Mistral Large | https://console.mistral.ai/api-keys |
Note Windows : si tu installes pnpm via npm install -g pnpm et que tu es encore en Node 20, utilise pnpm 9 explicitement (npm install -g pnpm@9) — pnpm 10+ exige Node 22.13+ et plantera avec ERR_UNKNOWN_BUILTIN_MODULE: node:sqlite.
Si tu n'as pas envie d'installer pnpm, npm marche pareil : remplace pnpm par npm run dans toutes les commandes (npm run dev, npm run db:reset, etc.). Pas de fonctionnalité bloquée, juste un peu plus lent à l'install.
Supabase est un ensemble de services (Postgres, API REST, Auth, Storage, mailbox locale Inbucket, Studio). En dev, la CLI Supabase lance ~7 conteneurs Docker pour répliquer tout ça sur ta machine. En prod, plus aucun Docker — tu utilises le service hébergé supabase.com.
Tu peux complètement zapper Docker si tu préfères : utilise un projet Supabase free tier directement → voir Setup sans Docker plus bas.
esbenp.prettier-vscodedbaeumer.vscode-eslintbradlc.vscode-tailwindcssms-playwright.playwrightsupabase.supabase-vscode
Toutes les commandes sont sous PowerShell (Windows) — adapte pour bash si tu es sur macOS/Linux. Substitue pnpm par npm run partout si tu n'utilises pas pnpm.
cd C:\chemin\vers\Sidekick
npm install # ou : pnpm installCompte ~2-5 min selon ta connexion. Tu verras added 800+ packages à la fin.
Ouvre l'app Docker Desktop, attends que l'icône baleine soit verte (état "Engine running"). Sans Docker lancé, l'étape 3 plante.
npm run db:start # ou : pnpm db:startLa 1re fois ça télécharge les images Docker (~500 Mo, 2-3 min). Ensuite la sortie affiche les valeurs à copier — garde-la sous les yeux :
API URL: http://127.0.0.1:54321
DB URL: postgresql://postgres:postgres@127.0.0.1:54322/postgres
Studio: http://127.0.0.1:54323
Inbucket: http://127.0.0.1:54324
anon key: eyJh...
service_role key: eyJh...
Copy-Item .env.local.example .env.local
notepad .env.local # ou ton éditeur préféréColle les valeurs affichées par db:start à la place des placeholders. Ajoute ta clé Mistral dans MISTRAL_API_KEY=... (obtenue sur https://console.mistral.ai/api-keys).
Les variables Stripe / PostHog / Sentry sont optionnelles — laisse les valeurs par défaut, l'app marche en mode no-op pour ces intégrations.
npm run db:reset # ou : pnpm db:resetÇa applique les ~12 fichiers SQL de supabase/migrations/ dans l'ordre. Tu verras Applying migration 20260515000000_init_schema.sql... etc. Si tu vois Database reset complete! à la fin, c'est OK.
npm run db:typesÇa écrit types/database.ts. Pas obligatoire pour faire tourner l'app.
npm run dev # ou : pnpm dev→ Ouvre http://localhost:3000.
- Clique « Commencer » sur la landing → tape n'importe quel email (ex.
osteo@test.fr). - Va sur Inbucket : http://127.0.0.1:54324 — c'est la boîte mail locale Supabase, tous les emails dev arrivent là.
- Ouvre le dernier email, clique « Me connecter » → tu arrives sur
/app.
http://127.0.0.1:54323 — UI pour voir les tables, les rows, les policies RLS, lancer du SQL ad hoc.
Si Docker est compliqué chez toi (machine d'entreprise, pas envie d'installer), tu peux pointer Sidekick vers un projet Supabase cloud free tier (500 Mo de DB gratuit).
# 1. Crée un compte sur https://supabase.com
# 2. New project → région "Europe (Paris)" eu-west-3 → free tier
# 3. Project Settings → API → copie l'URL et les 2 clés (anon + service_role)
# 4. Édite .env.local avec ces valeurs cloud (au lieu de http://127.0.0.1:54321)
# 5. Lie ton repo local au projet cloud
npx supabase login
npx supabase link --project-ref <ton-ref> # le ref est dans l'URL du dashboard
# 6. Push les migrations vers la DB cloud
npx supabase db push
# 7. Lance Next.js comme d'habitude
npm run devDifférence avec le mode local : Inbucket n'existe pas en cloud → les magic links partent vers ta vraie boîte mail (configure le SMTP dans Supabase → Auth → SMTP Settings, ou laisse Supabase envoyer via leur SMTP par défaut limité à 3 emails/h). Le Studio est accessible depuis le dashboard web supabase.com, pas en localhost.
Trade-off : tu touches une vraie DB. Un db reset efface tes vraies données. Latence réseau légère. Plus de chance de garder ton state entre les sessions.
pnpm n'est pas installé. Soit installe-le (npm install -g pnpm@9 pour rester compatible Node 20), soit utilise npm run X au lieu de pnpm X.
Tu as installé pnpm v10+ qui exige Node ≥ 22.13. Soit upgrade Node vers 22 LTS, soit downgrade pnpm : npm install -g pnpm@9.
Tu as oublié de lancer npm install (ou pnpm install) dans le dossier du projet. Les binaires sont dans ./node_modules/.bin/, npm les trouve automatiquement quand tu lances npm run X depuis le dossier.
Une dépendance demande une version qui n'existe pas sur npm. Note le nom du package et corrige la version demandée dans package.json. Tu peux aussi tenter npm install --legacy-peer-deps qui est plus tolérant.
Quelqu'un d'autre tourne sur 54321/54322/54323/54324. Soit tue le process (netstat -ano | findstr :54321), soit change les ports dans supabase/config.toml.
Docker Desktop n'est pas lancé OU le user Docker n'a pas les droits sur les volumes. Relance Docker en admin la première fois, ou docker volume prune puis re-essaie.
Vérifie que tu lis Inbucket (http://127.0.0.1:54324) et pas ta vraie boîte mail — en local, les emails ne sortent jamais. Si Inbucket est vide, regarde supabase logs dans la console Supabase Studio.
Depuis la v0.6, Sidekick supporte le travail en équipe (cabinet de groupe, collaborations). Chaque ostéopathe a au minimum une team perso "Mon cabinet" créée automatiquement à son signup. Un praticien peut être membre de plusieurs teams. Rôles techniques (en base) : owner, coach, assistant — le rôle coach est conservé tel quel pour ne pas casser la migration de schéma, malgré le pivot ostéo (cf. NEXT.md).
Toutes les RLS basculent sur team_id in (select user_team_ids(auth.uid())). Le helper Postgres user_team_ids est en SECURITY DEFINER (pattern Supabase canonique pour casser la récursion sur team_members).
→ Procédure d'invitation + test manuel des RLS dans docs/TEAMS.md.
pnpm db:reset : la séquence test 1-8 de TEAMS.md vérifie que l'isolation RLS marche. À exécuter avant tout push prod sur une fresh DB.
Chaque commande marche en pnpm <X> ou en npm run <X> (substituable).
| Commande | Rôle |
|---|---|
dev |
Next.js en dev (http://localhost:3000) |
build |
Build prod |
start |
Serveur prod |
lint |
ESLint |
lint:fix |
ESLint + autofix |
format |
Prettier write |
format:check |
Prettier check (utilisé en CI) |
typecheck |
tsc --noEmit |
test |
Vitest run |
test:watch |
Vitest mode watch |
test:coverage |
Vitest + couverture v8 |
test:e2e |
Playwright |
test:e2e:ui |
Playwright UI mode |
test:install |
Installe les navigateurs Playwright |
db:start |
Démarre Supabase local (Docker) |
db:stop |
Arrête Supabase local |
db:reset |
Drop + remigre + seed |
db:types |
Génère types/database.ts depuis le schéma local |
stripe:listen |
Stripe CLI → forwarde le webhook vers localhost |
cap:sync |
Sync la config Capacitor + plugins (iOS/Android) |
cap:ios |
Ouvre le projet iOS dans Xcode |
cap:android |
Ouvre le projet Android dans Android Studio |
app/
(auth)/login magic link (form server-side)
auth/callback handler du code OTP
(app)/ routes protégées par middleware
page.tsx dashboard
clients/ CRUD + historique + archivage
notes/ recorder, détail, retry, édition
settings/ compte, plan, CGU/Privacy, RGPD
legal/ cgu, privacy (publiques)
error.tsx, not-found.tsx, loading.tsx, robots.ts, sitemap.ts, manifest.ts, icon.svg
lib/
supabase/ server, client, middleware, admin (service_role)
ai/ transcribe + structure (+ retry backoff) + prompts
actions/ server actions : auth, clients, notes, account
data/ lectures DB (RSC-friendly)
schemas/ Zod : client, note, plan limits (+ tests)
rate-limit.ts sliding window in-memory (+ tests)
logger.ts logger structuré JSON pour Vercel Logs
observability.ts wrapper Sentry-compatible (passthrough actuel)
constants.ts ROUTES, AUDIO, AI, RATE_LIMIT, APP
components/
ui/ shadcn (button, input, textarea, label, card, skeleton, toaster)
MobileNav, UsageBadge, PublicFooter
supabase/
config.toml région, ports, magic link template
migrations/ schema + RLS + storage
seed.sql, templates/magic_link.html
test/setup.ts vitest jsdom + env mock
e2e/landing.spec.ts Playwright smoke
instrumentation.ts hook Next.js boot
middleware.ts refresh session + redirection auth
.github/workflows/ci.yml lint + typecheck + tests + build + e2e
[Recorder.tsx]
└─ MediaRecorder → Blob → FormData → useTransition
└─ Server Action createNoteFromAudioAction
├─ rate_limit_check (12/h par user) ──► sinon ?limit=...
├─ canCreateNote(plan, count) ──► sinon ?limit=...
├─ Zod parse FormData + MIME + size ──► sinon ?error=...
├─ INSERT session + note(status=processing)
├─ Storage.upload('audio/{coach_id}/{note_id}.{ext}')
├─ runPipelineAndPersist():
│ ├─ withRetry(voxtral) max 2 retries, backoff 1.5/3s
│ ├─ withRetry(mistral-large) + zod.parse → rejette les hallus
│ ├─ UPDATE note(status='ready', motif, anamnese, examen, traitement, conseils, raw_transcript)
│ └─ DELETE audio (RGPD + coût zéro)
│ ↳ catch: UPDATE note(status='failed', error_message) — AUDIO CONSERVÉ pour retry
└─ redirect /app/notes/{id}
Retry : si status='failed', la page note affiche un bouton "Réessayer la mise en forme". L'action retryNoteAction retélécharge l'audio depuis Storage et relance runPipelineAndPersist. Si l'audio n'existe plus (suppression, RGPD), on propose le réenregistrement.
# Tests unitaires (rapide, isolé)
pnpm test
# Watch mode pendant le dev
pnpm test:watch
# Couverture (seuils 70 % sur lib/ hors actions/supabase)
pnpm test:coverage
# E2E Playwright
pnpm test:install # 1re fois seulement
pnpm test:e2e
pnpm test:e2e:ui # mode interactifCouverture cible : les fonctions pures (schemas Zod, plan limits, rate-limit, retry, utils) sont testées à fond. Les server actions sont volontairement non testées en unit — testées via Playwright avec un Supabase local. Pour aller plus loin → ajouter Vitest sur les actions en mockant Supabase.
Voir TESTING.md pour la procédure de test manuelle bout-en-bout.
- Logs structurés (JSON sur stdout, parsé par Vercel Logs) via
lib/logger.ts. Événements clés :note_pipeline_start,note_pipeline_ok,note_pipeline_failed,note_retry_start,magic_link_sent,rate_limit_hit,sub_upserted. Les emails et user IDs sont hashés (hashPii). - PostHog analytics :
lib/analytics/(server + client). Funnel d'activationsignup_completed → first_client_added → first_note_started → first_note_ready. Region EU. No-op si la clé manque. - Sentry non branché par défaut. Pour activer : décommente dans
instrumentation.tsetlib/observability.ts, ajouteSENTRY_DSNà l'env. LescaptureException()sont déjà placés aux bons endroits. - Rate limiter in-memory → suffisant pour 1 instance Vercel. Pour multi-instance, swap vers Upstash Redis (cf.
NEXT.md).
- Headers : HSTS, X-Frame-Options DENY, X-Content-Type-Options, Referrer-Policy, Permissions-Policy (
microphone=(self)). - Noindex sur
/app/*(header + robots.ts). - Cookies : uniquement strictement nécessaires (session auth).
- PII dans les logs : emails et user IDs hashés (
hashPiinon crypto, juste anti-corrélation passive). - Audio : supprimé automatiquement après transcription réussie. Conservé seulement si échec, jusqu'à retry ou suppression manuelle.
- Effacement total : page
/app/settings→ bouton "Effacer toutes mes données" → cascade DB + cleanup Storage + suppressionauth.users. - CGU + Privacy :
/legal/cguet/legal/privacy.
| Nom | Côté | Rôle |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL |
client + server | Endpoint Supabase |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
client + server | Clé anonyme (RLS-safe) |
SUPABASE_SERVICE_ROLE_KEY |
server only | Bypass RLS pour cleanup audio + suppression |
MISTRAL_API_KEY |
server only | Voxtral + Mistral Large |
NEXT_PUBLIC_APP_URL |
client + server | Base URL (emails, sitemap, redirects) |
SENTRY_DSN (optionnel) |
server | Active Sentry quand renseigné |
.env.local n'est jamais commit (cf. .gitignore).
- Crée un projet dans la région eu-west-3 (Paris) via dashboard.
supabase link --project-ref <ref>supabase db push(applique les 3 migrations)- Dans Auth → URL Configuration : ajoute
https://<ton-domaine>/auth/callbackaux Redirect URLs. - Dans Auth → Email Templates : copie/colle
supabase/templates/magic_link.htmldans le template "Magic Link".
- Importe le repo, région cdg1 (Paris).
- Variables d'env : copie les 5 (Supabase URL + anon + service_role + Mistral + APP_URL prod).
- Configure le custom domain.
- Vérifie les Logs après le 1er déploiement → tu dois voir
app_bootau démarrage.
- Crée 2 prix dans Dashboard Stripe → Produits → "Sidekick Pro" : mensuel 29 € et annuel 290 €. Copie les
price_xxxdansSTRIPE_PRICE_MONTHLYetSTRIPE_PRICE_ANNUAL. - Active le Customer Portal : Dashboard → Settings → Billing → Customer portal → Activate.
- Configure le webhook prod :
https://<domaine>/api/webhooks/stripe. Events nécessaires :checkout.session.completed,customer.subscription.updated,customer.subscription.deleted,invoice.payment_failed. Copie lewhsec_xxxdansSTRIPE_WEBHOOK_SECRET. - En dev,
pnpm stripe:listenforwarde le webhook local. Lewhsec_xxxaffiché va dans.env.local. - Test card :
4242 4242 4242 4242, date future, CVC libre.
- Projet sur https://eu.posthog.com → API key →
NEXT_PUBLIC_POSTHOG_KEYetPOSTHOG_KEY. - Crée un funnel
signup_completed → first_client_added → first_note_started → first_note_ready. - Si vide → les modules
lib/analytics/sont no-op, l'app marche sans.
- Magic link arrive (pas dans spam)
- CGU et Privacy à jour (raison sociale, adresse postale)
- Stripe webhook → test card →
profile.plan='pro'(Studio) - PostHog reçoit
signup_completedau 1er login - Service Worker enregistré (DevTools → Application → Service Workers)
- Export PDF d'un client avec ≥ 1 note → fichier propre
- Mode offline : Network → Offline → enregistre → badge "1 note en attente" → repasse online → upload auto
- CI verte sur main
- Service Worker pas de Background Sync API : iOS Safari ne le supporte pas. À la place : drain auto sur
online+ bouton manuel. Couvre 95 % des cas usage entre deux consultations. - Rate limiter in-memory : 1 instance Vercel = un store séparé. Swap Upstash Redis quand le trafic l'exige.
- PDF export server-side (
@react-pdf/renderer, Node, ~2 MB bundle) plutôt que client jsPDF : meilleure typo, code réutilisable pour futurs templates. - Stripe
apiVersion: "2024-09-30.acacia"figée : un bump vers2025-03-31.basildéplaceSubscription.current_period_end(breaking silencieux). Commentaire de garde danslib/stripe.ts. - RootLayout async : récupère le profil pour identifier PostHog en SSR. Coût : la landing perd la SSG statique. Trade-off OK pour un MVP analytique-first.
- TypeScript strict +
noUncheckedIndexedAccess+ pas deany. - Server Actions pour les mutations (sauf callbacks → Route Handlers).
- Zod valide les entrées utilisateur ET les sorties LLM.
- Mobile-first : bouton hero atteignable au pouce (96px Ø, position basse, safe-area iOS).
- Vocabulaire UI : « Ta note s'écrit toute seule » — repris du field report Reddit, jamais « Génération IA en cours ».
- Commits :
feat:,fix:,refactor:,test:,docs:,chore:(Conventional Commits, pas de commitlint forcé). - PR : remplit le template
.github/PULL_REQUEST_TEMPLATE.md.
TESTING.md— procédure manuelle bout-en-boutdocs/TEAMS.md— multi-coach, RLS, tests d'isolationdocs/WEBHOOKS.md— webhooks sortants, signature HMAC, exemplesdocs/MOBILE.md— wrap Capacitor iOS + Androiddocs/API.md— API publique v1NEXT.md— dette technique v0.7+CHANGELOG.md— historique des versions
Code source publié à titre de démonstration (portfolio). © 2026 Martin Mialaret — tous droits réservés : pas de réutilisation commerciale sans accord écrit.