Architettura Software 13 min di lettura 24 febbraio 2026

Costruire piattaforme SaaS moderne: architettura, scalabilità e design multi-tenant

Una guida tecnica completa all'architettura di piattaforme SaaS: modelli di multi-tenancy, strategie di isolamento dei dati, integrazione con sistemi di billing, pattern di scalabilità e le decisioni infrastrutturali che determinano se la tua piattaforma sopravvive alla crescita.

ME

Marco Esposito

Lead Software Architect

Costruire una piattaforma SaaS è fondamentalmente diverso dal costruire un'applicazione su misura per un singolo cliente. Nel momento in cui servi più tenant da una codebase e infrastruttura condivise, erediti una classe di problemi — isolamento dei dati, consumo equo delle risorse, accuratezza del billing, personalizzazione per tenant — che lo sviluppo su commessa non deve mai affrontare. Questa guida è una sintesi delle decisioni architetturali che abbiamo preso e degli errori che abbiamo osservato in più progetti SaaS.

Scegliere il modello di multi-tenancy

Il modello di multi-tenancy definisce come i tenant condividono (o non condividono) la tua infrastruttura. È la decisione architetturale più determinante che prenderai, perché influenza l'isolamento dei dati, la complessità operativa, la struttura dei costi e la capacità di servire clienti enterprise.

Modelli di multi-tenancy a confronto

DB condiviso, schema condivisoTutti i tenant in un unico DB con colonna tenant_id. Il più economico, meno isolato. Adatto a SaaS orientati alle PMI.
DB condiviso, schema separatoUn DB, uno schema per tenant. Gli schema PostgreSQL funzionano bene qui. Buon equilibrio tra isolamento e costi.
Database per tenantMassimo isolamento. Adatto per tenant enterprise con requisiti di compliance. Elevato overhead operativo.
Silo (infrastruttura dedicata)Ogni tenant ha uno stack dedicato. Massimo isolamento, massimo costo. Solo per contratti enterprise di grandi dimensioni.

La nostra raccomandazione predefinita per il SaaS nelle fasi iniziali è il modello database condiviso con schema separato usando PostgreSQL. Offre un forte isolamento logico senza il costo operativo di gestire N database, e la gestione degli schemi di PostgreSQL è abbastanza matura da supportare il provisioning automatico dei tenant.

Implementare la multi-tenancy basata su schema con PostgreSQL

tenant-provisioning.service.tstypescript
@Injectable()
export class TenantProvisioningService {
  constructor(private readonly db: DatabaseService) {}

  async provisionaTenant(tenantId: string, piano: TipoPiano): Promise<void> {
    const nomeSchema = `tenant_${tenantId.replace(/-/g, "_")}`;

    await this.db.transaction(async (trx) => {
      // 1. Crea lo schema
      await trx.raw(`CREATE SCHEMA IF NOT EXISTS ??`, [nomeSchema]);

      // 2. Imposta il search path ed esegui le migration
      await trx.raw(`SET search_path TO ??`, [nomeSchema]);
      await this.eseguiMigration(trx, nomeSchema);

      // 3. Registra il tenant nel registro master pubblico
      await trx("public.tenant").insert({
        id: tenantId,
        nome_schema: nomeSchema,
        piano,
        creato_il: new Date(),
        stato: "attivo",
      });
    });
  }

  // Risolve lo schema tenant dal contesto della richiesta
  async risolviSchemaTenant(tenantId: string): Promise<string> {
    const tenant = await this.db("public.tenant")
      .where("id", tenantId)
      .where("stato", "attivo")
      .first();

    if (!tenant) throw new TenantNonTrovatoException(tenantId);
    return tenant.nome_schema;
  }
}

Middleware per il contesto tenant

tenant.middleware.tstypescript
@Injectable()
export class TenantMiddleware implements NestMiddleware {
  constructor(
    private readonly tenantService: TenantProvisioningService,
    private readonly cls: ClsService, // wrapper AsyncLocalStorage
  ) {}

