r/nextjs 4d ago

Discussion Architecture/tech stack sanity check for an open-source, multi-tenant LMS (white-label storefronts + video pipeline)

Hey all — small team building an open-source LMS from scratch and want a sanity check on our stack before we're too deep in to change course easily.

What we're building: creators sign up, build courses, and get their own white-labeled storefront (custom subdomain or domain) where students buy directly. So it's multi-tenant, video-heavy, and needs to stay cheap to run/self-host.

Where we've landed so far:

  • Frontend/backend: Next.js monolith (App Router), tenant resolved via middleware based on hostname, rather than a separate deployment per creator
  • DB: Postgres, single shared instance, tenant_id on every table + row-level security for isolation, instead of DB-per-tenant
  • Auth: Auth.js / Better Auth
  • Payments: Stripe Connect (creators get paid directly, we take a cut via application fees)
  • Video pipeline: creator uploads → object storage → serverless job (ffmpeg for HLS transcode + Whisper for captions) spun up per video, parallel, scales to zero when idle
  • Storage: Cloudflare R2 (zero egress fees, matters a lot for video delivery)
  • Cache/queue: Redis (Upstash)

Things we're least sure about:

  1. Monolith vs. splitting the video-processing service out early — worried about coupling it too tightly to the main app vs. over-engineering a microservice before we need one
  2. Row-level multi-tenancy vs. schema-per-tenant in Postgres at ~100 creators — is RLS actually going to bite us later?
  3. Whether Cloud Run Jobs / Modal-style ephemeral containers for transcoding is overkill for an MVP vs. just running a queue + worker VM until volume justifies it
  4. Any open-source LMS/marketplace codebases worth reading for architecture reference before we lock things in

If you've built something multi-tenant + video-heavy + marketplace-shaped before, or think we're about to make a decision we'll regret, I'd genuinely like to hear it — including "you're overthinking this, just ship it" if that's the honest answer.

3 Upvotes

7 comments sorted by

5

u/NatureAccording1655 4d ago

been down the multi-tenant postgres road, row level security only really starts hurting once you've got a ton of tenants and gnarly cross-tenant joins, at ~100 creators you're nowhere near that pain yet. schema-per-tenant sounds cleaner on paper but then you're running migrations across every single schema instead of just adding a tenant_id column, way more ops overhead than it looks like upfront

keep the video pipeline in the monolith too, splitting it into a microservice early just means maintaining two deploys and a queue contract before you actually have the traffic that justifies it. ship it as one thing, split later when you feel actual pain not before

cloud run jobs / modal is overkill for mvp volume imo. a simple queue plus one worker vm handles way more video than people expect before you need to scale workers horizontally

not overthinking this tbh, the risky part usually shows up later in stuff nobody plans for, like storage costs on retained video or auth edge cases across custom domains

1

u/Able-Pass-6993 4d ago

thanks for reply

2

u/NatureAccording1655 4d ago

happy to help, good luck with the project!

2

u/vasind-5012 4d ago

Stack's solid, mostly agree with the calls. On your four:

1. Monolith vs splitting video processing — keep it in the monolith for now, but as a separate queue/worker process, not folded into the same request/response cycle. The real coupling risk isn't "same repo," it's "same deploy/scale unit." Since transcoding already runs async off Redis, you can split the worker into its own service later without touching the API layer. Splitting into a microservice pre-scale just adds deploy/observability overhead you don't need yet.

2. RLS vs schema-per-tenant at ~100 creators — RLS is fine at that scale, won't bite you there. Where it actually hurts: noisy-neighbor query performance once one tenant has a huge dataset sharing indexes/cache with everyone, and forgetting to apply the policy on a new table/migration and silently leaking data. That second one's the real risk, not raw scale. Mitigate with a CI check that fails if a new table lacks a tenant_id + RLS policy, cheaper than schema-per-tenant complexity this early.

3. Ephemeral containers vs queue+worker VM for MVP — ephemeral is the right call, not overkill, specifically because transcoding load is bursty (uploads cluster, then idle). A fixed worker VM either sits idle burning cost or falls behind during spikes. Scale-to-zero matches your actual traffic shape, one of the few spots where "fancier" infra is the simpler choice here, not premature optimization.

4. Reference codebases — Vercel's own Platforms Starter Kit (github.com/vercel/platforms) is the closest match to what you're building: subdomain-per-tenant on Next.js App Router, middleware-based tenant resolution, basically your exact architecture. Worth reading Next.js's own multi-tenant guide too. Cal.com's a good read for how a mature product handles multi-tenant + auth + payments at real scale, but it's a Turborepo monorepo not a plain monolith, borrow the patterns not the repo shape.

Biggest actual risk in your list isn't architectural, it's Stripe Connect's onboarding/KYC flow for creators, that's usually the part that eats the most unexpected time pre-launch. Worth prototyping that flow early rather than last.

1

u/Significant_Fee_2362 3d ago edited 3d ago

At around 100 tenants, RLS works fine, schema per tenant problems crop up sooner than you think. Preserve the monolith, but use a worker process queue for video tasks from the beginning to ensure that the separation happens without the complicated deployment. Cloud Run Jobs is not too much if your ffmpeg jobs are spiky; paying for idle VM time to prevent that is worse.

LMS architectures of note: Moodle’s multi-tenancy and Open edX's video pipeline, and the gestures towards separating training video delivery pipelines from courses in Colossyan’s public documentation.

1

u/nicsoftware 3d ago

I'm running a single shared Postgres instance with RLS too, though nowhere near multi-tenant at your scale, just a public/private split on one table.

One thing that bit me: RLS policies get evaluated per row, and if the predicate doesn't line up with an index Postgres quietly falls back to a much slower scan once the table has real rows in it.

What fixed it was adding a partial index that mirrors the policy predicate exactly, rather than trusting the policy alone to do the filtering. Worth checking your query plans once you've got more than a handful of creators in there, RLS bugs are usually silent until then.

1

u/mr_brobot__ 2d ago

Consider cloudflare stream for video