laravelsaasarquitecturaavanzado

SaaS en Laravel: Construye tu Plataforma con SaaSykit

Introducción: El Auge de los SaaS y Laravel

Construir una aplicación SaaS (Software as a Service) desde cero es un desafío complejo. No solo necesitas lógica de negocio sólida, sino también características empresariales críticas como facturación, multi-tenancy, autenticación avanzada y gestión de suscripciones.

SaaSykit es un starter kit de Laravel diseñado específicamente para resolver este problema. Viene preconfigurado con todas las características que una aplicación SaaS moderna necesita, permitiéndote enfocarte en lo que realmente importa: tu producto.

En este artículo, exploraremos cómo SaaSykit acelera el desarrollo de SaaS, qué incluye de fábrica y cómo personalizarlo para tu caso específico.

¿Qué es SaaSykit?

SaaSykit es un kit de inicio (starter kit) de Laravel que proporciona una base lista para producción con componentes esenciales para aplicaciones SaaS. No es un framework adicional, sino una distribución configurada de Laravel con arquitectura y características preconstruidas.

Características principales

Multi-tenancy: Soporte nativo para múltiples clientes con datos aislados.

Facturación integrada: Sistema de pagos y suscripciones preconfigurado.

Autenticación avanzada: Manejo de usuarios, roles y permisos listos para usar.

Plantillas de UI: Interfaces modernas basadas en Tailwind CSS.

API REST: Endpoints ya configurados para integraciones.

Documentación completa: Guías paso a paso para cada feature.

Instalación de SaaSykit

Requisitos previos

Antes de comenzar, asegúrate de tener:

  • PHP 8.2 o superior
  • Composer instalado
  • Node.js 18+ (para compilar assets)
  • Base de datos MySQL o PostgreSQL
  • Un servidor web (Apache, Nginx o Laravel Herd)

Pasos de instalación

La instalación es tan simple como clonar el repositorio:

git clone https://github.com/saasykit/saasykit.git tu-proyecto
cd tu-proyecto

Instala las dependencias de PHP:

composer install

Copia el archivo de configuración:

cp .env.example .env

Genera la clave de aplicación:

php artisan key:generate

Ejecuta las migraciones para crear la estructura de base de datos:

php artisan migrate

Instala las dependencias de Node.js y compila los assets:

npm install
npm run dev

Finalmente, inicia el servidor de desarrollo:

php artisan serve

Ahora puedes acceder a tu aplicación en http://localhost:8000.

Estructura Multi-Tenancy en SaaSykit

¿Qué es Multi-tenancy?

Multi-tenancy es una arquitectura donde múltiples clientes (tenants) comparten la misma aplicación, pero sus datos están completamente aislados. Es fundamental en cualquier SaaS.

SaaSykit usa el enfoque de database per tenant, donde cada cliente tiene su propia base de datos.

Configuración de Tenants

El archivo de configuración principal está en config/tenancy.php:

<?php

return [
    'database' => [
        'connection' => env('DB_TENANT_CONNECTION', 'tenant'),
        'prefix' => env('DB_TENANT_PREFIX', 'tenant_'),
    ],

    'domain' => [
        'pattern' => env('TENANT_DOMAIN_PATTERN', '{tenant}.localhost'),
    ],

    'path' => [
        'enabled' => env('TENANCY_BY_PATH', false),
        'pattern' => '{tenant}',
    ],
];

Crear un nuevo Tenant

SaaSykit incluye comandos artisan para gestionar tenants:

php artisan tenants:create nombre-empresa ejemplo.com

Este comando crea automáticamente:

  • Una nueva base de datos para el tenant
  • Un registro en la tabla tenants
  • Las migraciones específicas del tenant

Modelo Tenant

El modelo Tenant es el corazón de la arquitectura:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Stancl\Tenancy\Database\Models\Tenant as BaseTenant;

class Tenant extends BaseTenant
{
    protected $guarded = [];

