Adios
BlogNext.js SaaS

Next.js SaaS

Come progettare un SaaS multitenant in Next.js: isolamento, instradamento e appartenenza dei dati

Progetta risoluzione dei tenant, appartenenza dei dati, autorizzazione, chiavi di cache, job, domini personalizzati e distribuzione per un SaaS Next.js multitenant.

Team di AdiosAggiornato 17 luglio 20268 min di lettura

Il modello multitenant impone un invariante: ogni lettura, scrittura, voce di cache, job, log e hostname deve risolversi nel tenant previsto prima dell'inizio del lavoro privilegiato.

Definisci cosa rappresenta un tenant

Un tenant può essere un'organizzazione, uno spazio di lavoro, un negozio o un account cliente. Definisci chi lo crea, chi vi appartiene, se gli utenti possono far parte di più tenant e quali record possiede. Usa internamente un ID stabile del tenant anche se l'identificatore pubblico è uno slug o un hostname personalizzato.

Descrivi la garanzia di isolamento. Infrastruttura condivisa con appartenenza a livello di riga è diversa da un database per tenant. Il modello più forte costa di più da predisporre e gestire, ma può essere giustificato da normative, scala o requisiti dei clienti. Scegli in base al confine effettivo del prodotto, anziché a un ideale astratto di purezza.

Risolvi il contesto tenant da input attendibili

Le app basate sui percorsi possono risolvere /acme/projects dallo slug della route. Le app con sottodomini e domini personalizzati risolvono l'host della richiesta in un record tenant. Normalizza l'host, rifiuta valori sconosciuti e non fidarti mai di un ID tenant fornito dal modulo quando la route autenticata stabilisce già il contesto.

Proxy può riscrivere gli hostname in anticipo o applicare instradamento preliminare, ma la risoluzione sicura deve stare anche nel codice server. Passa un contesto tenant verificato al livello dei dati; non lasciare che ogni componente interpreti da sé le intestazioni e indovini l'appartenenza.

export async function resolveTenant(host: string) {
  const normalized = host.toLowerCase().split(":")[0];
  const tenant = await db.tenant.findUnique({
    where: { hostname: normalized },
  });
  if (!tenant) notFound();
  return tenant;
}

Verifica l'appartenenza in ogni query

Inserisci tenantId nei record delle tabelle condivise e includilo in ogni ricerca, aggiornamento, eliminazione e vincolo di unicità. Un ID di record globalmente univoco non verifica l'autorizzazione. Interroga usando sia l'ID richiesto sia l'ID tenant verificato, così un identificatore esposto non può attraversare il confine.

La sicurezza a livello di riga del database può aggiungere difesa in profondità dove lo stack la supporta, ma l'autorizzazione applicativa resta necessaria. Verifica deliberatamente i rifiuti creando due tenant e tentando ogni operazione protetta con gli identificatori dell'altro.

const project = await db.project.findFirst({
  where: {
    id: projectId,
    tenantId: context.tenantId,
  },
});

Rendi i ruoli specifici del tenant

Un utente può essere owner in un tenant e viewer in un altro. Salva i ruoli nei record di appartenenza, senza usare un unico campo globale sull'utente. Risolvi l'appartenenza dopo il tenant e verifica poi il permesso esatto richiesto dall'operazione.

Evita di spargere stringhe dei ruoli tra i componenti. Centralizza le regole dei permessi in funzioni che restituiscano decisioni del dominio e ripeti il controllo nelle Server Actions e nei Route Handlers. L'interfaccia può nascondere controlli non disponibili per chiarezza, ma è l'autorizzazione sul server a proteggere l'operazione.

Separa cache, job e archiviazione per tenant

Ogni chiave di cache condivisa richiede l'identità del tenant. Una chiave chiamata projects può esporre il risultato di un tenant a un altro; projects:tenant-id stabilisce il confine. Applica la stessa regola a tag di cache, limiti di frequenza, percorsi dell'archivio oggetti, indici di ricerca, payload dei job e chiavi di idempotenza.

I worker in background devono verificare di nuovo l'autorizzazione oppure operare da un contesto tenant attendibile e immutabile registrato alla creazione del job. Includi nei log identificatori sicuri per il tenant, ma non registrare contenuti privati dei clienti solo per semplificare il debug.

"use cache";
cacheTag("projects:" + tenantId);
return db.project.findMany({ where: { tenantId } });

Tratta i domini personalizzati come configurazione verificata

Un tenant non deve poter rivendicare un hostname scrivendolo in un modulo. Richiedi una verifica prima di instradare traffico, impedisci che l'hostname appartenga a due tenant e definisci cosa succede quando cambia proprietario. Tieni separati i domini della piattaforma da quelli gestiti dai clienti.

Genera URL canonici dall'hostname verificato del tenant quando le sue pagine pubbliche sono indicizzabili. Le dashboard autenticate normalmente non devono essere indicizzate. Rendi reindirizzamenti e cookie compatibili con il modello degli hostname, soprattutto quando gli utenti passano tra dominio centrale di accesso e dominio del tenant.

Verifica l'isolamento come proprietà del sistema

Crea fixture automatizzate per almeno due tenant e due ruoli. Verifica pagine dirette, Server Actions, Route Handlers, file esportati, cache, job, ricerca e risoluzione degli host. Nascondere solo la voce di navigazione di un altro tenant non verifica l'isolamento.

Esamina query del database e log per condizioni tenant mancanti. Aggiungi vincoli di unicità che includano tenantId quando i nomi devono essere unici solo nel tenant. Verifica eliminazione ed esportazione dei tenant, così dati in background non rimangono accidentalmente senza proprietario.

Distribuisci l'instradamento dei tenant con rilasci osservabili

Adios mantiene instradamento Next.js, ambiente di esecuzione persistente, dipendenza dal database, riferimenti ai segreti, log e configurazione dei domini personalizzati collegati al rilascio applicativo. Prima della promozione, verifica nell'anteprima un hostname della piattaforma, un hostname di tenant verificato, un host sconosciuto, due account tenant e la route di controllo dello stato.

I log della compilazione e dell'ambiente di esecuzione distinguono un rilascio non riuscito da un errore dei dati specifico del tenant. TLS gestito e promozione mantengono le route pubbliche verificate sulla versione sana, mentre modifiche al sorgente e al manifest restano esaminabili. L'isolamento dei tenant rimane responsabilità della progettazione applicativa e dei dati; l'hosting rende quel progetto distribuibile e ispezionabile.

name: multi-tenant-app
build_cmd: npm ci && npm run build
start_cmd: npm start

runtime:
  name: node@24
  port: 3000
  health_path: /api/health

env:
  DATABASE_URL: secret://DATABASE_URL
Tutti gli articoli