{"id":2251,"date":"2026-07-23T23:05:09","date_gmt":"2026-07-23T21:05:09","guid":{"rendered":"https:\/\/kindsonthegenius.com\/blog\/multi-tenancy-in-saas-explained-alkademy-press-case-study\/"},"modified":"2026-07-23T23:05:14","modified_gmt":"2026-07-23T21:05:14","slug":"multi-tenancy-in-saas-explained-alkademy-press-case-study","status":"publish","type":"post","link":"https:\/\/kindsonthegenius.com\/blog\/multi-tenancy-in-saas-explained-alkademy-press-case-study\/","title":{"rendered":"Multi-Tenancy in SaaS Explained \u2014 Patterns, Trade-offs, and Alkademy Press Case Study"},"content":{"rendered":"<p><!-- ktg-updated-banner --><\/p>\n<div class=\"ktg-updated-banner\" style=\"margin:1em 0;padding:0.75em 1em;background:#eff6ff;border-left:4px solid #3b82f6;border-radius:4px;\">\n<p><strong>July 23, 2026:<\/strong> A practical guide to multi-tenancy strategies, with a real case study of how <strong>Alkademy Press<\/strong> isolates publishing organisations on a shared PostgreSQL database.<\/p>\n<\/div>\n<p><strong>Multi-tenancy<\/strong> is how modern SaaS products serve many customers from one application \u2014 without building a separate install for every organisation. Get it right and you ship faster, keep costs predictable, and still protect each customer\u2019s data. Get it wrong and you risk cross-tenant leaks, painful migrations, or an ops bill that never stops growing.<\/p>\n<p>This guide explains what multi-tenancy is, compares the main <strong>implementation methods<\/strong>, and walks through a real product: <a href=\"https:\/\/www.alkademy.com\/solutions\/alkademy-press\" target=\"_blank\" rel=\"noopener noreferrer\"><strong>Alkademy Press<\/strong><\/a> \u2014 Alkademy\u2019s journal management platform for academic publishers. You will see which method Press chose, why it fits, and how tenant context flows from browser request to database query.<\/p>\n<p><strong>Who this is for:<\/strong> backend and SaaS engineers choosing a tenancy model. <strong>Depth:<\/strong> conceptual patterns plus light code snippets (no full project walkthrough). <strong>Reading time:<\/strong> about 15\u201320 minutes.<\/p>\n<p><!-- ktg-article-toc --><\/p>\n<nav class=\"ktg-article-toc\" aria-label=\"Multi-tenancy article table of contents\" style=\"margin:1.5em 0;padding:1em 1.25em;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;\">\n<h2>Table of Contents<\/h2>\n<ol>\n<li><a href=\"#what-is-multi-tenancy\">What is multi-tenancy?<\/a><\/li>\n<li><a href=\"#why-it-is-hard\">Why multi-tenancy is hard<\/a><\/li>\n<li><a href=\"#implementation-methods\">Implementation methods compared<\/a><\/li>\n<li><a href=\"#how-to-choose\">How to choose a method<\/a><\/li>\n<li><a href=\"#tenant-context\">Resolving tenant context on every request<\/a><\/li>\n<li><a href=\"#case-study-alkademy-press\">Case study: Alkademy Press<\/a><\/li>\n<li><a href=\"#what-press-chose\">What method Alkademy Press uses<\/a><\/li>\n<li><a href=\"#press-features\">Multi-tenant features in Alkademy Press<\/a><\/li>\n<li><a href=\"#pitfalls\">Common pitfalls and how to harden<\/a><\/li>\n<li><a href=\"#faq\">Frequently asked questions<\/a><\/li>\n<li><a href=\"#next-steps\">Next steps<\/a><\/li>\n<\/ol>\n<\/nav>\n<h2 id=\"what-is-multi-tenancy\">What Is Multi-Tenancy?<\/h2>\n<p><strong>Multi-tenancy<\/strong> means a single running product serves many independent customers (tenants). Each tenant feels like they have their own system \u2014 their users, data, branding, and often their own plan limits \u2014 while the vendor runs one codebase and shared infrastructure.<\/p>\n<p>Contrast that with <strong>single-tenant<\/strong> (or multi-instance) deployments: one customer, one dedicated stack. Single-tenant can be simpler for extreme compliance or heavy customization, but it does not scale operationally when you have hundreds of organisations.<\/p>\n<p>In B2B SaaS, a tenant is usually an <strong>organisation<\/strong>, not a single person. Inside that organisation you still have roles (admin, editor, author) and nested resources (projects, workspaces, journals). Multi-tenancy is the outer boundary: <em>Organisation A must never see Organisation B\u2019s data<\/em>.<\/p>\n<p>A useful mental model:<\/p>\n<ul>\n<li><strong>Platform<\/strong> \u2014 your company, shared catalog, billing engine, platform admins<\/li>\n<li><strong>Tenant<\/strong> \u2014 the paying organisation (university press, institute, consortium)<\/li>\n<li><strong>Resources<\/strong> \u2014 everything that belongs to that organisation (users, journals, submissions, invoices)<\/li>\n<\/ul>\n<p>Do not confuse <strong>tenants<\/strong> with <strong>users<\/strong>. A tenant is the billing and isolation boundary. A user is a person who acts inside that boundary (and sometimes inside several tenants over time). Good SaaS products model both explicitly: organisations own data; people get memberships and roles.<\/p>\n<p>Alkademy Press follows exactly that shape: platform above, organisations as tenants, journals and editorial workflows underneath. We return to that case study after the pattern comparison.<\/p>\n<h2 id=\"why-it-is-hard\">Why Multi-Tenancy Is Hard<\/h2>\n<p>Multi-tenancy is not just a <code>tenant_id<\/code> column. It cuts across architecture, security, product, and operations:<\/p>\n<ul>\n<li><strong>Data isolation<\/strong> \u2014 every query, file download, cache key, and background job must know which tenant it belongs to. One missing filter can leak data across customers.<\/li>\n<li><strong>Shared vs custom<\/strong> \u2014 tenants want branding, domains, and plan limits; you still want one product to evolve. Too much customization recreates single-tenant complexity.<\/li>\n<li><strong>Noisy neighbors<\/strong> \u2014 one heavy tenant can saturate a shared database or queue unless you meter usage and enforce limits.<\/li>\n<li><strong>Compliance<\/strong> \u2014 some customers demand dedicated databases, region pinning, or exportable backups. Your tenancy model either supports that or forces a hybrid later.<\/li>\n<li><strong>Blast radius<\/strong> \u2014 a bad migration or outage hits every tenant on shared infrastructure. Isolation strategy is also a risk strategy.<\/li>\n<\/ul>\n<p>Isolation is a <strong>spectrum<\/strong>, not a boolean. The methods below trade isolation strength against cost and operational complexity.<\/p>\n<h2 id=\"implementation-methods\">Implementation Methods Compared<\/h2>\n<p>There are three classic database strategies for multi-tenancy. Most SaaS products pick one as the default and optionally offer a stronger tier for enterprise customers (hybrid).<\/p>\n<figure style=\"margin:1.5em 0;text-align:center;\">\n<img decoding=\"async\" src=\"https:\/\/kindsonthegenius.com\/blog\/wp-content\/uploads\/2026\/07\/multi-tenancy-strategies-comparison.png\" alt=\"Comparison of multi-tenancy strategies: shared database with tenant_id, schema-per-tenant, and database-per-tenant\" width=\"860\" height=\"420\" loading=\"lazy\" style=\"max-width:100%;height:auto;border-radius:8px;border:1px solid #dee2e6;\" \/><figcaption style=\"font-size:0.85em;color:#666;margin-top:0.5em;\">Three core strategies \u2014 Alkademy Press uses shared database with a tenant discriminator column<\/figcaption><\/figure>\n<h3 id=\"method-a\">Method A \u2014 Shared database, shared schema, <code>tenant_id<\/code> (row-level)<\/h3>\n<p>All tenants live in the <strong>same database and same tables<\/strong>. Almost every business row carries a discriminator such as <code>tenantId<\/code>. Application code (or database policies) ensures queries always filter by that column.<\/p>\n<p><strong>Pros:<\/strong> lowest infrastructure cost, simplest migrations, easiest analytics across tenants, natural fit for identical product schemas.<\/p>\n<p><strong>Cons:<\/strong> isolation depends on discipline \u2014 a forgotten <code>WHERE tenant_id = ?<\/code> is a security bug; noisy neighbors share indexes and connection pools.<\/p>\n<p><strong>Best for:<\/strong> most B2B SaaS at early and mid scale; products where every tenant uses the same domain model.<\/p>\n<p>Light shape of the data model:<\/p>\n<pre><code class=\"language-prisma\">model Tenant {\n  id     String @id @default(uuid())\n  slug   String @unique\n  name   String\n  plan   String @default(\"free\")\n  status String @default(\"active\")\n}\n\nmodel User {\n  id       String @id @default(uuid())\n  tenantId String\n  email    String\n  name     String\n\n  @@unique([tenantId, email])\n  @@index([tenantId])\n}\n\nmodel Submission {\n  id       String @id @default(uuid())\n  tenantId String\n  \/\/ ... workflow fields\n\n  @@index([tenantId])\n}\n<\/code><\/pre>\n<p>And the corresponding query pattern:<\/p>\n<pre><code class=\"language-typescript\">\/\/ Always scope by both resource id AND tenant\nconst submission = await prisma.submission.findFirst({\n  where: { id: submissionId, tenantId },\n});\n<\/code><\/pre>\n<h3 id=\"method-b\">Method B \u2014 Shared database, schema-per-tenant<\/h3>\n<p>Still one database server, but each tenant gets a <strong>separate schema<\/strong> (for example <code>tenant_acme<\/code>, <code>tenant_northgate<\/code>). Connection or session settings point queries at the correct schema.<\/p>\n<p><strong>Pros:<\/strong> stronger logical separation; easier to drop or export one tenant; fewer \u201cforgot the filter\u201d bugs inside a schema.<\/p>\n<p><strong>Cons:<\/strong> migrations must run across every schema; connection routing and pooling get harder; tooling is less convenient than a single schema.<\/p>\n<p><strong>Best for:<\/strong> mid-market products with moderate tenant counts and stronger isolation needs without full DB silos.<\/p>\n<h3 id=\"method-c\">Method C \u2014 Database-per-tenant<\/h3>\n<p>Each tenant (or each enterprise customer) gets a <strong>dedicated database<\/strong> \u2014 sometimes even a dedicated cluster. The app selects a connection string from a tenant registry.<\/p>\n<p><strong>Pros:<\/strong> strongest isolation, per-tenant backup\/restore, clearer compliance story, tunable performance per customer.<\/p>\n<p><strong>Cons:<\/strong> highest ops cost; schema migrations become a fleet problem; you need solid automation for provisioning, monitoring, and connection management.<\/p>\n<p><strong>Best for:<\/strong> enterprise \/ regulated offerings, or a premium \u201cprivate instance\u201d tier alongside a shared pool.<\/p>\n<h3 id=\"method-hybrid\">Method D \u2014 Hybrid and extras<\/h3>\n<p>Many mature platforms combine approaches:<\/p>\n<ul>\n<li>Shared DB for standard plans; dedicated DB for enterprise<\/li>\n<li><strong>Row-Level Security (RLS)<\/strong> in PostgreSQL as defense-in-depth on top of Method A<\/li>\n<li><strong>Cell-based<\/strong> architecture \u2014 groups of tenants on isolated \u201ccells\u201d of infra for blast-radius control<\/li>\n<\/ul>\n<p>These are evolutions. You rarely need them on day one if Method A is implemented carefully.<\/p>\n<h2 id=\"how-to-choose\">How to Choose a Method<\/h2>\n<table style=\"width:100%;border-collapse:collapse;margin:1.25em 0;\">\n<thead>\n<tr style=\"background:#f8fafc;\">\n<th style=\"border:1px solid #e2e8f0;padding:0.6em;text-align:left;\">Question<\/th>\n<th style=\"border:1px solid #e2e8f0;padding:0.6em;text-align:left;\">Lean toward<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"border:1px solid #e2e8f0;padding:0.6em;\">Hundreds\/thousands of similar tenants?<\/td>\n<td style=\"border:1px solid #e2e8f0;padding:0.6em;\">Method A (shared DB + <code>tenant_id<\/code>)<\/td>\n<\/tr>\n<tr>\n<td style=\"border:1px solid #e2e8f0;padding:0.6em;\">Need easy per-tenant export\/delete?<\/td>\n<td style=\"border:1px solid #e2e8f0;padding:0.6em;\">B or C (or A with careful tooling)<\/td>\n<\/tr>\n<tr>\n<td style=\"border:1px solid #e2e8f0;padding:0.6em;\">Contracts demand dedicated data stores?<\/td>\n<td style=\"border:1px solid #e2e8f0;padding:0.6em;\">Method C (or hybrid)<\/td>\n<\/tr>\n<tr>\n<td style=\"border:1px solid #e2e8f0;padding:0.6em;\">Small team, one product schema?<\/td>\n<td style=\"border:1px solid #e2e8f0;padding:0.6em;\">Method A<\/td>\n<\/tr>\n<tr>\n<td style=\"border:1px solid #e2e8f0;padding:0.6em;\">Heavy per-tenant customization of schema?<\/td>\n<td style=\"border:1px solid #e2e8f0;padding:0.6em;\">Revisit product design first \u2014 then B\/C<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Rule of thumb: <strong>start with Method A unless compliance or enterprise sales force you higher.<\/strong> You can always offer a dedicated database later for a premium tier; starting on Method C for every free trial customer is how teams drown in ops work.<\/p>\n<p>Also decide early what \u201cdelete tenant\u201d means: soft-suspend, hard purge, or export-then-delete. Method A makes purge a careful multi-table cleanup. Methods B and C make drop-schema \/ drop-database tempting \u2014 still verify backups and legal retention first.<\/p>\n<p>Alkademy Press made the Method A choice deliberately \u2014 covered next after tenant resolution.<\/p>\n<h2 id=\"tenant-context\">Resolving Tenant Context on Every Request<\/h2>\n<p>Regardless of database strategy, every request must answer: <em>which tenant is this?<\/em> Common signals:<\/p>\n<ol>\n<li><strong>Explicit header<\/strong> \u2014 e.g. <code>X-Tenant-Id<\/code> from a logged-in SPA<\/li>\n<li><strong>Subdomain<\/strong> \u2014 <code>northgate.yourproduct.com<\/code><\/li>\n<li><strong>Custom domain<\/strong> \u2014 <code>journal.university.edu<\/code> mapped to a tenant<\/li>\n<li><strong>JWT \/ session claim<\/strong> \u2014 <code>tenant_id<\/code> embedded at login<\/li>\n<li><strong>Path<\/strong> \u2014 <code>\/t\/{slug}\/...<\/code> (less common for APIs)<\/li>\n<\/ol>\n<p>Production systems often support several signals with a clear <strong>priority order<\/strong>. The browser app may always send a header, while public journal sites resolve via custom domain. Auth still carries a claim so tokens cannot wander across organisations.<\/p>\n<figure style=\"margin:1.5em 0;text-align:center;\">\n<img decoding=\"async\" src=\"https:\/\/kindsonthegenius.com\/blog\/wp-content\/uploads\/2026\/07\/alkademy-press-tenant-resolution-flow.png\" alt=\"Alkademy Press tenant resolution flow from HTTP request through middleware to scoped database query\" width=\"860\" height=\"400\" loading=\"lazy\" style=\"max-width:100%;height:auto;border-radius:8px;border:1px solid #dee2e6;\" \/><figcaption style=\"font-size:0.85em;color:#666;margin-top:0.5em;\">Request \u2192 resolve tenant \u2192 attach context \u2192 query with id + tenantId<\/figcaption><\/figure>\n<h2 id=\"case-study-alkademy-press\">Case Study: Alkademy Press<\/h2>\n<p><strong>Alkademy Press<\/strong> is a multi-tenant <strong>Journal Management System<\/strong> for academic publishing. University presses, research institutes, and consortia run submissions, peer review, editorial workflows, and public journal websites from one SaaS product \u2014 instead of installing and maintaining a separate journal stack per organisation.<\/p>\n<p>That product shape is a textbook multi-tenancy problem: dozens or hundreds of organisations, nearly identical workflows, strong expectations that Press A never sees Press B\u2019s manuscripts, and a need for self-serve branding (logos, colors, custom domains) without forking the codebase.<\/p>\n<p>Tenant hierarchy in Press:<\/p>\n<figure style=\"margin:1.5em 0;text-align:center;\">\n<img decoding=\"async\" src=\"https:\/\/kindsonthegenius.com\/blog\/wp-content\/uploads\/2026\/07\/alkademy-press-multi-tenant-hierarchy.png\" alt=\"Alkademy Press multi-tenant hierarchy showing platform, organisation tenants, journals, and scoped workflows\" width=\"820\" height=\"440\" loading=\"lazy\" style=\"max-width:100%;height:auto;border-radius:8px;border:1px solid #dee2e6;\" \/><figcaption style=\"font-size:0.85em;color:#666;margin-top:0.5em;\">Platform \u2192 Tenant (organisation) \u2192 Journal(s) \u2192 submissions, reviews, members<\/figcaption><\/figure>\n<ul>\n<li><strong>Platform<\/strong> \u2014 Alkademy operators; shared website templates; feature flags; tenant provisioning<\/li>\n<li><strong>Tenant<\/strong> \u2014 the publishing organisation (name, slug, plan, status, Stripe customer)<\/li>\n<li><strong>Journal<\/strong> \u2014 one or more journals per tenant, with branding, APC settings, optional custom domain<\/li>\n<li><strong>Workflow data<\/strong> \u2014 members, submissions, review rounds, volumes\/issues, audit logs \u2014 all tenant-scoped<\/li>\n<\/ul>\n<p>Users are unique per tenant (<code>tenantId + email<\/code>), so the same person can belong to different organisations with separate memberships and roles. Roles span platform admin down through tenant admin, editor, reviewer, and author.<\/p>\n<h2 id=\"what-press-chose\">What Method Alkademy Press Uses<\/h2>\n<p><strong>Alkademy Press uses Method A: shared PostgreSQL database, shared schema, row-level multi-tenancy via a <code>tenantId<\/code> discriminator.<\/strong><\/p>\n<p>That was an explicit architecture decision early in the product: one database, tenant isolation enforced by tagging domain rows and filtering in application services \u2014 not schema-per-tenant, not database-per-tenant, and not Postgres Row-Level Security as the primary control.<\/p>\n<p>Why that fit Press:<\/p>\n<ul>\n<li>Every organisation runs the <strong>same journal\/editorial domain model<\/strong><\/li>\n<li>SaaS economics matter \u2014 many presses on shared infra<\/li>\n<li>A NestJS + Prisma stack stays simple when there is one schema to migrate<\/li>\n<li>Plan limits, branding, and custom domains provide differentiation without siloed databases<\/li>\n<\/ul>\n<h3>Tenant resolution in Press<\/h3>\n<p>Incoming requests resolve tenant context in this priority order:<\/p>\n<ol>\n<li><code>X-Tenant-Id<\/code> header (highest) \u2014 the React app sends the logged-in user\u2019s tenant UUID on API calls<\/li>\n<li><strong>Platform subdomain<\/strong> \u2014 host under the product domain suffix<\/li>\n<li><strong>Custom domain<\/strong> \u2014 look up the journal\u2019s configured domain, then attach that journal\u2019s <code>tenantId<\/code><\/li>\n<li><strong>JWT claim <code>tenant_id<\/code><\/strong> \u2014 backfilled during authentication if still unset<\/li>\n<\/ol>\n<p>Conceptual middleware (simplified):<\/p>\n<pre><code class=\"language-typescript\">async use(req, _res, next) {\n  const headerTenantId = req.headers['x-tenant-id'];\n  if (headerTenantId) {\n    req.tenantId = headerTenantId;\n    return next();\n  }\n\n  const host = (req.headers.host ?? '').split(':')[0];\n\n  if (host.endsWith(domainSuffix)) {\n    const subdomain = host.slice(0, -domainSuffix.length);\n    if (subdomain &amp;&amp; !subdomain.includes('.')) {\n      req.tenantId = subdomain;\n      return next();\n    }\n  }\n\n  const journal = await prisma.journal.findFirst({\n    where: { customDomain: host },\n    select: { tenantId: true },\n  });\n  if (journal) {\n    req.tenantId = journal.tenantId;\n    return next();\n  }\n\n  \/\/ JWT strategy may still attach tenant_id after auth\n  next();\n}\n<\/code><\/pre>\n<p>On the client, the access token and tenant id travel together:<\/p>\n<pre><code class=\"language-typescript\">api.interceptors.request.use((config) => {\n  const token = authStore.accessToken;\n  const tenantId = authStore.user?.tenantId;\n  if (token) config.headers['Authorization'] = `Bearer ${token}`;\n  if (tenantId) config.headers['X-Tenant-Id'] = tenantId;\n  return config;\n});\n<\/code><\/pre>\n<h3>Isolation in practice<\/h3>\n<p>Controllers pass <code>req.tenantId<\/code> into services. Services do not look up a submission \u201cby id alone\u201d \u2014 they look it up by <strong>id and tenant<\/strong>. That pattern repeats across journals, members, reviews, and billing usage events.<\/p>\n<pre><code class=\"language-typescript\">const submission = await prisma.submission.findFirst({\n  where: { id, tenantId },\n});\nif (!submission) throw new NotFoundException('Submission', id);\n<\/code><\/pre>\n<p>Returning \u201cnot found\u201d (instead of \u201cforbidden\u201d) for cross-tenant ids avoids leaking whether a resource exists in another organisation.<\/p>\n<h3>What is shared vs isolated in Press<\/h3>\n<table style=\"width:100%;border-collapse:collapse;margin:1.25em 0;\">\n<thead>\n<tr style=\"background:#f8fafc;\">\n<th style=\"border:1px solid #e2e8f0;padding:0.6em;text-align:left;\">Resource<\/th>\n<th style=\"border:1px solid #e2e8f0;padding:0.6em;text-align:left;\">Shared or isolated?<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"border:1px solid #e2e8f0;padding:0.6em;\">PostgreSQL database &amp; schema<\/td>\n<td style=\"border:1px solid #e2e8f0;padding:0.6em;\">Shared<\/td>\n<\/tr>\n<tr>\n<td style=\"border:1px solid #e2e8f0;padding:0.6em;\">Journals, submissions, reviews, audit logs<\/td>\n<td style=\"border:1px solid #e2e8f0;padding:0.6em;\">Isolated by <code>tenantId<\/code><\/td>\n<\/tr>\n<tr>\n<td style=\"border:1px solid #e2e8f0;padding:0.6em;\">Users &amp; roles<\/td>\n<td style=\"border:1px solid #e2e8f0;padding:0.6em;\">Isolated per tenant<\/td>\n<\/tr>\n<tr>\n<td style=\"border:1px solid #e2e8f0;padding:0.6em;\">Stripe billing &amp; plan meters<\/td>\n<td style=\"border:1px solid #e2e8f0;padding:0.6em;\">Per tenant<\/td>\n<\/tr>\n<tr>\n<td style=\"border:1px solid #e2e8f0;padding:0.6em;\">Website templates (catalog)<\/td>\n<td style=\"border:1px solid #e2e8f0;padding:0.6em;\">Shared platform catalog; adopted per journal<\/td>\n<\/tr>\n<tr>\n<td style=\"border:1px solid #e2e8f0;padding:0.6em;\">App servers, Redis queues, object storage stack<\/td>\n<td style=\"border:1px solid #e2e8f0;padding:0.6em;\">Shared infra; logical isolation in app<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Suspended tenants are blocked at login when status is not active \u2014 another tenancy control that sits above raw SQL filters.<\/p>\n<h2 id=\"press-features\">Multi-Tenant Features in Alkademy Press<\/h2>\n<p>Row-level tenancy is the foundation. Product features built on top of it include:<\/p>\n<ol>\n<li><strong>Organisation provisioning<\/strong> \u2014 platform admins create tenants (name, slug, plan) and optional tenant admins.<\/li>\n<li><strong>RBAC<\/strong> \u2014 platform_admin, tenant admins, editors, reviewers, authors \u2014 claims stay within the active tenant.<\/li>\n<li><strong>Per-journal branding<\/strong> \u2014 logos, banners, color themes, and publishable website templates.<\/li>\n<li><strong>Routing flexibility<\/strong> \u2014 subdomain-style hosts, custom domains for public journal sites, and path\/slug public URLs.<\/li>\n<li><strong>Plan limits<\/strong> \u2014 free \/ starter \/ pro \/ enterprise meters (journals, submissions per month, AI screenings, storage).<\/li>\n<li><strong>Impersonation<\/strong> \u2014 platform support can open a session as a user inside a target tenant when needed.<\/li>\n<li><strong>Feature flags &amp; announcements<\/strong> \u2014 global defaults with per-tenant overrides or targeting.<\/li>\n<li><strong>Audit trail<\/strong> \u2014 tenant-scoped audit logs for editorial and admin actions.<\/li>\n<\/ol>\n<p>That mix is typical of healthy Method A products: <strong>shared core, tenant-scoped data, tenant-visible customization at the edges.<\/strong><\/p>\n<h2 id=\"pitfalls\">Common Pitfalls and How to Harden<\/h2>\n<p>Whether you build the next Alkademy Press or a different SaaS, watch for these Method A failure modes:<\/p>\n<ul>\n<li><strong>Missing tenant filter<\/strong> \u2014 any <code>findUnique({ where: { id } })<\/code> on tenant data is suspect. Prefer composite checks (<code>id + tenantId<\/code>) or automated ORM scoping.<\/li>\n<li><strong>IDOR across tenants<\/strong> \u2014 authorization that checks \u201cuser is editor\u201d without also checking \u201cresource.tenantId === currentTenantId\u201d.<\/li>\n<li><strong>Cache and file keys<\/strong> \u2014 Redis keys and storage paths should include tenant (or be reachable only through tenant-checked DB rows).<\/li>\n<li><strong>Background jobs<\/strong> \u2014 enqueue <code>tenantId<\/code> with the job payload; never infer it from ambient global state alone.<\/li>\n<li><strong>Ambiguous host resolution<\/strong> \u2014 if subdomains carry slugs but foreign keys store UUIDs, map slug \u2192 id explicitly before querying.<\/li>\n<li><strong>Analytics shortcuts<\/strong> \u2014 admin \u201call tenants\u201d reports must be platform-only routes, never reused by tenant APIs.<\/li>\n<\/ul>\n<p>Hardening path as you mature:<\/p>\n<ol>\n<li>Lint \/ code review checklists for tenant filters<\/li>\n<li>Integration tests that attempt cross-tenant reads and expect 404<\/li>\n<li>Optional Prisma (or ORM) middleware that auto-injects <code>tenantId<\/code><\/li>\n<li>Optional PostgreSQL RLS as defense-in-depth for the same discriminator column<\/li>\n<\/ol>\n<p>Alkademy Press today relies on <strong>application-level filtering<\/strong> \u2014 clear, explicit, and sufficient for a disciplined codebase \u2014 with room to add automated scoping later without changing the underlying Method A model.<\/p>\n<p>One more practical tip: treat platform-admin \u201cview all tenants\u201d screens as a separate surface with separate authorization. Reusing a tenant list API without a platform role check is how internal tools accidentally become cross-tenant data dumps. In Press, platform concerns (provisioning, impersonation, global flags) sit above the tenant app, not beside it with weaker guards.<\/p>\n<h2 id=\"faq\">Frequently Asked Questions<\/h2>\n<p><strong>What is multi-tenancy in SaaS?<\/strong><br \/>\nMulti-tenancy is an architecture where one application instance serves many customer organisations (tenants). Each tenant\u2019s users and data are logically isolated while infrastructure and codebase stay shared.<\/p>\n<p><strong>What are the main multi-tenancy implementation methods?<\/strong><br \/>\nThe three core methods are: (A) shared database with a <code>tenant_id<\/code> column, (B) schema-per-tenant in one database, and (C) database-per-tenant. Hybrids and Postgres RLS build on these.<\/p>\n<p><strong>Which multi-tenancy method does Alkademy Press use?<\/strong><br \/>\nAlkademy Press uses Method A \u2014 a shared PostgreSQL database and shared schema, with row-level isolation via <code>tenantId<\/code> on domain tables. Tenant context is resolved from header, subdomain, custom domain, or JWT, and services filter every query by tenant.<\/p>\n<p><strong>Is shared-database multi-tenancy secure?<\/strong><br \/>\nYes, when every code path enforces tenant scope and you test for cross-tenant access. Security depends on engineering discipline (and optionally RLS). It is the industry-standard approach for most SaaS products.<\/p>\n<p><strong>When should I use database-per-tenant instead?<\/strong><br \/>\nChoose it when contracts, compliance, or performance isolation require a dedicated store \u2014 or as an enterprise tier beside a shared pool for standard customers.<\/p>\n<p><strong>How do you resolve which tenant a request belongs to?<\/strong><br \/>\nCommon signals are <code>X-Tenant-Id<\/code>, subdomain, custom domain, and JWT <code>tenant_id<\/code>. Alkademy Press checks them in that priority order, with the SPA typically sending the header for authenticated API calls.<\/p>\n<p><strong>What is the difference between multi-tenant and multi-instance?<\/strong><br \/>\nMulti-tenant: one deploy, many organisations. Multi-instance (single-tenant): separate deploys or stacks per customer. Multi-instance maximizes isolation and cost; multi-tenant maximizes leverage.<\/p>\n<h2 id=\"next-steps\">Next Steps<\/h2>\n<ul>\n<li>Map your domain: what is a tenant, and what nests under it?<\/li>\n<li>Pick Method A unless compliance forces B\/C \u2014 document the decision.<\/li>\n<li>Design tenant resolution (header \/ host \/ JWT) before writing feature CRUD.<\/li>\n<li>Make <code>tenantId<\/code> mandatory on every tenant-owned table and every query path.<\/li>\n<li>Add cross-tenant access tests early \u2014 they pay for themselves.<\/li>\n<li>Explore <a href=\"https:\/\/www.alkademy.com\/solutions\/alkademy-press\" target=\"_blank\" rel=\"noopener noreferrer\">Alkademy Press<\/a> if you work in academic publishing workflows.<\/li>\n<\/ul>\n<p><!-- ktg-related --><\/p>\n<nav class=\"ktg-series-nav\" aria-label=\"Related architecture and API articles\">\n<p><strong>Related:<\/strong><br \/>\n<a href=\"https:\/\/kindsonthegenius.com\/blog\/json-web-token-how-to-secure-rest-api-using-jwt-in-spring-boot\/\">Secure REST APIs with JWT<\/a> \u00b7<br \/>\n<a href=\"https:\/\/kindsonthegenius.com\/blog\/how-to-build-a-rag-chatbot-with-spring-boot-and-openai-step-by-step\/\">Spring Boot RAG tutorial<\/a> \u00b7<br \/>\n<a href=\"https:\/\/kindsonthegenius.com\/blog\/deploy-spring-boot-kubernetes-docker-compose-minikube-2026\/\">Deploy Spring Boot to Kubernetes<\/a>\n<\/p>\n<\/nav>\n<p><!-- ktg-alkademy-cta --><\/p>\n<div class=\"ktg-alkademy-cta\" style=\"margin:2em 0;padding:1.25em;border-left:4px solid #2563eb;background:#f8fafc;\">\n<p><strong>Building SaaS or academic publishing software?<\/strong> See <a href=\"https:\/\/www.alkademy.com\/solutions\/alkademy-press\" target=\"_blank\" rel=\"noopener noreferrer\">Alkademy Press<\/a> for multi-tenant journal management, or explore <a href=\"https:\/\/www.alkademy.com\/courses\" target=\"_blank\" rel=\"noopener noreferrer\">Alkademy courses<\/a> for hands-on backend and cloud training.<\/p>\n<\/div>\n<p><!-- ktg-faq-schema --><br \/>\n<script type=\"application\/ld+json\">{\"@context\":\"https:\/\/schema.org\",\"@type\":\"FAQPage\",\"mainEntity\":[{\"@type\":\"Question\",\"name\":\"What is multi-tenancy in SaaS?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Multi-tenancy is an architecture where one application instance serves many customer organisations. Each tenant\u2019s users and data are logically isolated while infrastructure and codebase stay shared.\"}},{\"@type\":\"Question\",\"name\":\"What are the main multi-tenancy implementation methods?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"The three core methods are shared database with a tenant_id column, schema-per-tenant in one database, and database-per-tenant. Hybrids and Postgres Row-Level Security build on these.\"}},{\"@type\":\"Question\",\"name\":\"Which multi-tenancy method does Alkademy Press use?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Alkademy Press uses a shared PostgreSQL database and shared schema with row-level isolation via tenantId. Tenant context is resolved from header, subdomain, custom domain, or JWT, and services filter queries by tenant.\"}},{\"@type\":\"Question\",\"name\":\"Is shared-database multi-tenancy secure?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Yes when every code path enforces tenant scope and cross-tenant access is tested. Security depends on engineering discipline and optionally database Row-Level Security.\"}},{\"@type\":\"Question\",\"name\":\"When should I use database-per-tenant?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Use database-per-tenant when contracts, compliance, or performance isolation require a dedicated store, or as an enterprise tier beside a shared pool for standard customers.\"}},{\"@type\":\"Question\",\"name\":\"How do you resolve which tenant a request belongs to?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Common signals are the X-Tenant-Id header, subdomain, custom domain, and JWT tenant_id claim. Alkademy Press checks them in that priority order.\"}}]}<\/script><\/p>\n<p><!-- ktg-article-schema --><br \/>\n<script type=\"application\/ld+json\">{\"@context\":\"https:\/\/schema.org\",\"@type\":\"Article\",\"headline\":\"Multi-Tenancy in SaaS Explained \u2014 Patterns, Trade-offs, and Alkademy Press Case Study\",\"description\":\"Learn multi-tenancy strategies for SaaS and how Alkademy Press isolates organisations with shared-database row-level tenancy.\",\"dateModified\":\"2026-07-23\",\"author\":{\"@type\":\"Person\",\"name\":\"Kindson Munonye\"},\"publisher\":{\"@type\":\"Organization\",\"name\":\"KindsonTheGenius\"},\"mainEntityOfPage\":\"https:\/\/kindsonthegenius.com\/blog\/multi-tenancy-in-saas-explained-alkademy-press-case-study\/\",\"keywords\":\"multi-tenancy, multi-tenant saas, tenant_id, alkademy press, shared database\"}<\/script><\/p>\n","protected":false},"excerpt":{"rendered":"<p>July 23, 2026: A practical guide to multi-tenancy strategies, with a real case study of how Alkademy Press isolates publishing organisations on a shared PostgreSQL &hellip; <\/p>\n","protected":false},"author":1,"featured_media":2250,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"pagelayer_contact_templates":[],"_pagelayer_content":"","footnotes":""},"categories":[289],"tags":[],"class_list":["post-2251","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-rest-web-services"],"_links":{"self":[{"href":"https:\/\/kindsonthegenius.com\/blog\/wp-json\/wp\/v2\/posts\/2251","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/kindsonthegenius.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/kindsonthegenius.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/kindsonthegenius.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/kindsonthegenius.com\/blog\/wp-json\/wp\/v2\/comments?post=2251"}],"version-history":[{"count":1,"href":"https:\/\/kindsonthegenius.com\/blog\/wp-json\/wp\/v2\/posts\/2251\/revisions"}],"predecessor-version":[{"id":2252,"href":"https:\/\/kindsonthegenius.com\/blog\/wp-json\/wp\/v2\/posts\/2251\/revisions\/2252"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/kindsonthegenius.com\/blog\/wp-json\/wp\/v2\/media\/2250"}],"wp:attachment":[{"href":"https:\/\/kindsonthegenius.com\/blog\/wp-json\/wp\/v2\/media?parent=2251"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/kindsonthegenius.com\/blog\/wp-json\/wp\/v2\/categories?post=2251"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/kindsonthegenius.com\/blog\/wp-json\/wp\/v2\/tags?post=2251"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}