Skip to main content

Overview

SaaS Starter Vue includes a flexible subscription plan system that allows you to:
  • Create multiple pricing tiers
  • Attach features to plans
  • Manage tenant subscriptions
  • Track billing periods
  • Support free and paid plans
Plans are managed from the central domain and assigned to tenants during creation or upgrade.

Plan Structure

Plan Model

Plans are stored in the plans table with the following attributes:
// app/Models/System/Plan.php:15
protected $fillable = [
    'name',
    'slug',
    'description',
    'price',
    'currency',
    'duration_in_days',
    'is_free',
    'is_active',
];

Database Schema

// database/migrations/2026_02_06_063829_create_plans_table.php:14
Schema::create('plans', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('slug')->unique();
    $table->text('description')->nullable();
    $table->decimal('price', 10, 2);
    $table->string('currency')->default('USD');
    $table->integer('duration_in_days');
    $table->boolean('is_free')->default(false);
    $table->boolean('is_active')->default(true);
    $table->timestamps();
    $table->softDeletes();
});

Plan Properties

name
string
required
Display name of the plan (e.g., “Pro”, “Enterprise”)
slug
string
required
Unique identifier for the plan (e.g., “pro”, “enterprise”)
description
text
Detailed description of what’s included in the plan
price
decimal
Plan price with 2 decimal precision (e.g., 29.99)
currency
string
default:"USD"
Currency code (USD, EUR, GBP, etc.)
duration_in_days
integer
Billing cycle length in days (30 for monthly, 365 for yearly)
is_free
boolean
default:"false"
Whether this is a free plan
is_active
boolean
default:"true"
Whether the plan is available for new subscriptions

Managing Plans

List Plans

View all subscription plans:
// routes/web.php:67
Route::resource('plans', \App\Http\Controllers\System\PlanController::class)
    ->middleware(['auth', 'verified'])
    ->except(['show', 'create', 'edit']);
Endpoint: GET /plans

Create a Plan

Create a new subscription plan:
// app/Http/Controllers/System/PlanController.php:39
public function store(StorePlanRequest $request): RedirectResponse
{
    $this->planService->createPlan($request->validated());

    return redirect()
        ->route('plans.index')
        ->with('success', 'Plan created successfully');
}
Endpoint: POST /plans Example Request:
{
  "name": "Professional",
  "slug": "pro",
  "description": "For growing businesses",
  "price": 49.99,
  "currency": "USD",
  "duration_in_days": 30,
  "is_free": false,
  "is_active": true
}

Update a Plan

Modify an existing plan:
// app/Http/Controllers/System/PlanController.php:48
public function update(UpdatePlanRequest $request, Plan $plan): RedirectResponse
{
    $this->planService->updatePlan($plan, $request->validated());

    return redirect()
        ->route('plans.index')
        ->with('success', 'Plan updated successfully');
}
Endpoint: PUT /plans/{plan}
Changing plan pricing or features may affect existing subscribers. Consider creating a new plan version instead.

Delete a Plan

Soft delete a plan:
// app/Http/Controllers/System/PlanController.php:58
public function destroy(Plan $plan): RedirectResponse
{
    // Check if plan has active tenants
    if ($plan->tenants()->count() > 0) {
        return redirect()
            ->route('plans.index')
            ->with('error', 'Cannot delete plan with active tenants');
    }

    $this->planService->deletePlan($plan);

    return redirect()
        ->route('plans.index')
        ->with('success', 'Plan deleted successfully');
}
Endpoint: DELETE /plans/{plan}
Plans with active tenants cannot be deleted. You must migrate tenants to another plan first or use soft deletes.

Plan Features

Feature Model

Features can be attached to plans:
// app/Models/System/Feature.php:13
protected $fillable = [
    'name',
    'code',
    'description',
];

Feature Schema

// database/migrations/2026_02_06_064706_create_features_table.php:14
Schema::create('features', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('code')->unique();
    $table->text('description')->nullable();
    $table->timestamps();
});

Attaching Features to Plans

Features are attached via a pivot table with values:
// app/Models/System/Plan.php:37
public function features(): BelongsToMany
{
    return $this->belongsToMany(Feature::class, 'feature_plan')
                ->withPivot('value')
                ->withTimestamps();
}
The value field stores the feature limit or configuration: Example Features:
// Attach features to a plan
$plan->features()->attach($feature->id, ['value' => '100']);

// Examples:
// - max_users: "10"
// - storage_gb: "50"
// - api_calls_per_month: "10000"
// - custom_domain: "true"

Checking Feature Access

Check if a tenant’s plan includes a feature:
$tenant = Tenant::find($tenantId);
$plan = $tenant->plan;

if ($plan->features->contains('code', 'custom_domain')) {
    // Feature is available
    $value = $plan->features->where('code', 'custom_domain')->first()->pivot->value;
}

Subscriptions

Subscription Model

Tenants can have multiple subscription records:
// app/Models/System/Subscription.php:13
protected $fillable = [
    'tenant_id',
    'plan_id',
    'stripe_id',
    'stripe_status',
    'stripe_price',
    'quantity',
    'trial_ends_at',
    'ends_at',
];

Subscription Schema

