Pourquoi un template
On a livré 11 SaaS B2B en 2025. Sur chacun, on retombait sur les mêmes 8-10 jours de plomberie en début de projet : auth, organisations, abonnement Stripe, webhooks, dashboard admin.
On a fini par extraire le tout dans un template public. C'est ce qu'on clone aujourd'hui pour démarrer la majorité de nos MVPs B2B.
Repo : github.com/devoria/saas-starter (à venir, en cours d'open-sourcing).
Ce qu'il y a dedans
- Next.js 16 (App Router, RSC partout où ça a du sens)
- TypeScript strict, ESLint + Prettier configurés
- Supabase : Postgres + Auth (magic link + Google OAuth) + Storage
- Drizzle ORM : schemas typés, migrations, RLS policies
- Stripe Checkout : abonnement mensuel + annuel + period d'essai
- Webhook Stripe : Cloudflare Worker dédupliqué (pattern
stripe_eventstable) - Dashboard admin : impersonation, logs, dump CSV
- Tests E2E : Playwright sur les 5 parcours critiques
- Tailwind v4 + design tokens partagés
Le schéma DB minimal
-- 4 tables, suffisantes pour 80 % des SaaS B2B
users (id, email, created_at)
organizations (id, name, owner_id, created_at)
memberships (user_id, org_id, role)
subscriptions (org_id, stripe_id, status, current_period_end)
Multi-tenant via organization_id partout, RLS strict, pas de leak possible entre orgs.
Le pattern auth qu'on utilise
Magic link par défaut. Google OAuth en option. Le redirect après login va sur /post-login qui :
- Vérifie la session
- Si
organization_idmanquant en cookie → page de sélection d'organisation - Sinon redirige vers
/dashboard
Ce pattern résout 90 % des cas multi-org. Pour les 10 % restants (utilisateur dans plusieurs orgs), un menu de switch est ajouté.
Le webhook Stripe, en 4 étapes
C'est la partie qui casse le plus souvent en prod. Voici notre pattern :
- Receive : Cloudflare Worker reçoit le webhook, vérifie la signature.
- Dedupe : on insère l'
event.iddansstripe_events. Si conflict, on ignore. - Dispatch : selon
event.type, on appelle un handler typé. - Ack : on retourne
200immédiatement. Le handler s'exécute en background.
const eventTypes = {
"checkout.session.completed": handleCheckout,
"customer.subscription.updated": handleSubUpdated,
"customer.subscription.deleted": handleSubDeleted,
"invoice.payment_failed": handlePaymentFailed,
} as const;
Pas de switch infini, pas de if/else à rallonge. Un dispatch table typé et une fonction par type d'événement. Lisible, testable, extensible.
Ce qui n'est PAS dedans (et pourquoi)
- Pas de Redis : Postgres + indexes bien posés suffisent pour 99 % des cas. On ajoute Redis quand on a une raison de le faire.
- Pas de queue Job : Cloudflare Queues quand vraiment nécessaire. Sinon
setTimeoutcôté server est OK pour < 1 000 jobs/jour. - Pas de feature flags : on déploie ou on ne déploie pas. Si on a besoin de gates, c'est une décision projet par projet.
- Pas de "design system" : juste Tailwind + composants atomiques. Pas de système d'UI lourd à maintenir.
C'est volontaire. Un template doit être boring pour être utile.
Comment l'utiliser
git clone [email protected]:devoria/saas-starter.git my-app
cd my-app
pnpm install
cp .env.example .env.local
# remplir SUPABASE_URL, SUPABASE_KEY, STRIPE_KEY, STRIPE_WEBHOOK_SECRET
pnpm db:push
pnpm dev
5 minutes pour avoir un SaaS qui démarre, accept des paiements et gère le cycle de vie d'abonnement. À vous de coder votre métier au-dessus.
Le cas typique où on l'utilise
- MVP B2B avec abonnement mensuel
- Time-to-market < 6 semaines
- Stack moderne, déployable sur Vercel + Supabase Pro
- Code propre, testé, livrable au fondateur en fin de projet
Si c'est votre cas, parlez-nous. On démarre avec ce template, on enlève ce qui sert pas, on ajoute votre métier au-dessus, et on livre en 4 à 6 semaines.






