Laravel SaaS Starter: Teams, Stripe Billing, Horizon (Production Guide)

Laravel SaaS Starter: Teams, Stripe Billing, Horizon & Octane (Production Guide)

You’re here to ship, not write boilerplate. This starter gives you opinionated building blocks: teams, roles and permissions, Stripe billing and webhooks, queues via Horizon, and an Octane-ready structure so you can focus on product instead of plumbing.

Repo: laravel-saas-starter
Billing Package (Packagist): budventure/billing

What You Get (At a Glance)

  • Team model, invites, roles/permissions
  • Subscriptions, trials, invoices via Stripe
  • Durable webhooks (signature verification + idempotency)
  • Horizon queues with sane defaults
  • Octane-ready structure for when you need more speed

1) Project Layout That Won’t Fight You Later

Why: keep billing isolated so you can version, reuse, and test it independently

apps/web # main Laravel app
packages/billing # Stripe/webhooks routes + provider (also published on Packagist)
routes/ # web/api entrypoints
resources/ # views, mail, assets

Install the billing package from Packagist (recommended):

composer require budventure/billing:^0.1

2) Teams, Roles & Permissions That Scale

Reality check: “is owner?”.checks don’t scale. Make it explicit.

  • Scope everything by team_id.
  • Define roles → permissions (e.g., billing:manage, members:invite, resources:publish).
  • Store the role on the pivot between user ↔ team.
  • Emit audit events for invites, role updates, and membership changes.

Policy example (short and obvious)

public function manageBilling(User $user, Team $team): bool
{
    return $user->inTeam($team) && $user->can('billing:manage');
}

Invite link (signed + short-lived)

URL::temporarySignedRoute(
  'invites.accept',
  now()->addHours(24),
  ['token' => $invite->token]
);

3) Stripe Billing: Fast Path to Revenue

Set these in production first:

STRIPE_KEY=pk_live_********
STRIPE_SECRET=sk_live_********
STRIPE_WEBHOOK_SECRET=whsec_********

Package routes (auto-registered):

  • POST /api/billing/webhook
  • GET /api/billing/ping (sanity)
  • POST /api/billing/portal (optional customer portal)
  • POST /api/billing/subscribe (example subscription flow)

Why: the package handles signature verification and idempotency, so webhooks are safe to retry.

Verify your Stripe config:

// config/services.php
'stripe' => [
  'key' => env('STRIPE_KEY'),
  'secret' => env('STRIPE_SECRET'),
  'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
],

Local test with Stripe CLI:

stripe listen --forward-to http://localhost:8000/api/billing/webhook
stripe trigger checkout.session.completed

Common Production Traps (Do Not Gloss Over)

  • Do not hardcode plan IDs in controllers — source them from config or the database.
  • Upgrades: prorate by default (happy path for customers).
  • Downgrades mid-cycle: often no-prorate plus schedule the change at period end.
  • Always ACK webhooks quickly and push heavy work (emails, invoices, sync) onto a queue.

4) Webhooks That Never Lose Events

Why: network flaps and retries are normal; your job is to be idempotent.

Skeleton handler:

public function __invoke(Request $request)
{
  $payload    = $request->getContent();
  $signature = $request->header('Stripe-Signature');

  $event = WebhookSignature::verify($payload, $signature, config('services.stripe.webhook_secret'));

  // idempotency guard
  if (WebhookEvent::alreadyHandled($event->id)) {
    return response()->noContent();
  }

  dispatch(new HandleStripeEvent($event->type, $event->data));
  WebhookEvent::markHandled($event->id);

  return response()->noContent();
}

Checklist:

  • Verify the signature on every request.
  • Idempotency by event.id (or Stripe’s id field).
  • Return a 2xx quickly and enqueue heavy work.
  • Log minimal context (type, id) for audits and debugging.

5) Queues with Horizon: Boring Reliability

Why: webhooks, emails, and billing tasks shouldn’t hold HTTP requests hostage.