// database/migrations/2026_02_06_064712_create_subscriptions_table.php:14
Schema::create('subscriptions', function (Blueprint $table) {
    $table->id();
    $table->string('tenant_id');
    $table->foreign('tenant_id')->references('id')->on('tenants')->cascadeOnDelete();
    $table->foreignId('plan_id')->constrained('plans');
    $table->string('stripe_id')->nullable()->index();
    $table->string('stripe_status')->nullable();
    $table->string('stripe_price')->nullable();
    $table->integer('quantity')->nullable();
    $table->timestamp('trial_ends_at')->nullable();
    $table->timestamp('ends_at')->nullable();
    $table->timestamps();
});

Tenant-Plan Relationship

// app/Models/System/Tenant.php:40
public function plan()
{
    return $this->belongsTo(Plan::class);
}

public function subscriptions()
{
    return $this->hasMany(Subscription::class);
}

public function currentSubscription()
{
    return $this->hasOne(Subscription::class)->latestOfMany();
}

Plan Assignment

During Tenant Creation

Plans are assigned when creating a tenant:
// app/Services/System/TenantService.php:25
$tenant = Tenant::create([
    'id' => Str::uuid(),
    'name' => $data['name'],
    'owner_name' => $data['owner_name'],
    'owner_email' => $data['owner_email'],
    'owner_password' => Hash::make($data['owner_password']),
    'plan_id' => $data['plan_id'] ?? null,
    'status' => $data['status'] ?? 'Trial',
    'is_active' => true,
    'tenancy_db_name' => $dbName,
]);

Guest Registration (Free Plan)

Guest registration automatically assigns the free plan:
// app/Http/Controllers/System/GuestRegisterController.php:36
$freePlan = Plan::where('price', 0)->first();

$data = [
    'plan_id' => $freePlan ? $freePlan->id : null,
    'status' => 'Active',
    // ...
];
Create at least one free plan (price = 0) for guest registration to work properly.

Trial Periods

Tenants can have trial periods before requiring payment:
// app/Models/System/Tenant.php:16
protected $casts = [
    'subscription_ends_at' => 'datetime',
    'trial_ends_at' => 'datetime',
    'canceled_at' => 'datetime',
];

Setting Trial Period

$tenant->update([
    'status' => 'Trial',
    'trial_ends_at' => now()->addDays(14),
]);

Checking Trial Status

if ($tenant->status === 'Trial' && $tenant->trial_ends_at->isFuture()) {
    // Tenant is in active trial
} elseif ($tenant->status === 'Trial' && $tenant->trial_ends_at->isPast()) {
    // Trial has expired
}

Stripe Integration

The subscription model includes Stripe fields for payment processing:
stripe_id
string
Stripe subscription ID
stripe_status
string
Stripe subscription status (active, canceled, past_due, etc.)
stripe_price
string
Stripe price ID
Stripe integration requires additional setup. See the Payments documentation for details.

Plan Statistics

View plan usage statistics:
// app/Http/Controllers/System/PlanController.php:31
'stats' => [
    'total' => Plan::count(),
    'active' => Plan::where('is_active', true)->count(),
    'inactive' => Plan::where('is_active', false)->count(),
]

Example Plan Setup

Here’s a complete example of creating a tiered pricing structure:
1

Create Free Plan

Plan::create([
    'name' => 'Free',
    'slug' => 'free',
    'description' => 'Perfect for trying out',
    'price' => 0.00,
    'currency' => 'USD',
    'duration_in_days' => 30,
    'is_free' => true,
    'is_active' => true,
]);
2

Create Pro Plan

Plan::create([
    'name' => 'Professional',
    'slug' => 'pro',
    'description' => 'For growing businesses',
    'price' => 49.99,
    'currency' => 'USD',
    'duration_in_days' => 30,
    'is_free' => false,
    'is_active' => true,
]);
3

Create Enterprise Plan

Plan::create([
    'name' => 'Enterprise',
    'slug' => 'enterprise',
    'description' => 'For large organizations',
    'price' => 199.99,
    'currency' => 'USD',
    'duration_in_days' => 30,
    'is_free' => false,
    'is_active' => true,
]);

Best Practices

  1. Version Plans - Create new plan versions instead of modifying existing ones
  2. Grandfather Existing Customers - Let existing subscribers keep their plan
  3. Clear Descriptions - Make plan differences obvious to customers
  4. Feature Limits - Use the feature system to enforce plan limitations
  5. Trial Periods - Offer trials to increase conversions
  6. Annual Discounts - Use duration_in_days: 365 for annual plans with discounts

Common Patterns

Monthly vs Annual Plans

Create both billing cycles:
// Monthly
Plan::create([
    'name' => 'Pro Monthly',
    'slug' => 'pro-monthly',
    'price' => 49.99,
    'duration_in_days' => 30,
]);

// Annual (with discount)
Plan::create([
    'name' => 'Pro Annual',
    'slug' => 'pro-annual',
    'price' => 499.99, // ~17% discount
    'duration_in_days' => 365,
]);

Metered vs Flat Pricing

Use the quantity field in subscriptions for metered billing:
$subscription->update([
    'quantity' => $userCount, // Bill based on number of users
]);

Build docs developers (and LLMs) love