  async use(req: Request, _res: Response, next: NextFunction): Promise<void> {
    // Estrae il tenant dal sottodominio, header o claim JWT
    const tenantId = this.estraiTenantId(req);

    if (!tenantId) {
      throw new UnauthorizedException("Contesto tenant mancante");
    }

    const nomeSchema = await this.tenantService.risolviSchemaTenant(tenantId);

    // Memorizza in AsyncLocalStorage — accessibile per tutta la durata della richiesta
    this.cls.set("tenantId", tenantId);
    this.cls.set("nomeSchema", nomeSchema);

    next();
  }

  private estraiTenantId(req: Request): string | null {
    // Opzione 1: sottodominio (acme.tuosaas.com)
    const sottodominio = req.hostname.split(".")[0];
    if (sottodominio && sottodominio !== "www" && sottodominio !== "app") return sottodominio;

    // Opzione 2: header personalizzato (per accesso API)
    return req.headers["x-tenant-id"] as string ?? null;
  }
}

Architettura dei dati per la scalabilità

Il layer database è dove la maggior parte delle piattaforme SaaS incontra il suo primo muro di scalabilità. Row-level security, performance delle query alla scala dei tenant e la complessità operativa delle migration degli schemi su N tenant devono essere progettati sin dall'inizio.

Migration multi-tenant automatizzate

Eseguire migration degli schemi su N schemi tenant è una delle sfide operative più sottovalutate. Una migration sequenziale su 500 schemi può richiedere ore e bloccare i deployment. Il nostro approccio è eseguire le migration in batch paralleli con locking esplicito per prevenire stati parziali.

tenant-migration-runner.tstypescript
async function eseguiMigrationSuTuttiITenant(fileMigration: string): Promise<void> {
  const tenant = await db("public.tenant").select("nome_schema").where("stato", "attivo");

  // Elabora in batch da 20 per limitare la pressione sulle connessioni DB
  const DIMENSIONE_BATCH = 20;
  const batch = chunk(tenant, DIMENSIONE_BATCH);

  for (const gruppo of batch) {
    await Promise.all(
      gruppo.map(async ({ nome_schema }) => {
        try {
          await db.raw(`SET search_path TO ??`, [nome_schema]);
          await eseguiMigration(db, fileMigration);
          console.log(`✓ Schema ${nome_schema} migrato`);
        } catch (err) {
          // Registra e continua — gli schemi falliti vengono segnalati per revisione manuale
          console.error(`✗ Migration fallita per ${nome_schema}:`, err.message);
          await db("public.errori_migration").insert({
            nome_schema,
            migration: fileMigration,
            errore: err.message,
            timestamp: new Date(),
          });
        }
      })
    );
  }
}

Integrazione del billing

Il billing è il nucleo finanziario di un prodotto SaaS e la fonte più comune di dispute con i clienti. Usiamo Stripe come provider di billing predefinito e seguiamo un pattern rigoroso: Stripe è il sistema di riferimento per tutti i dati di abbonamento e pagamento; il nostro database memorizza solo gli ID Stripe e informazioni del piano in cache locale per le performance.

billing.service.tstypescript
@Injectable()
export class BillingService {
  constructor(private readonly stripe: Stripe) {}

  async creaAbbonamento(
    tenantId: string,
    idPiano: string,
    idMetodoPagamento: string,
  ): Promise<Stripe.Subscription> {
    // 1. Recupera o crea il cliente Stripe
    let cliente = await this.getClienteStripe(tenantId);
    if (!cliente) {
      cliente = await this.stripe.customers.create({
        metadata: { tenantId },
      });
      await this.salvaTenantStripeId(tenantId, cliente.id);
    }

    // 2. Collega il metodo di pagamento
    await this.stripe.paymentMethods.attach(idMetodoPagamento, {
      customer: cliente.id,
    });

    // 3. Crea l'abbonamento
    return this.stripe.subscriptions.create({
      customer: cliente.id,
      items: [{ price: idPiano }],
      payment_behavior: "default_incomplete",
      expand: ["latest_invoice.payment_intent"],
    });
  }

  // Gestione webhook — mantiene lo stato locale sincronizzato con Stripe
  async gestisciWebhook(evento: Stripe.Event): Promise<void> {
    switch (evento.type) {
      case "customer.subscription.updated":
        await this.sincronizzaStatoAbbonamento(evento.data.object as Stripe.Subscription);
        break;
      case "invoice.payment_failed":
        await this.gestisciPagamentoFallito(evento.data.object as Stripe.Invoice);
        break;
    }
  }
}

