L’architettura di database multi-tenant è un modello per archiviare molti clienti in un unico livello dati, isolati per riga, schema o database dedicato. Quei tre gradi di isolamento esauriscono tutto lo spazio decisionale – il resto di questo tema ne è solo la conseguenza: come girano le migrazioni, quanto costa un ripristino, dove vive il noisy neighbor (il vicino rumoroso) e fino a che punto ogni pattern scala.
Punti chiave
| Domanda | Risposta |
|---|---|
| I tre pattern | Schema condiviso (tenant ID per riga) · schema per tenant · database per tenant |
| Il vocabolario cloud | Gli stessi tre: pool · bridge · silo |
| La scelta predefinita | Schema condiviso, a meno che conformità o scala impongano l’isolamento |
| I tetti pratici | Silo: ~centinaia di tenant · bridge: ~1.000 · pool: milioni (con sharding: illimitato) |
| La regola di ferro | Far rispettare l’isolamento nel database, non in ogni query |
I tre pattern, in SQL
-- Pattern 1 · Schema condiviso ("pool"): un solo insieme di tabelle, il tenant su ogni riga
CREATE TABLE invoices (
tenant_id uuid NOT NULL,
id bigint GENERATED ALWAYS AS IDENTITY,
total numeric(10,2),
PRIMARY KEY (tenant_id, id) -- il tenant in testa: pronto per indici e sharding
);
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_rows ON invoices
USING (tenant_id = current_setting('app.tenant')::uuid);
-- Pattern 2 · Schema per tenant ("bridge"): un solo database, un namespace ciascuno
CREATE SCHEMA tenant_acme; -- le stesse tabelle, ripetute per ogni tenant
-- Pattern 3 · Database per tenant ("silo"): separazione fisica completa
CREATE DATABASE tenant_acme; -- l'isolamento più forte; N copie di tutto
Su un backend gestito la stessa garanzia si esprime senza SQL: il controllo degli accessi vive sui dati stessi, quindi l’isolamento tiene su ogni percorso – API, dashboard o SDK:
// JavaScript / Node.js — Back4app JS SDK
// The same query for every tenant — ACLs scope results server-side
const query = new Parse.Query('Invoice');
const invoices = await query.find({ sessionToken: user.getSessionToken() });
// Only rows this tenant's role can read come back. No WHERE clause to forget. // Flutter / Dart — Back4app Flutter SDK
// The same query for every tenant — ACLs scope results server-side
final query = QueryBuilder<ParseObject>(ParseObject('Invoice'));
final response = await query.query();
// The session's role decides which rows exist, before results leave the server
if (response.success) {
print('${response.results?.length} invoices visible to this tenant');
} // iOS / Swift — Back4app Swift SDK
// The same query for every tenant — ACLs scope results server-side
let query = Invoice.query()
query.find { result in
if case .success(let invoices) = result {
print("\(invoices.count) invoices visible to this tenant")
}
} // Android / Kotlin — Back4app Android SDK
// The same query for every tenant — ACLs scope results server-side
val query = ParseQuery.getQuery<ParseObject>("Invoice")
query.findInBackground { invoices, e ->
if (e == null) {
Log.d("Billing", "${invoices.size} invoices visible to this tenant")
}
} Schema condiviso vs. schema per tenant vs. database per tenant
| Dimensione | Schema condiviso | Schema per tenant | Database per tenant |
|---|---|---|---|
| Isolamento | Logico, per riga | Namespace | Fisico |
| Tetto di tenant | Milioni | ~Centinaia–1.000 | Decine–poche centinaia |
| Costo per tenant | Il più basso | Intermedio | Il più alto |
| Migrazioni | Girano una volta, toccano tutti | Girano × N, orchestrate | Girano × N, orchestrate |
| Ripristino per tenant | Difficile (copia selettiva) | Moderato | Banale (ripristinare un DB) |
| Noisy neighbor | Il più esposto | In parte contenuto | Eliminato |
| Personalizzazione per tenant | La più difficile | Possibile per schema | La più facile |
| Onboarding di un tenant | Inserire una riga | Creare uno schema | Fare il provisioning di un database |
Le insidie nei dettagli
- L’indicizzazione parte dal tenant.
tenant_idva in ogni tabella – anche dove i join lo fanno sembrare ridondante – e apre ogni indice composito:(tenant_id, created_at), non il contrario. Ogni query reale è limitata a un tenant; gli indici dovrebbero esserlo altrettanto. - Le migrazioni si moltiplicano con l’isolamento. La flessibilità del silo si paga con un livello di orchestrazione: tracciamento delle versioni per tenant, logica di ritentativo, rilevamento della deriva. I team sottovalutano questa riga più di ogni altra della tabella.
- La sicurezza a livello di riga ha le sue clausole in piccolo operative. Le policy si appoggiano a impostazioni per connessione, che interagiscono con le modalità di pooling delle connessioni – il tenant va impostato per transazione, e il pooler va testato. Va anche verificato quali ruoli aggirano RLS; i percorsi da superutente sono il buco classico.
- L’aritmetica delle connessioni. Un pool per ogni database di tenant esaurisce in fretta le connessioni; i progetti a schema condiviso condividono un solo pool – un altro vantaggio silenzioso del pattern pool, che emerge solo su larga scala.
- L’asimmetria del ripristino spinge alla ripartizione in tier. “È possibile ripristinare soltanto i nostri dati a ieri?” è una domanda da contratto enterprise; se la risposta deve essere sì, quel tenant appartiene a un silo – il che porta direttamente all’ibrido.
Scalabilità e stato finale ibrido
La via di crescita del pattern pool è lo sharding per tenant: più database a schema condiviso, ciascuno con una fetta di tenant, e un catalogo che associa tenant → shard (il progetto open source Citus lo ha integrato in PostgreSQL, compreso lo spostamento di un tenant molto attivo su un nodo proprio). Combinato con la ripartizione in tier, questo produce l’architettura verso cui converge la maggior parte dei SaaS maturi: il tier gratuito nel pool, i tenant di taglia media nel pool su più shard, e i pochi tenant regolamentati o enormi in silo – con tenant_id conservato in ogni schema, dappertutto, così che qualunque tenant possa cambiare tier senza una riprogettazione. La mobilità dei tenant è la proprietà da progettare dal primo giorno; è quasi impossibile aggiungerla in un secondo momento.
Casi d’uso tipici
- SaaS B2B. Il caso fondativo: ogni workspace, organizzazione o team del prodotto è un tenant in uno di questi pattern.
- Prodotti freemium su larga scala. Migliaia di piccoli tenant gratuiti nel pool a un costo marginale vicino a zero – l’economia che rende possibili i piani gratuiti.
- Settori regolamentati. Clienti della sanità, della finanza e del legale che esigono per contratto dei silo – serviti dalla stessa codebase grazie all’ibrido.
- Agenzie e piattaforme. Una sola applicazione al servizio di molte organizzazioni clienti, ciascuna con il proprio confine.
- Piattaforme interne multi-team. I reparti come tenant su strumenti condivisi – stessi pattern, modello di minaccia più clemente.
Quale pattern conviene scegliere? Matrice decisionale
| Scegliere lo schema condiviso quando… | Scegliere lo schema per tenant quando… | Scegliere il database per tenant quando… |
|---|---|---|
| I tenant sono molti e piccoli | I tenant si contano a centinaia | I tenant sono pochi e grandi |
| Il costo per tenant deve tendere a zero | Un isolamento moderato vale un po’ di lavoro operativo | La conformità esige la separazione fisica |
| Una sola migrazione deve aggiornare tutti | Servono ritocchi di schema per tenant | Il ripristino per tenant è contrattuale |
| Registrazione self-service, onboarding istantaneo | I tenant arrivano a ritmo umano | Ogni tenant giustifica il provisioning |
| Si farà sharding crescendo | Si metterà un tetto al numero di tenant | Si automatizzeranno le migrazioni × N |
E la meta-risposta: conviene scegliere per tier, non per azienda – i progetti ibridi mettono ogni tenant nel pattern più economico che soddisfa i suoi requisiti, e lo spostano quando i requisiti cambiano.
Limiti e trade-off
- Schema condiviso: il bisogno più forte di un isolamento applicato dal database – un filtro mancato è una violazione, ed è per questo che RLS o le ACL del livello dati non sono negoziabili, non un irrobustimento opzionale.
- Schema per tenant: la scomoda via di mezzo – orchestrazione delle migrazioni come nel silo, isolamento più debole che nel silo, e limiti ai metadati del database che arrivano sorprendentemente presto.
- Database per tenant: tutto × N – migrazioni, backup, monitoring, connessioni, costi – e l’analisi fra tenant che diventa un progetto di data engineering.
- Tutti e tre: il contesto di tenant invade tutto (query, cache, job, log), e passare da un pattern all’altro costa caro senza disciplina sugli identificatori dal primo giorno: identificatori globalmente univoci e colonne
tenant_iddappertutto, anche nei silo.
I dati multi-tenant su Back4app
Back4app è una piattaforma open-source di Backend as a Service (BaaS) che combina database gestito, API REST e GraphQL generate automaticamente, autenticazione, archiviazione di file e funzioni serverless con Cloud Code. La sua risposta alla regola di ferro di questo articolo – far rispettare l’isolamento nel database – sono le ACL per oggetto, i ruoli per tenant e i permessi di classe, verificati dalla piattaforma a ogni richiesta e da ogni superficie, come mostrano le schede di codice qui sopra. È il pattern a schema condiviso, liberato dal rischio della clausola WHERE; l’approfondimento di ingegneria sulla sicurezza a livello di riga percorre l’intero pattern su un database documentale, e la vista a livello di hosting dello stesso tema copre il livello di infrastruttura che gli sta sopra.
Domande frequenti
Quali sono i tre pattern di database multi-tenant?
Lo schema condiviso, cioè un solo insieme di tabelle con ogni riga che porta un identificatore di tenant; lo schema per tenant, cioè un solo database con un namespace di tabelle separato per cliente; e il database per tenant, cioè la separazione fisica completa. Le guide di architettura cloud chiamano questi stessi tre pattern pool, bridge e silo. I sistemi reali li combinano sempre più spesso, distribuendo i tenant fra i pattern secondo dimensione ed esigenze di conformità.
Database condiviso o database per tenant: quale conviene scegliere?
La scelta predefinita che fa consenso è lo schema condiviso, a meno che qualcosa imponga l'isolamento: requisiti normativi, separazione contrattuale dei dati, forte personalizzazione per tenant oppure singoli tenant abbastanza grandi da richiedere risorse proprie. Lo schema condiviso massimizza la densità e riduce al minimo il lavoro operativo; i database per tenant massimizzano l'isolamento e moltiplicano tutto il resto – migrazioni, backup, connessioni, costi.
Quanti tenant riesce a gestire ciascun pattern?
I tetti pratici ricavati dall'esperienza in produzione: il database per tenant regge comodamente fino a decine o poche centinaia di tenant prima che il lavoro operativo prenda il sopravvento; lo schema per tenant arriva alle centinaia, fino a circa un migliaio, prima che i metadati e l'orchestrazione delle migrazioni vadano in sofferenza; lo schema condiviso scala a migliaia o milioni di tenant, e lo sharding dello schema condiviso per tenant lo estende praticamente senza limiti.
Come si impedisce a un tenant di vedere i dati di un altro tenant?
Con la difesa in profondità, mai con una clausola WHERE da sola. La limitazione al tenant a livello applicativo (middleware o filtri dell'ORM) è il primo livello; la sicurezza a livello di riga applicata dal database è il secondo – policy che filtrano ogni query in base al tenant corrente, qualunque cosa l'applicazione abbia dimenticato. Nei progetti a schema condiviso un solo filtro mancato è una violazione fra tenant, ed è per questo che il confine deve essere fatto rispettare dal database stesso.
Come funzionano le migrazioni di schema nei vari pattern?
Con lo schema condiviso una migrazione aggiorna tutti i tenant in un colpo solo – semplice, con un raggio d'impatto all'altezza. Con lo schema o il database per tenant la stessa migrazione deve girare una volta per tenant: centinaia di esecuzioni che richiedono orchestrazione, tracciamento delle versioni e rilevamento della deriva, perché un'esecuzione fallita lascia un tenant su uno schema più vecchio. Gli strumenti di migrazione sono la tassa nascosta dell'isolamento.
Si possono ripristinare i dati di un solo tenant?
Con il database per tenant è banale: si ripristina quel database a qualunque istante, senza toccare nessun altro. Con lo schema condiviso è davvero difficile: il backup contiene tutti, quindi si ripristina su un'istanza laterale e si ricopiano selettivamente le righe del tenant. Il ripristino per singolo tenant è uno degli argomenti pratici più forti che le grandi aziende portano a favore dei tier di isolamento.
Come va indicizzato un database multi-tenant a schema condiviso?
L'identificatore di tenant va messo in ogni tabella – anche dove sembra ridondante – e va posto in testa a ogni indice composito, così che ogni pattern di query diventi "prima il tenant". Fare della colonna del tenant la prima parte della chiave primaria prepara anche i dati allo sharding per tenant, che è la via di crescita standard.
Che cos'è lo sharding per tenant?
Distribuire un progetto a schema condiviso su più database, con tutte le righe di un tenant che vivono su esattamente uno shard e un catalogo che associa i tenant agli shard. Preserva la densità dello schema condiviso mettendo un tetto alla dimensione di ogni singolo database, e permette di spostare i tenant più attivi su shard più tranquilli. Il prezzo sono il catalogo, gli strumenti di ribilanciamento e la perdita delle query banali fra tenant.