Back to projects
L4 · Multi-tenant Live

001 · Technical case study

Multi-tenant
SaaS platform.

B2B platform con aislamiento estricto por organización, dual-layer payments (suscripción de plataforma + cuentas conectadas con split-payouts) y una capa de integraciones OAuth extensible a cualquier proveedor.

Role
Full-stack engineer
Year
2025 — ongoing
Status
Live · production
Scope
Architecture · Backend · Frontend · Infra
Team
Solo + design partner
Sector
B2B SaaS · NDA

By the numbers

The codebase, quantified.

Snapshot of the production repo — counted directly from source.

0

Migrations

35 central · 67 tenant

0

Eloquent models

Domain entities

0

Controllers

Inertia + REST

0

Services

Business logic

0

Background jobs

Horizon queues

0

Vue files

82 pages · 179 components

0

LOC routes

6 route files

0 +

OAuth providers

Pluggable layer

Schema map

Two databases, 43 tables.

Central DB owns identity, billing and tenancy metadata. Each tenant DB owns its full domain.

Central DB

Identity · Billing · OAuth

16 tables
tenantsdomainscentral_userstenant_usercentral_subscriptionscentral_subscription_planscentral_oauth_configsoauth_statescentral_account_share_tokensbilling_plan_assignmentssubscriptionssubscription_itemscustomersrolespermissionssessions

Tenant DB

Per organisation · Isolated

25 tables × N tenants
cuentascuenta_usuariocuenta_subscriptioncuenta_metric_snapshotspublicacionspublicacion_metric_snapshotspublication_readshistoriasguionsguion_inline_commentscommentstipoctipo_cuentabilling_planssubscriptionssubscription_itemsnotification_settingsnotification_send_logtenant_notificationstenant_oauth_configsmediatagstaggablesjobsfailed_jobs
central_user_id (string) cross-DB · no FK

Module map

Eight domain clusters.

How the 32 models cluster into bounded contexts. Each module owns its own services, jobs, and routes.

01 6 entities

Tenancy & Auth

Path-based isolation, central users, cross-DB permissions.

  • › Tenant
  • › CentralUser
  • › MainUsers
  • › Role
  • › Permission
  • › AccountShareToken
02 5 entities

Billing & Plans

Dual-layer subscriptions: platform Cashier + per-account billing.

  • › CentralSubscription
  • › CentralSubscriptionPlan
  • › BillingPlan
  • › BillingPlanAssignment
  • › CuentaSubscription
03 4 entities

OAuth Layer

Per-tenant + central OAuth configs + state manager.

  • › CentralOAuthConfig
  • › TenantOAuthConfig
  • › OAuthService
  • › OAuthStateManager
04 7 entities

Publishing

Posts, stories, scripts, accounts — the content domain.

  • › Publicacion
  • › Historia
  • › Guion
  • › GuionInlineComment
  • › Cuenta
  • › CuentaUsuario
  • › Tipoc
05 4 entities

Analytics

Time-series snapshots of accounts and posts for trend analysis.

  • › CuentaMetricSnapshot
  • › PublicacionMetricSnapshot
  • › PublicationRead
  • › SnapshotAccountMetricsJob
06 4 entities

Notifications

Per-tenant settings, send log, queued delivery via Brevo.

  • › TenantNotification
  • › NotificationSendLog
  • › NotificationSetting
  • › NotificationService
07 4 entities

Media & Annotations

Spatie MediaLibrary on S3 + comment threads on scripts.

  • › Media
  • › Comment
  • › Comentario
  • › MediaUploadService
08 3 entities

Auto-publish Pipeline

Self-rescheduling fan-out — see the deep-dive →

  • › AutoPublishScheduler
  • › ProcessAutoPublishPosts
  • › CheckAutoPublishSystemJob
Read deep-dive →

01

Tenancy.

Architecture · Isolation · Routing

Construida sobre stancl/tenancy 3.9 con identificación path-based: cada request resuelve el contexto de tenant antes del controlador, conmutando conexión de DB, cache, filesystem y cola.

  1. 01

    Path-based tenant resolution

    El middleware InitializeTenancyByPath resuelve el tenant a partir del primer segmento de URL y conmuta la conexión activa de DB, cache y filesystem antes de que el controlador se ejecute. Todo el dominio funcional vive bajo /{tenant_id}/… sin colisionar con rutas centrales.

  2. 02

    Database-per-tenant isolation

    Cada organización obtiene su propia base de datos (tenant{id}) con 27 tablas migradas vía artisan tenants:migrate. La DB central mantiene únicamente 16 tablas de scope global: tenants, domains, usuarios, billing, OAuth configs.

  3. 03

    Cross-database relationships

    Los usuarios viven en la DB central pero se referencian desde DBs de tenant mediante central_user_id como string — sin FK físicas. CentralUser::can() sobrescribe Spatie Permission para resolver siempre contra la conexión central, independiente del contexto activo.

  4. 04

    Permission cache isolation

    Un middleware ClearPermissionCacheForTenancy purga la caché de permisos al entrar al contexto de tenant, eliminando el clásico bug de autorización cruzada que aparece al reutilizar workers entre tenants.