Pattern di scalabilità

Scaling orizzontale e statelessness

Ogni server applicativo deve essere stateless. I dati di sessione risiedono in Redis, i file caricati in object storage compatibile S3, i job in background sono accodati in una coda persistente. Questo permette di aggiungere istanze senza coordinamento. Usa un load balancer con health check e una strategia di deployment (blue-green o rolling) che mantiene zero downtime.

Strategia di caching

Fai caching al livello giusto. La configurazione del tenant e i limiti del piano cambiano raramente — mettili in cache su Redis per 5 minuti. I dati di sessione utente sono già su Redis. Le query di lettura pesante (analytics, report) devono essere servite da una read replica o da un database analitico dedicato. Non mettere in cache dati che devono essere in tempo reale (livelli di inventario, stato dei pagamenti).

Job in background e architettura delle code

Invio email, generazione PDF, sincronizzazione con API di terze parti, esportazioni di dati in bulk — tutto questo deve avvenire fuori dal ciclo di richiesta HTTP. Usiamo BullMQ supportato da Redis per le code di job. Ogni tipo di job ha la propria coda con impostazioni di concorrenza indipendenti, policy di retry e livelli di priorità.

Feature flag e personalizzazione per tenant

I clienti enterprise SaaS vogliono inevitabilmente personalizzazioni: branding personalizzato, toggle di funzionalità, diverse configurazioni di workflow. Lo implementiamo tramite un sistema di configurazione a layer: default globali → override a livello di piano → override a livello di tenant. I feature flag sono memorizzati nello schema del tenant e valutati a runtime, mai hardcoded.

Infrastruttura: lo stack production-ready

Architettura di riferimento SaaS Nexora

FrontendNext.js su Vercel (o self-hosted su ECS). Routing sottodominio per tenant.
APINestJS su AWS ECS Fargate. Auto-scaling basato su CPU e metriche di richieste.
DatabaseAWS RDS PostgreSQL (Multi-AZ). Read replica per query analitiche.
Cache / CodeAWS ElastiCache Redis (cluster mode). BullMQ per job, session store.
File StorageAWS S3 con isolamento basato su prefisso per tenant.
CDNCloudFront per asset statici e risposte API in cache.
ObservabilityOpenTelemetry → Grafana Cloud. Alert via PagerDuty.

Considerazioni di sicurezza

  • Row-level security: oltre all'isolamento degli schemi, abilita PostgreSQL RLS come misura di difesa in profondità contro le errate configurazioni dell'ORM.
  • Audit log: ogni mutazione dei dati deve scrivere un audit log tamper-evident in una tabella separata append-only.
  • Rate limiting per tenant: impedisci a un singolo tenant di consumare risorse sproporzionate e influenzare gli altri.
  • Gestione dei segreti: usa AWS Secrets Manager o HashiCorp Vault — mai variabili d'ambiente in chiaro per credenziali di produzione.
  • SOC2 readiness: progetta controlli di accesso, logging e gestione delle modifiche per essere SOC2-compatibili fin dall'inizio se le vendite enterprise sono nel roadmap.

Conclusioni

Una piattaforma SaaS moderna non è solo un'applicazione web con una pagina di abbonamento. È una composizione attenta di isolamento dei dati, accuratezza del billing, equità delle risorse, automazione del deployment e garanzie di sicurezza. Le decisioni che prendi nei primi tre mesi di architettura sono quelle con cui conviverai quando avrai 500 tenant. Investi nell'ottenere il modello di multi-tenancy, la strategia di migration e l'integrazione del billing corretti prima di investire nelle funzionalità.

Abbiamo costruito piattaforme SaaS da zero e modernizzato monoliti legacy in architetture multi-tenant. Se stai progettando un nuovo prodotto SaaS o scalando uno esistente, offriamo architecture review e delivery hands-on.

Tag

SaaSMulti-tenancyArchitetturaPostgreSQLScalabilitàInfrastruttura

Prossimo passo

Hai bisogno di implementare questa architettura?

Il nostro team di ingegneria costruisce e scala i sistemi descritti in questo articolo. Dalla discovery alla produzione — con risultati misurabili.