Le couteau suisse numérique — toolbox personnelle et modulaire (Next.js + Supabase). Voir PRD.MD pour le détail du produit.
- Va sur supabase.com → New Project.
- Une fois créé, ouvre SQL Editor et colle le contenu de
supabase/schema.sql, puis exécute-le. Ça crée les tablesclipboard_items/shared_files, le bucket Storageshared-files, active Realtime, et pose les politiques RLS. - Va dans Authentication → Providers → Email et désactive "Allow new users to sign up". C'est important : SwissTool n'a pas de page d'inscription (comptes créés uniquement par toi, voir §Multi-tenant plus bas), donc si le sign-up public reste activé côté Supabase, n'importe qui trouvant l'URL de l'app pourrait se créer un compte tout seul.
- Va dans Authentication → Users → Add user et crée ton compte (email + mot de passe) à la main. Répète cette étape pour chaque personne à qui tu veux donner un accès — voir §Multi-tenant.
- Va dans Authentication → Policies (ou Providers → Email → Password) et active Leaked Password Protection si ton plan Supabase le permet (Pro et plus — vérifie contre HaveIBeenPwned.org à la création du compte). Pas accessible via SQL/MCP, uniquement le dashboard ou l'API de management.
cp .env.local.example .env.localRemplis NEXT_PUBLIC_SUPABASE_URL et NEXT_PUBLIC_SUPABASE_ANON_KEY avec
les valeurs de Project Settings → API dans le dashboard Supabase.
Renseigne aussi RAPIDAPI_KEY (voir §Module Téléchargeur) si tu comptes
utiliser cet outil.
npm run devOuvre http://localhost:3000 — tu seras redirigé
vers /login.
src/
├── app/ # Routes Next.js (App Router)
│ ├── login/ # Page de connexion (email + mot de passe)
│ └── (app)/ # Routes protégées (groupe de routes, pas d'URL ajoutée)
│ ├── clipboard/
│ ├── password/
│ ├── history/
│ ├── files/
│ └── settings/
├── core/
│ ├── layout/ # Sidebar, TopBar, BottomNav, AppShell
│ ├── services/ # eventBus, clipboardApi, filesApi, downloaderApi, storage (localStorage)
│ ├── store/ # Zustand : thème, toasts
│ └── registry/ # Liste des modules (contrat PRD §12.2)
├── modules/ # Un dossier = un module indépendant
│ ├── clipboard/
│ ├── password/
│ ├── history/
│ ├── files/ # "Secure Drop" — upload vers Supabase Storage
│ └── downloader/ # Téléchargeur de médias (RapidAPI + VidsSave)
├── shared/ # UI réutilisable (Button, Card, Input, Badge...)
└── lib/
├── supabase/ # Clients Supabase (browser / server / proxy)
└── media-downloader/ # Clients RapidAPI/VidsSave + garde-fou SSRF
src/proxy.ts # Protection des routes (redirige vers /login si non connecté)
supabase/schema.sql # Schéma DB + Storage + RLS + triggers
Depuis le 2026-08-20, plusieurs comptes peuvent coexister, chacun avec son
propre espace totalement isolé (Presse-papier, Fichiers, Historique) —
un compte ne voit ni ne modifie jamais les données d'un autre. Isolation
posée en base (RLS Postgres scopée sur user_id/owner_id), pas dans le
code applicatif : voir supabase/schema.sql.
Ajouter un nouveau compte (invite-only, pas de formulaire public) : Authentication → Users → Add user dans le dashboard Supabase. Le nouveau compte démarre avec un espace vide — rien à migrer, rien à configurer côté app.
Récupère un lien de téléchargement direct depuis Instagram, TikTok,
Facebook, X/Twitter et YouTube. Deux providers externes, routés
automatiquement selon le domaine de l'URL collée (voir
src/lib/media-downloader/) :
- RapidAPI
social-download-all-in-one— Instagram/TikTok/Facebook/X. Un seul appel, URL de téléchargement immédiatement disponible. NécessiteRAPIDAPI_KEY(compte sur rapidapi.com). - VidsSave (non-officiel) — YouTube uniquement, plus fiable que RapidAPI sur cette plateforme spécifique. Flux en 3 appels dont une attente SSE (résolution déclenchée seulement au moment où l'utilisateur choisit un format, jamais pour tous les formats d'un coup).
Les deux providers renvoient des fichiers déjà complets (vidéo + son intégré) — pas de montage côté client nécessaire.
Enregistrement direct dans la galerie (iPhone/Android) : le bouton
"Enregistrer dans Photos" utilise navigator.share({ files }) (Web Share
API) plutôt qu'un geste (appui long), plus fiable en pratique. Comme les
CDN médias externes n'envoient pas d'en-têtes CORS, ce bouton passe par un
proxy serveur (/api/downloader/stream) qui relaie les octets — ce proxy
valide que la cible ne résout pas vers une IP privée/interne avant de la
fetcher (défense SSRF, voir src/lib/media-downloader/ssrf.ts).
Vérifié via les Security Advisors Supabase (MCP get_advisors) :
| Point | État |
|---|---|
RLS activé sur clipboard_items, shared_files, storage.objects |
✅ |
Isolation multi-tenant (user_id/owner_id, policies scopées auth.uid()) |
✅ vérifiée le 20.08 |
Fonctions SECURITY DEFINER non exposées publiquement (enforce_clipboard_history_limit, redact_expired_passwords) |
✅ |
Bucket shared-files privé (pas d'URL publique, téléchargement via lien signé 5 min) |
✅ |
replica identity par défaut (pas full) sur clipboard_items/shared_files — évite qu'un DELETE fasse fuiter la ligne complète via Realtime, qui n'applique pas RLS aux DELETE |
✅ corrigé le 20.08 |
| Inscription publique désactivée, comptes créés à la main (dashboard) | ✅ |
| Leaked Password Protection | |
Proxy /api/downloader/stream (module Téléchargeur) protégé contre le SSRF par résolution DNS + blocage des IP privées/internes, pas d'allowlist de domaine |
✅ |
public.rls_auto_enable() visible par anon/authenticated |
ℹ️ fonction interne à la plateforme Supabase, pas la nôtre — rien à corriger |
Contrairement à clipboard_items (dont le contenu des mots de passe
expirés est redirigé par redact_expired_passwords()), il n'existe pas
encore de nettoyage automatique des fichiers expirés dans
shared_files / le bucket Storage. Un fichier expiré s'affiche "Expiré"
dans l'UI mais reste techniquement téléchargeable et consomme du quota
jusqu'à suppression manuelle. À industrialiser plus tard via une Edge
Function + pg_cron si l'usage le justifie.
Déploiement continu sur Vercel — connecte ce repo,
renseigne les mêmes variables d'environnement que .env.local.