Build This Now
Build This Now
Qu'est-ce que le code Claude ?Installer Claude CodeL'installateur natif de Claude CodeTon premier projet Claude Code
Failed Payment RecoveryLocalization and i18nPDF InvoicesReferral ProgramTwo-Factor AuthNext.js DevTools MCPNext.js Agent SetupSubscription BillingMulti-Tenant SaaSAI Chat FeatureRate LimitingStripe WebhooksSemantic SearchRealtime UpdatesDrip Email Sequencesv2.1.122 Release NotesClaude Code Dynamic Workflows : comment orchestrer 1 000 sous-agents sur une vraie codebaseBonnes pratiques Claude CodeMeilleures pratiques pour Claude Opus 4.7Claude Code sur un VPSIntégration GitRevue de code avec Claude CodeLes Worktrees avec Claude CodeClaude Code à distanceClaude Code ChannelsChannels, Routines, Teleport, DispatchTâches planifiées avec Claude CodePermissions Claude CodeLe mode auto de Claude CodeAjouter les paiements Stripe avec Claude CodeFeedback LoopsWorkflows TodoGestion des tâches dans Claude CodeTemplates de projetTarification et utilisation des tokens Claude CodeTarifs de Claude Code : ce que tu vas vraiment payerClaude Code Ultra ReviewConstruire une app Next.js avec Claude CodeSupabase DatabaseVercel DeepsecTest-Driven DevelopmentConstruire un MVP SaaS avec Claude CodeAjouter l'authentification avec Claude Code (Supabase Auth)Ajouter les emails transactionnels avec Claude Code (Resend + React Email)Construire une API type-safe avec Claude Code (oRPC + Zod)File UploadsBackground Jobs (Inngest)Admin DashboardFull-Text SearchClaude Agent SDKKiro Migration GuideClaude Research AgentLeave Grok BuildRoute Subagent ModelsClaude Monorepo SetupCI Repair AgentVisual Regression TestsAgent Cost DashboardCursor Migration GuideCommerce agentique : comment construire une app que les agents IA peuvent payer1M ContextUser API KeysAudit LogsCSV Import PipelineDatabase MigrationsProduction Error TrackingFeature FlagsGitHub ActionsHeadless ModeMax Plan vs APICaching and RevalidationIn-App NotificationsOutbound WebhooksPrompt CachingRoles & PermissionsMarketplace PaymentsUsage-Based BillingCombien coûte la création d'un SaaS avec Claude Code en 2026Parallel AI AgentsCoding Agent Injection
speedy_devvkoen_salo
Blog/Handbook/Workflow/SaaS MVP

Construire un MVP SaaS avec Claude Code

Un journal de build sur un week-end : monter une app Next.js 16, ajouter l'auth Supabase et Postgres, brancher la facturation Stripe, et déployer sur Vercel, le tout avec Claude Code.

Vous voulez le framework derrière ces projets ?

Obtenez le système Claude Code que nous utilisons pour planifier, construire, tester et livrer des logiciels en production.

Découvrez ce que nous construisons pour les entreprises →
speedy_devvkoen_salo
speedy_devvWritten by speedy_devvPublished Jul 14, 202614 min readHandbook hubWorkflow index

Le week-end dernier, j'ai construit une petite app de feedback board de bout en bout avec Claude Code : connexion, création d'un board, collecte de demandes de fonctionnalités, upvotes, paiement de $19 par mois pour débloquer plus d'un board. Rien de compliqué là-dedans, et c'est bien le but. Voici le build réel, dans l'ordre, avec le code qui a fini dans le repo.

Choisir un petit SaaS réel à construire

L'app d'exemple s'appelle Signal. C'est un feedback board public : un fondateur crée un board, partage le lien, les utilisateurs soumettent des demandes de fonctionnalités et upvotent celles qu'ils veulent. Les comptes gratuits ont un seul board. Les comptes payants en ont un nombre illimité.

