Next.js · Payload CMS · Neon
Solo SaaS: from Payload CMS to a real client
Designed, built and deployed alone: a website-builder and business-management platform, live for a real client.
- 1
- client in production
- 8
- business modules
- 0
- passwords stored
The problem
Deliver a complete SaaS platform for a real client — an editable public site and a business back-office — alone, with no team, a minimal infrastructure budget and a fast path to production.
The approach
Decoupled architecture: a headless Payload CMS backend exposes REST APIs consumed by a Next.js frontend. Authentication runs on passwordless magic links instead of passwords, and PostgreSQL runs serverless on Neon to remove infrastructure management. The back-office covers an Elementor-like site builder, quotes and invoices, appointments, partner files, a client area, an agent area and internal messaging.
// 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);
};The result
The platform is live for a real client, Déménagement Tunisie, used daily by its team to edit the public site and run the business — not a showcase project, a tool that runs.