SF

Next.js · Payload CMS · Neon

SaaS solo : de Payload CMS au client réel

Conçue, développée et déployée en solo : une plateforme de sites web et de gestion commerciale, en production chez un client réel.

1
client en production
8
modules métier
0
mot de passe stocké

Le problème

Livrer une plateforme SaaS complète pour un client réel — site vitrine éditable et back-office métier — seul, sans équipe, avec un budget d'infrastructure minimal et une mise en production rapide à assurer.

L'approche

Architecture découplée : un backend headless Payload CMS expose des APIs REST consommées par un frontend Next.js. L'authentification passe par magic link plutôt que par mot de passe, et PostgreSQL tourne en serverless sur Neon pour éliminer la gestion d'infrastructure. Le back-office couvre un builder de site type Elementor, devis et factures, rendez-vous, dossiers partenaires, espace client, espace agent et messagerie interne.

Extrait de codeauth/magicLink.ts
// Passwordless by design: the client base for this platform is small
// business owners, and every password reset was a support call. A magic
// link removes the credential entirely — there is nothing to leak, forget
// or reuse across sites.
export const requestMagicLink = async (email: string) => {
  // Single-use, short-lived, and stored hashed: a leaked database row
  // cannot be replayed as a login.
  const token = crypto.randomUUID();
  const hash = await sha256(token);

  await payload.create({
    collection: 'login-tokens',
    data: {
      email,
      hash,
      expiresAt: new Date(Date.now() + 10 * 60_000),
      consumedAt: null,
    },
  });

  await sendEmail(email, buildLoginUrl(token));

  // Always the same response, whether or not the address exists.
  // Returning "unknown email" would turn this endpoint into a way to
  // enumerate the customer list.
  return { ok: true };
};

export const consumeMagicLink = async (token: string) => {
  const hash = await sha256(token);
  const record = await findValidToken(hash);
  if (!record) return null;

  // Consume before issuing the session, so a replayed link fails even if
  // two requests arrive at the same moment.
  await payload.update({
    collection: 'login-tokens',
    id: record.id,
    data: { consumedAt: new Date() },
  });

  return createSession(record.email);
};

Le résultat

La plateforme est en production chez un client réel, Déménagement Tunisie, utilisée au quotidien par ses équipes pour éditer le site public et gérer l'activité commerciale — pas un projet vitrine, un outil qui tourne.

Multi-tenant · EF Core · JWTIsolation multi-tenant : isoler sans ralentir l'APIPostgreSQL · SignalR · TestcontainersConcurrence optimiste : le conflit qu'on ne voit pas