C'est assez petit pour finir en un week-end et assez complet pour toucher chaque couche dont un vrai SaaS a besoin : auth, un schéma relationnel avec row-level security, une boucle de fonctionnalité centrale, et de la facturation récurrente. Si tu construis autre chose, la forme du travail ci-dessous reste valable. Remplace boards et posts par les objets centraux de ton produit.

Avant de commencer

Quatre choses doivent exister avant d'ouvrir Claude Code.

Node.js 20.9.0 ou plus récent, puisque Next.js 16 a abandonné le support de Node 18 :

node --version

Claude Code installé globalement, et un plan Claude Pro ou Max. Le tier gratuit ne marche pas avec Claude Code.

npm install -g @anthropic-ai/claude-code

Des comptes sur Supabase, Stripe et Vercel, tous avec des tiers gratuits généreux pour un projet de cette taille. Crée le projet Supabase et le compte Stripe maintenant, tu auras besoin des clés API des deux dans les étapes qui suivent.

Un repo GitHub, pour que le déploiement Vercel à la fin ne soit qu'un simple push au lieu d'un upload manuel.

Monter le projet

Pars d'un projet Next.js 16 propre avec Turbopack et Tailwind CSS v4 déjà branchés.

npx create-next-app@latest signal --typescript --tailwind --app --turbopack
cd signal
npx shadcn@latest init

Installe les packages dont dépend le reste du build : le client SSR Supabase pour l'auth et Postgres, et le SDK Stripe pour la facturation.

npm install @supabase/ssr @supabase/supabase-js stripe zod

Ouvre le projet dans Claude Code :

claude

Écrire CLAUDE.md et AGENTS.md

Claude Code lit AGENTS.md pour les conventions du framework et CLAUDE.md pour les règles propres au projet. Sur la release canary de Next.js 16, les deux sont générés pour toi. Si tu es sur la version stable, crée AGENTS.md toi-même avec une ligne qui pointe vers la doc embarquée :

node_modules/next/dist/docs/

Écris ensuite CLAUDE.md. C'est le fichier qui empêche Claude de deviner ta stack, l'organisation de tes fichiers et tes conventions de nommage à chaque session.

@AGENTS.md

## Stack

- Next.js 16 with App Router (TypeScript)
- Tailwind CSS v4 with shadcn/ui components
- PostgreSQL via Supabase, with row-level security on every table
- Stripe for subscription billing

## File Conventions

- Server Components by default. "use client" only for interactivity.
- Supabase server client: lib/supabase/server.ts
- Supabase admin client (service role, webhook use only): lib/supabase/admin.ts
- Server Actions live next to the routes that use them, in actions.ts files
- Route handlers for webhooks only, under app/api/

## Commands

- Dev server: npm run dev
- Type check: npx tsc --noEmit
- Build: npm run build

## Proxy

- Auth checks live in proxy.ts, not middleware.ts (Next.js 16)

La ligne sur la row-level security compte plus qu'elle n'en a l'air. Sans elle explicitée, Claude va parfois écrire une table et oublier d'activer la RLS dessus, ce qui veut dire que chaque ligne est lisible par le monde entier par défaut dans Supabase.

Planifier le schéma de base de données en Plan mode

Avant qu'aucun code ne soit écrit, utilise le plan mode pour poser le schéma.

claude --permission-mode plan "design the Postgres schema for Signal: boards owned by a user, posts on a board, and votes on a post. Free accounts get 1 board. Paid accounts get unlimited boards. Include row-level security policies."

Claude revient avec quatre tables (profiles, boards, posts, votes), les clés étrangères entre elles, et un plan pour la RLS : boards et posts sont lisibles publiquement pour qu'un lien de board fonctionne pour les visiteurs anonymes, mais les écritures exigent un utilisateur authentifié qui possède la ressource. Relis ça avant que quoi que ce soit ne soit construit. Les décisions de schéma sont les plus coûteuses à défaire une fois que tu as de vraies données dans les tables.