    public static function getCustomColumns(): array
    {
        return [
            'id',
            'name',
            'domain',
            'subscription_plan',
            'subscription_status',
            'stripe_customer_id',
        ];
    }

    public function users()
    {
        return $this->hasMany(User::class);
    }

    public function plan()
    {
        return $this->belongsTo(Plan::class, 'subscription_plan');
    }

    public function isActive()
    {
        return $this->subscription_status === 'active';
    }
}

Sistema de Facturación y Pagos

Integración con Stripe

SaaSykit viene preconfigurado con Stripe para manejar pagos. La integración se realiza a través de Laravel Cashier.

Primero, configura tus credenciales de Stripe en .env:

STRIPE_PUBLIC_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...

Modelos de suscripción

El modelo Subscription maneja toda la lógica de suscripciones:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Laravel\Cashier\Billable;

class Subscription extends Model
{
    use Billable;

    protected $fillable = [
        'tenant_id',
        'plan_id',
        'stripe_subscription_id',
        'status',
        'current_period_start',
        'current_period_end',
        'cancel_at_period_end',
    ];

    protected $casts = [
        'current_period_start' => 'datetime',
        'current_period_end' => 'datetime',
    ];

    public function tenant()
    {
        return $this->belongsTo(Tenant::class);
    }

    public function plan()
    {
        return $this->belongsTo(Plan::class);
    }

    public function isActive()
    {
        return $this->status === 'active';
    }

    public function isCancelled()
    {
        return $this->status === 'cancelled';
    }

    public function renew()
    {
        // Lógica de renovación
        $this->update(['status' => 'active']);
    }
}

Crear un plan de precios

Los planes se definen en la tabla plans:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
    public function up()
    {
        Schema::create('plans', function (Blueprint $table) {
            $table->id();
            $table->string('name'); // 'Starter', 'Pro', 'Enterprise'
            $table->text('description');
            $table->decimal('price', 10, 2);
            $table->string('stripe_price_id');
            $table->integer('billing_period'); // 30 para mensual
            $table->json('features'); // ['feature1', 'feature2']
            $table->boolean('is_active')->default(true);
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('plans');
    }
};

Seed algunos planes de ejemplo:

<?php

namespace Database\Seeders;

use App\Models\Plan;
use Illuminate\Database\Seeder;

class PlanSeeder extends Seeder
{
    public function run()
    {
        Plan::create([
            'name' => 'Starter',
            'description' => 'Para equipos pequeños',
            'price' => 29.00,
            'stripe_price_id' => 'price_...',
            'billing_period' => 30,
            'features' => [
                'users_limit' => 5,
                'storage_gb' => 10,
                'api_calls' => 10000,
            ],
        ]);

        Plan::create([
            'name' => 'Pro',
            'description' => 'Para equipos en crecimiento',
            'price' => 99.00,
            'stripe_price_id' => 'price_...',
            'billing_period' => 30,
            'features' => [
                'users_limit' => 50,
                'storage_gb' => 100,
                'api_calls' => 100000,
            ],
        ]);
    }
}

Autenticación y Control de Acceso

Sistema de permisos integrado

SaaSykit usa spatie/laravel-permission para un control de acceso granular:

<?php

namespace App\Models;

use Spatie\Permission\Traits\HasRoles;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use HasRoles;

    protected $fillable = [
        'tenant_id',
        'name',
        'email',
        'password',
        'email_verified_at',
    ];

    public function tenant()
    {
        return $this->belongsTo(Tenant::class);
    }

    public function isAdmin()
    {
        return $this->hasRole('admin');
    }

    public function canAccessFeature($feature)
    {
        $plan = $this->tenant->plan;
        return $plan->features[$feature] ?? false;
    }
}

Definir roles y permisos

En un seeder:

<?php

use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;

// Crear permisos
Permission::create(['name' => 'view_dashboard']);
Permission::create(['name' => 'manage_users']);
Permission::create(['name' => 'manage_billing']);
Permission::create(['name' => 'view_reports']);

