Cómo controlar qué módulos puede usar cada empresa, cómo cobrar por ello y dónde encajan las pasarelas de pago en Growth54.
Un plan define exactamente qué puede hacer una empresa dentro de la plataforma. No es solo un precio — es una lista de permisos.
Define qué módulos están habilitados, cuántos recursos se pueden usar y cuánto cuesta. Se crea una sola vez y aplica a muchas empresas.
Es el vínculo entre una empresa y un plan. Tiene fecha de inicio, fecha de vencimiento y estado: trial, activa, vencida o cancelada.
Cuando un usuario intenta usar un módulo, el sistema verifica si el plan activo de su empresa incluye ese módulo. Si no, devuelve error 403.
En un hosting, el plan Básico te da 1 dominio, 10 GB y sin SSL premium. El plan Business da 10 dominios, 100 GB y SSL incluido. En Growth54 funciona igual: el plan Starter da acceso a keywords y auditorías, el plan Pro desbloquea RRSS, agentes de IA y más recursos.
Diseñados para escalar desde el primer cliente hasta empresas con operación completa asistida por IA.
Cada módulo tiene un slug que se almacena en el JSON del plan. Si el slug está en la lista, el módulo está habilitado.
| Módulo | Slug | Free Trial | Starter | Pro | Enterprise |
|---|---|---|---|---|---|
| Perfil de empresa | company_profile |
✓ | ✓ | ✓ | ✓ |
| Auditoría inicial | auditoria_inicial |
✓ | ✓ | ✓ | ✓ |
| Keywords SEO | keywords |
✕ | 50 máx | 200 máx | ilimitado |
| Artículos SEO | articulos |
✕ | 20/mes | 100/mes | ilimitado |
| Páginas del sitio | paginas |
✕ | ✓ | ✓ | ✓ |
| Productos y servicios | productos_servicios |
✕ | ✓ | ✓ | ✓ |
| Competidores | competitors |
✕ | ✕ | ✓ | ✓ |
| Redes Sociales (RRSS) | rrss |
✕ | ✕ | ✓ | ✓ |
| Integración CMS | cms |
✕ | 1 sitio | ilimitado | ilimitado |
| Agentes IA | agentes_ia |
✕ | ✕ | ✓ | ✓ |
El campo features en la tabla plans es un JSON con esta estructura:
{
"modules": ["company_profile", "auditoria_inicial", "keywords", "articulos"],
"max_keywords": 50,
"max_articulos": 20,
"rrss": false,
"agentes_ia": false
}
El plan lo tiene el usuario, no la empresa. El usuario puede gestionar N empresas según su plan. Simple, sin tablas extra.
Mientras más simple sea la arquitectura, más fácil de programar, de mantener y de explicar al cliente. La regla es: el usuario tiene un plan, el plan dice cuántas empresas puede tener y qué puede hacer en cada una. Eso es todo.
Se puede conservar para historial de pagos y facturación, pero el control de acceso ya no depende de ella. El acceso se verifica directamente desde el plan_id del usuario.
{
"max_companies": 5, ← cuántas empresas puede tener
"modules": [
"company_profile",
"auditoria_inicial",
"keywords",
"articulos",
"rrss",
"agentes_ia"
],
"max_keywords": 200, ← por empresa
"max_articulos": 100, ← por empresa por mes
"rrss": true,
"agentes_ia": true
}
// User.php public function plan(): BelongsTo { return $this->belongsTo(Plan::class); } public function canAccessModule(string $module): bool { if (!$this->plan || !$this->planIsActive()) { return false; } return $this->plan->hasModule($module); } public function planIsActive(): bool { if (!$this->plan_expires_at) return true; // sin vencimiento return $this->plan_expires_at->isFuture(); } public function canCreateCompany(): bool { $max = $this->plan->features['max_companies'] ?? 1; $owned = $this->companies()->wherePivot('role', 'owner')->count(); return $owned < $max; }
// app/Http/Middleware/EnsureUserHasModule.php public function handle(Request $request, Closure $next, string $module): Response { $user = $request->user(); if (!$user->canAccessModule($module)) { return response()->json([ 'message' => 'Tu plan no incluye este módulo.', 'upgrade' => true, ], 403); } return $next($request); }
// OnboardingController::createCompany() — validación que se agrega al inicio if (!$user->canCreateCompany()) { return response()->json([ 'message' => 'Has alcanzado el límite de empresas de tu plan.', 'upgrade' => true, 'max_companies' => $user->plan->features['max_companies'], ], 403); }
El flujo completo desde que el usuario elige un plan hasta que su empresa tiene acceso activo.
POST /api/subscriptions/checkout con { plan_slug: "pro" }POST /api/webhooks/stripe o POST /api/webhooks/mercadopagocompany_subscriptions con status = "active", starts_at = ahora, ends_at = ahora + 30 días.
El backend NUNCA debe activar la suscripción solo porque el usuario dice "pagué". La activación solo ocurre cuando la pasarela confirma el pago mediante un webhook firmado. Esto evita fraudes.
Cada pasarela tiene sus ventajas según el mercado objetivo, la moneda y la complejidad técnica.
POST /api/webhooks/stripe
POST /api/webhooks/mercadopago
POST /api/webhooks/paypal
| Archivo | Responsabilidad |
|---|---|
app/Services/StripeService.php |
Crear sesiones de checkout y manejar cliente Stripe |
app/Http/Controllers/Api/SubscriptionController.php |
Endpoint para elegir plan e iniciar checkout |
app/Http/Controllers/Api/Webhooks/StripeWebhookController.php |
Recibir notificación de pago y activar suscripción |
app/Http/Middleware/EnsureCompanyHasModule.php |
Verificar plan activo antes de cada endpoint |
routes/api.php (modificar) |
Agregar middleware module:X a las rutas protegidas |
| Archivo | Responsabilidad |
|---|---|
src/hooks/useCanAccess.ts |
Hook: ¿el plan activo incluye este módulo? |
src/components/ModuleGate.tsx |
Wrapper que bloquea visualmente si no hay acceso |
src/pages/Planes.tsx |
Pantalla de selección y upgrade de plan |
src/services/subscriptions.ts |
Llamadas API para checkout y estado de suscripción |
Stripe tiene el mejor soporte para suscripciones recurrentes con Laravel. El paquete laravel/cashier (oficial de Laravel) hace que integrar Stripe tome horas en lugar de días. Maneja renovaciones, cancelaciones, facturas y webhooks de forma automática.
Los planes se pueden crear y editar directamente desde la API con un token de administrador. No se necesita desplegar código.
# POST /api/admin/plans { "slug": "starter", "name": "Starter", "price": 29.00, "duration_days": 30, "is_active": true, "features": { "modules": [ "company_profile", "auditoria_inicial", "keywords", "articulos", "paginas" ], "max_keywords": 50, "max_articulos": 20, "rrss": false, "agentes_ia": false } }
# PATCH /api/admin/plans/2 { "features": { "modules": [ "company_profile", "auditoria_inicial", "keywords", "articulos", "paginas", "rrss" ← se agregó este módulo ], "max_keywords": 50, "max_articulos": 20, "rrss": true, ← se habilitó "agentes_ia": false } }
Esto se hace cuando se activa una cuenta nueva o se cambia el plan de forma manual (sin pasarela de pago).
# POST /api/admin/companies/5/subscriptions (endpoint por implementar) { "plan_id": 2, "status": "active", "starts_at": "2026-05-06", "ends_at": "2026-06-06" }
El endpoint /api/auth/me debe devolver los módulos del plan activo de la empresa. El frontend usa esa información para mostrar u ocultar secciones del menú lateral. Hoy ese endpoint existe pero no incluye los módulos del plan — es un campo que falta agregar.
Un checklist honesto del estado del sistema de planes en Growth54 hoy.
planscompany_subscriptionsPlan con hasModule()CompanySubscription con isActive()GET/POST/PATCH/DELETE /api/admin/plansEnsureCompanyHasModulePOST /api/admin/companies/{id}/subscriptions/api/auth/meuseCanAccess() y componente ModuleGateexpiredPlanSeeder.php y correr el seeder.EnsureCompanyHasModuleapi.php./api/auth/meAuthController para devolver módulos del plan activo de la empresa.useCanAccess() y bloqueo visual en frontendlaravel/cashier.Antes de programar el sistema de planes hay que responder esto con el equipo. No es técnico — es estratégico.
Carlos tiene un restaurante. Compra Growth54 para gestionar el SEO de su propio negocio. Solo necesita 1 empresa. No va a gestionar clientes de terceros.
Laura es freelancer SEO. Tiene 6 clientes. Quiere un solo panel para gestionar todos. No quiere pagar 6 planes por separado — quiere 1 plan que le dé 6 empresas.
Growth54 puede atender ambos perfiles con el mismo modelo. El Starter (1 empresa) cubre al dueño de negocio. El Pro y Agency (5–8 empresas) cubren al freelancer y a la agencia. El precio diferencia quién es quién.