Mettre en place Supabase et Postgres

Crée un nouveau projet Supabase depuis le dashboard, puis passe le schéma dans l'éditeur SQL. Voici la migration réelle qui est partie :

-- profiles: one row per user, tracks plan status
create table profiles (
  id uuid primary key references auth.users(id) on delete cascade,
  plan text not null default 'free',
  stripe_customer_id text,
  created_at timestamptz default now()
);

-- boards: one feedback board per row
create table boards (
  id uuid primary key default gen_random_uuid(),
  owner_id uuid references auth.users(id) on delete cascade not null,
  name text not null,
  slug text unique not null,
  created_at timestamptz default now()
);

-- posts: feature requests on a board
create table posts (
  id uuid primary key default gen_random_uuid(),
  board_id uuid references boards(id) on delete cascade not null,
  title text not null,
  body text,
  vote_count int not null default 0,
  created_at timestamptz default now()
);

-- votes: one vote per user per post
create table votes (
  id uuid primary key default gen_random_uuid(),
  post_id uuid references posts(id) on delete cascade not null,
  voter_id uuid references auth.users(id) on delete cascade not null,
  created_at timestamptz default now(),
  unique (post_id, voter_id)
);

alter table profiles enable row level security;
alter table boards enable row level security;
alter table posts enable row level security;
alter table votes enable row level security;

create policy "Users manage their own profile"
  on profiles for all
  using (auth.uid() = id);

create policy "Boards are publicly readable"
  on boards for select
  using (true);

create policy "Owners manage their own boards"
  on boards for insert, update, delete
  using (auth.uid() = owner_id);

create policy "Posts are publicly readable"
  on posts for select
  using (true);

create policy "Authenticated users create posts"
  on posts for insert
  with check (auth.role() = 'authenticated');

create policy "Voters manage their own votes"
  on votes for all
  using (auth.uid() = voter_id);

-- auto-create a profile row on signup
create or replace function public.handle_new_user()
returns trigger as $$
begin
  insert into public.profiles (id) values (new.id);
  return new;
end;
$$ language plpgsql security definer;

create trigger on_auth_user_created
  after insert on auth.users
  for each row execute function public.handle_new_user();

-- atomic vote increment, called from a Server Action
create or replace function increment_vote(target_post_id uuid)
returns void as $$
begin
  update posts set vote_count = vote_count + 1 where id = target_post_id;
end;
$$ language plpgsql security definer;

Chaque table a la RLS activée et une policy. Boards et posts sont lisibles par n'importe qui, puisque tout l'intérêt d'un feedback board est un lien public, mais seul le propriétaire (ou un votant authentifié, pour les votes) peut écrire dedans. Récupère l'URL du projet et la clé anon dans les réglages API de Supabase et mets-les dans .env.local.

NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key

L'auth avec Supabase

Deux clients Supabase couvrent toute l'app : un qui tourne côté serveur avec la session du visiteur, et un avec la clé service role qui contourne la RLS pour le webhook Stripe plus tard.

// lib/supabase/server.ts
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";

export async function createClient() {
  const cookieStore = await cookies();

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll: () => cookieStore.getAll(),
        setAll: (cookiesToSet) => {
          cookiesToSet.forEach(({ name, value, options }) =>
            cookieStore.set(name, value, options)
          );
        },
      },
    }
  );
}
// lib/supabase/admin.ts
import { createClient as createSupabaseClient } from "@supabase/supabase-js";

export function createAdminClient() {
  return createSupabaseClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.SUPABASE_SERVICE_ROLE_KEY!,
    { auth: { persistSession: false } }
  );
}

proxy.ts (et non middleware.ts, ce nom a disparu dans Next.js 16) protège les routes du dashboard en vérifiant qu'une session existe avant que la requête n'atteigne la page.

// proxy.ts
import { NextResponse, type NextRequest } from "next/server";
import { createServerClient } from "@supabase/ssr";

