Speeding Up Laravel Tests with tmpfs (MySQL in RAM)
If your Laravel test suite is painfully slow and does a lot of database work (multi-tenancy, migrations, seeding), the bottleneck is almost…
Building a SaaS with Laravel Jetstream's team features and Spark for billing. Here's how to restrict team access based on subscription plans.
We'll implement a complete solution to:
We want to implement the following business rules:
First, let's set up our plans in config/spark.php. The important part here is to use the id field to identify plans, which we'll use later for permission checks:
'billables' => [
'user' => [
'model' => Team::class,
'trial_days' => 5,
'default_interval' => 'monthly',
'plans' => [
[
'id' => 'pro', // We'll use this ID for checks
'name' => 'Pro',
'monthly_id' => env('SPARK_PRO_MONTHLY_PLAN'),
'yearly_id' => env('SPARK_PRO_YEARLY_PLAN'),
'features' => [
'...",
],
],
[
'id' => 'team', // We'll use this ID for checks
'name' => 'Team',
'monthly_id' => env('SPARK_TEAM_MONTHLY_PLAN'),
'yearly_id' => env('SPARK_TEAM_YEARLY_PLAN'),
'features' => [
'...',
'Add team members (max 5)',
],
],
],
],
],
Create or update your app/Policies/TeamPolicy.php to implement the team member restrictions:
public function addTeamMember(User $user, Team $team): bool
{
// Must be team owner
if (!$user->ownsTeam($team)) {
return false;
}
// Must have an active subscription
$subscription = $team->subscription();
if (!$subscription) {
return false;
}
// Must be on team plan (not basic or pro)
$currentPlanId = $subscription->sparkPlan->id;
if ($currentPlanId !== 'team') {
return false;
}
// Check if team has reached member limit (5)
if ($team->allUsers()->count() >= 5) {
return false;
}
return true;
}
Modify your team management component (resources/js/Pages/Teams/Partials/TeamMemberManager.vue) to show appropriate messages:
<template>
<FormSection @submitted="addTeamMember">
<template #title>
Add Team Member
</template>
<template #form>
<!-- Show message if no subscription or not on team plan -->
<div v-if="!hasTeamPlan" class="col-span-6">
<div class="rounded-md bg-yellow-50 p-4">
<div class="flex">
<div class="flex-shrink-0">
<ExclamationTriangleIcon class="h-5 w-5 text-yellow-400" />
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-yellow-800">
Team Plan Required
</h3>
<div class="mt-2 text-sm text-yellow-700">
<p>Team member management is only available with our Team plan.</p>
<Link
:href="route('pricing')"
class="font-semibold text-yellow-800 hover:text-yellow-600"
>
Upgrade your subscription
</Link>
to invite team members.
</div>
</div>
</div>
</div>
</div>
<!-- Show message if team member limit reached -->
<div v-else-if="reachedMemberLimit" class="col-span-6">
<div class="rounded-md bg-yellow-50 p-4">
<!-- Similar warning for member limit -->
</div>
</div>
<!-- Show add member form only if on team plan and under member limit -->
<template v-else>
<!-- Your existing form fields -->
</template>
</template>
</FormSection>
</template>
<script setup>
const hasTeamPlan = computed(() => {
const subscription = props.team.subscription;
return subscription && subscription.spark_plan.id === 'team';
});
const reachedMemberLimit = computed(() => {
return props.team.users.length >= 5;
});
</script>
Policy Layer: The TeamPolicy enforces the business rules at the backend level, ensuring that even API requests follow the restrictions.
UI Layer: The Vue component provides immediate feedback to users about:
Plan Identification: We use the plan's id from the Spark config rather than Paddle plan IDs, making it easier to maintain and modify plans without changing the code.
Give Vroni a GitHub issue, bug report, spec, or rough idea. It reads the repo, plans the change, writes code, runs checks, and works toward a review-ready pull request.
Take a look at vroni.com