// Crear roles
$adminRole = Role::create(['name' => 'admin']);
$memberRole = Role::create(['name' => 'member']);

// Asignar permisos a roles
$adminRole->givePermissionTo(['view_dashboard', 'manage_users', 'manage_billing', 'view_reports']);
$memberRole->givePermissionTo(['view_dashboard', 'view_reports']);

Proteger rutas con permisos

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\BillingController;

Route::middleware(['auth:sanctum', 'verified'])->group(function () {
    Route::get('/dashboard', fn() => view('dashboard'))->name('dashboard');
    
    Route::middleware(['permission:manage_billing'])->group(function () {
        Route::get('/billing', [BillingController::class, 'show'])->name('billing.show');
        Route::post('/billing/update-payment', [BillingController::class, 'update'])->name('billing.update');
    });
});

API REST para Integraciones

SaaSykit incluye una API REST completamente funcional con autenticación token.

Endpoints de ejemplo

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\UserController;
use App\Http\Controllers\Api\SubscriptionController;

Route::middleware('api')->prefix('api')->group(function () {
    // Autenticación
    Route::post('/login', [AuthController::class, 'login']);
    Route::post('/register', [AuthController::class, 'register']);

    // Rutas protegidas
    Route::middleware('auth:sanctum')->group(function () {
        Route::apiResource('users', UserController::class);
        Route::apiResource('subscriptions', SubscriptionController::class);
        Route::get('/me', fn() => auth()->user());
    });
});

Ejemplo de controlador API

<?php

namespace App\Http\Controllers\Api;

use App\Models\User;
use App\Http\Resources\UserResource;
use Illuminate\Http\Request;

class UserController extends Controller
{
    public function index()
    {
        $users = auth()->user()->tenant->users()->paginate(15);
        return UserResource::collection($users);
    }

    public function store(Request $request)
    {
        $validated = $request->validate([
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:users',
        ]);

        $user = auth()->user()->tenant->users()->create($validated);
        
        return new UserResource($user);
    }

    public function show(User $user)
    {
        $this->authorize('view', $user);
        return new UserResource($user);
    }
}

Personalización y Extensión

Agregar campos personalizados al tenant

Crea una migración:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
    public function up()
    {
        Schema::table('tenants', function (Blueprint $table) {
            $table->string('logo_url')->nullable();
            $table->string('primary_color')->default('#000000');
            $table->json('metadata')->nullable();
            $table->boolean('enable_sso')->default(false);
        });
    }

    public function down()
    {
        Schema::table('tenants', function (Blueprint $table) {
            $table->dropColumn(['logo_url', 'primary_color', 'metadata', 'enable_sso']);
        });
    }
};

Crear eventos de ciclo de vida

Cuando un tenant se crea o cancela, dispara eventos:

<?php

namespace App\Events;

use App\Models\Tenant;
use Illuminate\Foundation\Events\Dispatchable;

class TenantCreated
{
    use Dispatchable;

    public function __construct(public Tenant $tenant)
    {
    }
}

class TenantCancelled
{
    use Dispatchable;

    public function __construct(public Tenant $tenant)
    {
    }
}

Luego, crea listeners para procesar estos eventos:

<?php

namespace App\Listeners;

use App\Events\TenantCreated;
use App\Mail\WelcomeEmail;
use Illuminate\Support\Facades\Mail;

class SendWelcomeEmail
{
    public function handle(TenantCreated $event)
    {
        Mail::to($event->tenant->owner_email)
            ->send(new WelcomeEmail($event->tenant));
    }
}

Despliegue de una aplicación SaaS

Consideraciones importantes

  • Seguridad de tenants: Asegúrate de que los datos de un tenant nunca sean accesibles a otro.
  • Escalabilidad de base de datos: Con muchos tenants, considera usar clustering.
  • Monitoreo: Configura Laravel Telescope o New Relic para monitoreo en producción.
  • Backups: Automat