export async function proxy(request: NextRequest) {
  const response = NextResponse.next({ request });

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll: () => request.cookies.getAll(),
        setAll: (cookiesToSet) => {
          cookiesToSet.forEach(({ name, value, options }) =>
            response.cookies.set(name, value, options)
          );
        },
      },
    }
  );

  const {
    data: { user },
  } = await supabase.auth.getUser();

  if (!user && request.nextUrl.pathname.startsWith("/dashboard")) {
    return NextResponse.redirect(new URL("/login", request.url));
  }

  return response;
}

export const config = {
  matcher: ["/dashboard/:path*"],
};

La page de login elle-même est un simple formulaire email et mot de passe adossé à une Server Action. Rien de sophistiqué, des magic links feraient aussi bien l'affaire si tu préfères te passer complètement de mots de passe.

// app/login/actions.ts
"use server";

import { redirect } from "next/navigation";
import { createClient } from "@/lib/supabase/server";

export async function signIn(formData: FormData) {
  const supabase = await createClient();

  const { error } = await supabase.auth.signInWithPassword({
    email: formData.get("email") as string,
    password: formData.get("password") as string,
  });

  if (error) redirect("/login?error=invalid-credentials");
  redirect("/dashboard");
}

export async function signUp(formData: FormData) {
  const supabase = await createClient();

  const { error } = await supabase.auth.signUp({
    email: formData.get("email") as string,
    password: formData.get("password") as string,
  });

  if (error) redirect("/login?error=signup-failed");
  redirect("/dashboard");
}

Construire la fonctionnalité centrale : boards et upvotes

Le dashboard liste les boards d'un utilisateur et lui permet d'en créer un nouveau. Les comptes gratuits sont plafonnés à un board, imposé dans la Server Action, pas seulement dans l'UI.

// app/dashboard/actions.ts
"use server";

import { createClient } from "@/lib/supabase/server";
import { redirect } from "next/navigation";

export async function createBoard(formData: FormData) {
  const supabase = await createClient();
  const {
    data: { user },
  } = await supabase.auth.getUser();
  if (!user) redirect("/login");

  const { data: profile } = await supabase
    .from("profiles")
    .select("plan")
    .eq("id", user.id)
    .single();

  const { count } = await supabase
    .from("boards")
    .select("id", { count: "exact", head: true })
    .eq("owner_id", user.id);

  if (profile?.plan === "free" && (count ?? 0) >= 1) {
    redirect("/dashboard/billing?limit=reached");
  }

  const name = formData.get("name") as string;
  const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 40);

  await supabase.from("boards").insert({ owner_id: user.id, name, slug });
  redirect("/dashboard");
}

La page publique du board est l'endroit où les params async comptent. Dans Next.js 16, params est une Promise, et await params est obligatoire avant de pouvoir lire le slug.

// app/b/[slug]/page.tsx
import { createClient } from "@/lib/supabase/server";
import { VoteButton } from "./vote-button";
import { notFound } from "next/navigation";

interface PageProps {
  params: Promise<{ slug: string }>;
}

export default async function BoardPage({ params }: PageProps) {
  const { slug } = await params;
  const supabase = await createClient();

  const { data: board } = await supabase
    .from("boards")
    .select("id, name")
    .eq("slug", slug)
    .single();

  if (!board) notFound();

  const { data: posts } = await supabase
    .from("posts")
    .select("id, title, body, vote_count")
    .eq("board_id", board.id)
    .order("vote_count", { ascending: false });

  return (
    <main className="max-w-2xl mx-auto py-12 px-4">
      <h1 className="text-2xl font-bold mb-6">{board.name}</h1>
      <ul className="space-y-3">
        {posts?.map((post) => (
          <li key={post.id} className="flex gap-4 border rounded-lg p-4">
            <VoteButton postId={post.id} initialCount={post.vote_count} />
            <div>
              <p className="font-medium">{post.title}</p>
              {post.body && (
                <p className="text-sm text-muted-foreground">{post.body}</p>
              )}
            </div>
          </li>
        ))}
      </ul>
    </main>
  );
}