02

Payments.

Dual-layer · Connected accounts

Dos flujos de dinero que conviven sobre la misma infraestructura — sin acoplarse, sin pisarse, y con reglas de acceso deterministas.

Layer 1

Platform subscriptions

La plataforma cobra a cada tenant mediante laravel/cashier 15. El middleware CheckTenantSubscription intercepta cada request al namespace del tenant y bloquea acceso si no se cumplen tres invariantes: subscription ID válido, estado active/trialing, y current_period_end futuro.

  • Cashier 15
  • Webhook sync
  • Middleware gate
Layer 2

Connected accounts (per merchant)

Onboarding de cuentas conectadas por tenant para cobrar a sus propios clientes finales. StripeConnectService orquesta destination charges hacia la cuenta del merchant con split-payouts automáticos y application fee retenida en cada transacción. Webhooks reconcilian estado de cada cuenta conectada.

  • StripeConnectService
  • Destination charges
  • Application fee
  • KYC sync

// access invariants — checked on every tenant request

public function isPaid(): bool
{
    return $this->stripe_subscription_id
        && in_array($this->status, ['active', 'trialing'])
        && (!$this->current_period_end || $this->current_period_end->isFuture());
}

03

OAuth.

6+ providers · pluggable

Un único OAuthService abstrae la lógica común; cada provider añade un controller especializado.

Provider
Flavor
Controllers
Status
Instagram
Graph API + SDK
4
Live
Facebook
Pages + Instagram Business
2
Live
TikTok
Webhook-signed
1
Live
Google
OAuth + Gemini API
1
Live
LinkedIn
happyr/linkedin-api
1
Ready
Twitter / X
twitteroauth v7
1
Ready

04

Lifecycle.

Tenant onboarding flow

  Manager registers     ┌──────────────────────┐
   on /register   ───▶  │  CentralUser created │
                        └──────────┬───────────┘
                                   │
                                   ▼
                        ┌──────────────────────┐
                        │  Tenant + Domain     │ ◀─── tenants:create
                        │  central_subscription│
                        └──────────┬───────────┘
                                   │ tenants:migrate
                                   ▼
                        ┌──────────────────────┐
                        │  Tenant DB seeded    │ 27 tables
                        │  permissions cleared │ ClearPermissionCache
                        └──────────┬───────────┘
                                   │ first request
                                   ▼
                        ┌──────────────────────┐
   CheckTenantSubscr.   │  isPaid()? ───── no  │ ───▶ /billing
   ────────────────────▶│        │             │
                        │       yes            │
                        │        ▼             │
                        │  Dashboard           │
                        └──────────┬───────────┘
                                   │
                ┌──────────────────┼──────────────────┐
                ▼                  ▼                  ▼
         ┌──────────┐      ┌────────────┐    ┌─────────────┐
         │ Connect  │      │  Connect   │    │   Create    │
         │ social   │      │  Stripe    │    │ publication │
         │ accounts │      │  account   │    │   draft     │
         └──────────┘      └────────────┘    └─────────────┘

05

Integrations.

External services orchestrated

OAuth providers
Instagram (Graph + SDK) · Facebook · TikTok · Google · LinkedIn · Twitter — vía Socialite manager con refresh automático
AI generation
Google Gemini para generación de scripts (google-gemini-php/client)
Media pipeline
Spatie MediaLibrary 11 + AWS S3 (Flysystem v3) — conversiones on-demand, fallbacks, streaming
Realtime
Laravel Reverb 1.7 (WebSocket nativo) para updates de dashboard sin polling
Transactional email
Brevo via sarfrazrizwan/laravel-brevo + colas priorizadas en Horizon
Auth dual-scope
Spatie Permission resolviendo contra central DB desde tenant context
PDF generation
Spatie laravel-pdf para reportes de billing
Tags & taxonomy
Spatie laravel-tags scoped a tenant

06

Operations.

Async · Deploy · Quality

Queues
Horizon 5.33 sobre Redis · workers persistentes vía Supervisor
Scheduler
Cron central que despacha jobs tenant-scoped
Deploy
Push-to-main = build + deploy automático a producción
Observability
Horizon metrics · Pail logs · webhook traces
Tests
Pest PHP 3 · feature tests sobre SQLite in-memory
Code quality
Pint + ESLint + Prettier en pre-commit

07

Stack.

Production dependencies — curated

Core

  • PHP 8.2
  • Laravel 12
  • Inertia.js 2.0
  • Vue 3
  • TypeScript
  • Tailwind v4
  • Reka UI

Tenancy

  • stancl/tenancy 3.9
  • Path-based identification
  • Database-per-tenant

Payments

  • Laravel Cashier 15
  • Stripe Connect
  • Webhook-driven state

Async · Realtime

  • Horizon 5.33
  • Redis · Predis 3
  • Laravel Reverb 1.7
  • Supervisor

Integrations

  • Socialite (5+ providers)
  • Gemini AI
  • Spatie MediaLibrary
  • AWS S3
  • Brevo

Quality

  • Pest PHP 3
  • Laravel Pint
  • ESLint
  • Prettier
  • SSR-ready Vite