Enable Horizon:

composer require laravel/horizon
php artisan horizon:install
php artisan migrate

Secure Dashboard (only admins):

// app/Providers/HorizonServiceProvider.php
Horizon::auth(fn($request) => optional($request->user())->is_admin);

Forge (Supervisor) Baseline:

[program:horizon]
process_name=%(program_name)s
command=php /home/forge/yourapp/current/artisan horizon
autostart=true
autorestart=true
user=forge
redirect_stderr=true
stdout_logfile=/home/forge/.horizon.log

Sizing (start small, measure weekly)

  • Small: 1–2 workers @ concurrency 5–10 for default, 1 worker dedicated for webhooks.
  • Medium: split webhooks, billing, emails into dedicated queues with 2–4 workers each.
  • Set visibility timeout ≥ your longest job run.

6) Octane: The “Later” Lever

When to flip it on: p95 request time is CPU-bound in profiles, and most work is stateless

composer require laravel/octane --with-all-dependencies
php artisan octane:install
php artisan octane:start --server=swoole

Do a stateless audit first: no per-request mutable singletons, clean up global state in middleware, use cache/DB for shared context.

7) Deploy Notes (Forge & Vapor)

Forge (classic VPS)

  • PHP-FPM + Nginx.
  • Redis + Horizon (Supervisor) for queues.
  • Let’s Encrypt with auto-renew.
  • Daily DB backups with offsite copies.
  • Fail2ban or Cloudflare WAF on /api/billing/webhook if possible.

Vapor (serverless)

  • Great for spiky workloads, queues via SQS.
  • Use RDS for persistent DB and ElastiCache for Redis.
  • Put the webhook behind API Gateway with strict routes.

Post-deploy warmups (if you SSR/ISR anywhere):

# hit top pages to build caches before users do
curl -sS https://yourapp.com/ | true
curl -sS https://yourapp.com/pricing | true

8) CI Sanity Checks (Catch Regressions Early)

Routing smoke test:

test('billing ping route exists', function () {
  $route = collect(\Route::getRoutes())->firstWhere('action.as', 'billing.ping');
  expect($route)->not->toBeNull();
});

Webhook signature unit:

test('rejects invalid stripe signature', function () {
  $this->withHeaders(['Stripe-Signature' => 'bad'])
    ->postJson('/api/billing/webhook', '{}')
    ->assertStatus(400);
});

9) Production Checklist (Pin This)

  • STRIPE_* secrets set and rotated quarterly.
  • Webhook signature verified and idempotent by event.id.
  • Horizon running; queues split; visibility timeout set correctly.
  • 2xx returned on webhook fast; heavy work always queued.
  • Role/permission policies exist for all billing actions.
  • Audit events emitted for invites, role changes, and billing changes.
  • Nginx restricts webhook path (method/UA/rate limits where possible).
  • Backups not just configured, but tested.

Services & Help

Need senior hands to audit or build your SaaS?
React/Next & Laravel delivery sprints: https://budventure.technology/reactjs-development

Want scope, timelines, and checkpoints first? Book a 15-minute discovery call:  https://budventure.technology/connect

Copy-Paste Commands (Index Card)

Install Billing

composer require budventure/billing:^0.1

Horizon

composer require laravel/horizon
php artisan horizon:install && php artisan migrate
php artisan horizon

Stripe CLI Dev

stripe login
stripe listen --forward-to http://localhost:8000/api/billing/webhook
stripe trigger invoice.payment_succeeded

Octane (Later)

composer require laravel/octane --with-all-dependencies
php artisan octane:start --server=swoole

Where This Starter Fits in Your Growth Path

  • MVP month 1–2: teams, invites, roles, basic subscriptions.
  • Month 3–4: annual plans, coupons, usage metering.
  • Scale: Horizon tuning, Octane, multi-region read replicas, SSO.

Starting a new project or
want to collaborate with us?

support@budventure.technology +91 99241 01601   |    +91 93161 18701