Le vote a besoin d'un petit Client Component, puisqu'il réagit à un clic. Le vote lui-même passe par une Server Action pour que la vérification RLS se fasse sur le serveur, pas dans le navigateur.

// app/b/[slug]/vote-button.tsx
"use client";

import { useState, useTransition } from "react";
import { castVote } from "./actions";

export function VoteButton({
  postId,
  initialCount,
}: {
  postId: string;
  initialCount: number;
}) {
  const [count, setCount] = useState(initialCount);
  const [isPending, startTransition] = useTransition();

  return (
    <button
      disabled={isPending}
      onClick={() =>
        startTransition(async () => {
          setCount((c) => c + 1);
          await castVote(postId);
        })
      }
      className="flex flex-col items-center justify-center w-12 h-12 rounded-md border hover:bg-accent"
    >
      <span className="text-sm font-semibold">{count}</span>
    </button>
  );
}
// app/b/[slug]/actions.ts
"use server";

import { createClient } from "@/lib/supabase/server";
import { redirect } from "next/navigation";

export async function castVote(postId: string) {
  const supabase = await createClient();
  const {
    data: { user },
  } = await supabase.auth.getUser();
  if (!user) redirect("/login");

  const { error } = await supabase
    .from("votes")
    .insert({ post_id: postId, voter_id: user.id });

  if (!error) {
    await supabase.rpc("increment_vote", { target_post_id: postId });
  }
}

La contrainte unique (post_id, voter_id) du schéma fait le vrai travail ici. Si un utilisateur vote deux fois, l'insert échoue, le compteur ne s'incrémente pas, et il n'y a besoin d'aucune logique applicative supplémentaire pour empêcher le double vote.

Ajouter Stripe Checkout pour le plan Pro

Crée d'abord un produit et un prix récurrent dans le dashboard Stripe, puis branche le flux de paiement. Démarrer le checkout est une Server Action qui redirige directement vers Stripe.

// app/dashboard/billing/actions.ts
"use server";

import Stripe from "stripe";
import { redirect } from "next/navigation";
import { createClient } from "@/lib/supabase/server";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function startCheckout() {
  const supabase = await createClient();
  const {
    data: { user },
  } = await supabase.auth.getUser();
  if (!user) redirect("/login");

  const session = await stripe.checkout.sessions.create({
    mode: "subscription",
    line_items: [{ price: process.env.STRIPE_PRO_PRICE_ID!, quantity: 1 }],
    success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?upgraded=true`,
    cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard/billing`,
    client_reference_id: user.id,
    metadata: { supabase_user_id: user.id },
  });

  redirect(session.url!);
}

Le plan ne change vraiment qu'une fois que Stripe confirme l'abonnement, via un webhook, et pas sur la redirection de succès. Les redirections peuvent être falsifiées ou interrompues, les webhooks sont la source de vérité.

// app/api/webhooks/stripe/route.ts
import Stripe from "stripe";
import { NextRequest, NextResponse } from "next/server";
import { createAdminClient } from "@/lib/supabase/admin";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;

export async function POST(req: NextRequest) {
  const body = await req.text();
  const signature = req.headers.get("stripe-signature");

  if (!signature) {
    return NextResponse.json({ error: "Missing signature" }, { status: 400 });
  }

  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
  } catch {
    return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
  }

  const supabase = createAdminClient();

  if (event.type === "checkout.session.completed") {
    const session = event.data.object as Stripe.Checkout.Session;
    const userId = session.metadata?.supabase_user_id;

    if (userId) {
      await supabase
        .from("profiles")
        .update({
          plan: "pro",
          stripe_customer_id: session.customer as string,
        })
        .eq("id", userId);
    }
  }

  if (event.type === "customer.subscription.deleted") {
    const subscription = event.data.object as Stripe.Subscription;

    await supabase
      .from("profiles")
      .update({ plan: "free" })
      .eq("stripe_customer_id", subscription.customer as string);
  }

  return NextResponse.json({ received: true });
}

