FairCTO
NL·EN·DE
← Alle cases

FairSkills — fairskills.app

A multi-tenant learning platform where every community gets its own subdomain — and the database, not the application, decides who sees what.

2026·Bekijk live
FairSkills — fairskills.app
Next.js 16React 19Supabase RLSMulti-tenantMolliePWATypeScript

De uitdaging

I ran an earlier version of this platform (SkillTransfers, 2019–2022) on AWS. It worked, but it had a flaw I did not want to carry forward: the tenant a request belonged to was supplied by the client, so the server trusted the browser to say which community's data it was allowed to read. That is one bug away from a cross-tenant data leak. The rebuild had to make that class of mistake structurally impossible — while also adding course authoring, peer teaching, bookings and payments.

Wat ik bouwde

A multi-tenant community-learning SaaS: organisations claim a subdomain, brand it, invite members, author courses out of levels and lessons, and let members book one-to-one sessions with peers — with paid enrolment over iDEAL. Each community also gets its own installable PWA, favicon and share image, generated at runtime from its branding.

Stack & waarom

Supabase Postgres + Row-Level Security
The core decision. Every tenant table carries a tenant_id and RLS is the isolation boundary — 51 policies across 23 tables. Even if the application code has a bug, the database refuses to hand back another community's rows. This is precisely what the old platform got wrong.
Next.js 16 proxy (wildcard subdomains)
One deployment serves every community: the proxy reads the Host header, rewrites <slug>.fairskills.app to an internal /t/[tenant]/… route so the visible URL stays clean, and refreshes the Supabase session cookie in the same pass. Adding a community needs no deploy and no DNS change.
React Server Components + server actions
Lesson content is fetched on the server, so video and guide URLs never reach a browser that isn't entitled to them. Mutations run as server actions that check admin rights and then write with the user's own database client — so RLS re-validates every write rather than trusting the check that just ran.
A security_barrier catalogue view
Anonymous visitors need to browse the course catalogue without being able to scrape paid content. Full lesson rows are visible only to admins, enrolled members, or on preview lessons; everyone else reads a catalog_skills view that simply does not contain the video and guide URLs.
Mollie (iDEAL)
The Dutch market pays by iDEAL, not by card. Enrolment runs checkout → webhook → membership, with the webhook as the source of truth so a closed browser tab can never lose a paid enrolment.
Brevo
Transactional mail for invites and booking confirmations — with a deliberate fallback: when no key is configured the UI hands the admin a copyable invite link instead of silently failing.
Per-tenant PWA manifest + icons
Route handlers generate each community's manifest, icons and OpenGraph image from its own branding, so members install what looks like their organisation's own app — all from a single codebase.

Hoe het werkt

  • The service-role key is used only for validated onboarding flows (registration, invite redemption) and never trusts a client-supplied tenant id; every other query runs as the signed-in user, so RLS applies.
  • The content model is tenants → courses → levels → lessons, with separate tables for teachers, enrolments, progress, bookings, coupons and payments — 23 tables, all RLS-protected.
  • Peer booking is availability-constrained: teachers publish their availability and the booking flow only offers slots that actually exist, pinned to Europe/Amsterdam so daylight saving cannot shift a session.
  • Hardest part: writing RLS policies strict enough to isolate tenants while still letting anonymous visitors browse a catalogue, members see their own progress, and admins manage everything — expressed as database policies instead of if-statements scattered across the app.

Resultaat

  • 10 communities running live on their own branded subdomains (webdev, guitar, chess, yoga, cooking and more) from one deployment — a community created through the UI is online instantly, with no deploy and no DNS change.
  • Tenant isolation enforced in the database (51 policies across 23 tables), closing the client-supplied-tenant-id hole the 2019 platform shipped with.
  • Paid enrolment verified end-to-end against Mollie in test mode (iDEAL checkout → webhook → enrolment), including a server-validated free-access coupon.

Mijn rol

Sole architect and engineer — a ground-up rebuild of a platform I ran from 2019 to 2022.

Klaar om AI écht te laten werken?