Redirige les événements vers ton serveur local pendant les tests avec la CLI Stripe, et copie le signing secret qu'elle affiche dans STRIPE_WEBHOOK_SECRET.

stripe listen --forward-to localhost:3000/api/webhooks/stripe

La page de facturation elle-même bouge à peine, c'est donc un endroit raisonnable pour utiliser la directive "use cache" qui a remplacé experimental.dynamicIO dans Next.js 16.

// app/pricing/page.tsx
"use cache";

export default function PricingPage() {
  return (
    <main className="max-w-2xl mx-auto py-16 px-4">
      <h1 className="text-3xl font-bold mb-8">Pricing</h1>
      <div className="grid grid-cols-2 gap-6">
        <div className="border rounded-lg p-6">
          <h2 className="font-semibold">Free</h2>
          <p className="text-sm text-muted-foreground">1 board, unlimited posts</p>
        </div>
        <div className="border rounded-lg p-6">
          <h2 className="font-semibold">Pro ($19/mo)</h2>
          <p className="text-sm text-muted-foreground">Unlimited boards</p>
        </div>
      </div>
    </main>
  );
}

Les contrôles qualité avant d'expédier

Deux vérifications tournent avant chaque commit, sans exception.

npx tsc --noEmit
npm run build

Demande à Claude Code de lancer les deux après avoir branché le webhook et le flux de facturation, puisque les types de Stripe sont stricts et faciles à se tromper d'un poil au premier passage.

claude "run tsc --noEmit and fix any type errors, then confirm the build passes"

C'est aussi ici que tu testes vraiment les flux à la main : inscription, création d'un board, atteinte de la limite du plan gratuit, upgrade via le mode test de Stripe, confirmation que le webhook bascule le plan sur pro. Rien de tout ça n'est attrapé par un vérificateur de types. Il faut cliquer dedans.

Déployer sur Vercel

Push le repo sur GitHub, puis importe-le dans Vercel. Renseigne chaque variable d'environnement de .env.local dans le dashboard Vercel avant le premier déploiement, y compris les clés Stripe et la clé service role Supabase.

npx vercel env add SUPABASE_SERVICE_ROLE_KEY production
npx vercel env add STRIPE_SECRET_KEY production
npx vercel env add STRIPE_WEBHOOK_SECRET production
npx vercel --prod

Un truc que les gens ratent : le webhook secret de ta session stripe listen locale est différent de celui que tu obtiens quand tu enregistres un vrai endpoint dans le dashboard Stripe pointé vers ton URL de prod. Crée cet endpoint après le premier déploiement, puis mets à jour STRIPE_WEBHOOK_SECRET dans Vercel avec le nouveau secret et redéploie.

À quoi ressemble un pipeline orchestré

Une seule session Claude Code, comme celle ci-dessus, gère très bien un projet de week-end. Le goulot d'étranglement, c'est toi : relire le plan, lire les policies RLS, cliquer à la main dans le flux de checkout. Ça reste vrai peu importe la qualité de l'agent, et c'est une quantité de relecture raisonnable pour quatre fonctionnalités.

Ça cesse d'être raisonnable dès que tu as vingt fonctionnalités au lieu de quatre. C'est le trou que comble le Code Kit à $29 : un moteur posé sur Claude Code qui lance plan, build, évaluation et test pour chaque fonctionnalité automatiquement, et impose un contrôle qualité (zéro erreur de type, zéro erreur de lint, un build propre) avant que quoi que ce soit ne parte. L'étape npx tsc --noEmit ci-dessus, c'est le même contrôle, lancé une fois à la main. Dans un pipeline, il tourne après chaque fonctionnalité sans que tu aies à le demander.

Posté par @speedy_devv

Continue in Workflow

  • Commerce agentique : comment construire une app que les agents IA peuvent payer
    Un guide en français simple du commerce agentique en 2026 : ce que font x402, ACP et le Machine Payments Protocol, plus un pas-à-pas d'un week-end pour livrer une API payante que les agents IA peuvent acheter.
  • Bonnes pratiques Claude Code
    Cinq habitudes séparent les ingénieurs qui livrent avec Claude Code : les PRDs, les règles CLAUDE.md modulaires, les slash commands personnalisés, les resets /clear, et un état d'esprit d'évolution du système.
  • Le mode auto de Claude Code
    Un second modèle Sonnet examine chaque appel d'outil Claude Code avant qu'il s'exécute. Ce que le mode auto bloque, ce qu'il autorise, et les règles d'autorisation qu'il place dans tes paramètres.
  • Channels, Routines, Teleport, Dispatch
    Les quatre fonctionnalités Claude Code livrées par Anthropic en mars et avril 2026 qui transforment le CLI en une couche de coordination orientée événements, entre téléphone, web et desktop.
  • Claude Code 1M Context in Practice: When Bigger Isn't Better
    The 1M-token context window is GA at flat pricing, but bigger isn't always better. A decision framework, token-cost math, and when to use /compact, subagents, and dynamic workflows instead.
  • How to Build an Admin Dashboard With Claude Code
    Ship an internal admin panel with Claude Code: role-gated routes, a searchable users and orders table with pagination, impersonation-safe RLS, and metrics tiles pulled through a type-safe API.

More from Handbook

  • Best SaaS Boilerplate 2026: The Honest Comparison
    An honest 2026 roundup of the best SaaS boilerplates and starter kits (ShipFast, Makerkit, Supastarter, SaaS Pegasus, Divjoy, open source), with real pricing, stacks, and the trade-off nobody mentions: a boilerplate still leaves you coding.
  • Changelog Claude Code
    Notes de version par version pour Claude Code, du bêta v0.2 à mars 2026. Bare mode, relais de permissions Channels, corrections OAuth, et chaque breaking change.
  • What It Really Costs to Build a SaaS MVP in 2026
    A buyer's cost breakdown for building a SaaS MVP in 2026. Real freelancer, agency, no-code, AI-tool, and DIY numbers with citations, plus where the money actually goes.
  • Templates de prompts qui livrent du code
    Dix recettes de prompts qui livrent du code : scaffolding full-stack, APIs, schémas, tests, refactors, debugging, reviews et CI. Chacun avec les modes d'échec à éviter.

Vous voulez le framework derrière ces projets ?

Obtenez le système Claude Code que nous utilisons pour planifier, construire, tester et livrer des logiciels en production.

Découvrez ce que nous construisons pour les entreprises →
speedy_devvkoen_salo

Test-Driven Development

Make Claude write failing tests from your spec, then implement until green without cheating. How to wire testing into the agent loop so quality is enforced, not hoped for.

Ajouter l'authentification avec Claude Code (Supabase Auth)

Ajoute l'inscription email/mot de passe, l'OAuth Google, les magic links, les routes protégées et la gestion des sessions à une app Next.js 16 avec Claude Code et Supabase Auth.

On this page

Choisir un petit SaaS réel à construire
Avant de commencer
Monter le projet
Écrire CLAUDE.md et AGENTS.md
Planifier le schéma de base de données en Plan mode
Mettre en place Supabase et Postgres
L'auth avec Supabase
Construire la fonctionnalité centrale : boards et upvotes
Ajouter Stripe Checkout pour le plan Pro
Les contrôles qualité avant d'expédier
Déployer sur Vercel
À quoi ressemble un pipeline orchestré

Vous voulez le framework derrière ces projets ?

Obtenez le système Claude Code que nous utilisons pour planifier, construire, tester et livrer des logiciels en production.

Découvrez ce que nous construisons pour les entreprises →