In the previous article, I showed you how to build a CRM using Backpack for Laravel. Now, let’s take it a step further and turn it into...
In the previous article, I showed you how to build a CRM using Backpack for Laravel. Now, let’s take it a step further and turn it into a SaaS app. Here’s exactly how I did it.
Before diving in, here's a quick context: when you're building SaaS, you need to decide how data is separated between users. By company, domain, organization, or simply... per user?
For this example, I’m going with the simplest model — SaaS per user. That means every logged-in user sees only their own data. No multi-tenancy complexities (for now 😅). To achieve this, I'll add a user_id
column to each of our CRM models and use it to separate the data. That's our differentiator. Also, I didn’t need a separate frontend for login or registration. Backpack already includes clean, ready-to-go authentication screens — so I used its built-in login and register pages.
user_id
to Each CRM Table and Limit Data AccessThis step is the foundation of our per-user SaaS logic. We want each user to see and manage only their own data, whether it's customers, contacts, or deals. I started by adding a user_id
field to all the tables.
user_id
to Each CRM TableCreate a migration:
php artisan make:migration add_user_id_to_crm_tables
Then, update the migration like this:
// Add user_id to customers
Schema::table('customers', function (Blueprint $table) {
$table->foreignId('user_id')->nullable()->constrained()->onDelete('set null');
});
// Add user_id to deals
Schema::table('deals', function (Blueprint $table) {
$table->foreignId('user_id')->nullable()->constrained()->onDelete('set null');
});
// Add user_id to contacts
Schema::table('contacts', function (Blueprint $table) {
$table->foreignId('user_id')->nullable()->constrained()->onDelete('set null');
});
Now, every record can be tied to a user. I made the user_id
nullable, just in case I ever need to assign orphaned records, and used onDelete('set null')
so I don’t accidentally delete everything if a user is removed.
user_id
on CreateI didn’t want to rely on the form to set user_id
, so I automated it.
You’ve got two options here:
public function setup(){
...
Customer::creating(function ($entry) {
$entry->user_id = backpack_user()->id;
});
}
protected static function booted()
{
static::creating(function ($entry) {
if (empty($entry->user_id) && backpack_user()) {
$entry->user_id = backpack_user()->id;
}
});
}
This way, any time a model gets created, Laravel sets the current logged-in user’s ID automatically.
Next, I need to make sure each user only sees their own stuff. Backpack makes this a breeze. In each CRUD controller (like CustomerCrudController
), I just added addBaseClause
inside setup()
:
CRUD::addBaseClause('where', 'user_id', '=', backpack_user()->id);
Now, when a user logs in, they only see the data they own. Same for every other user. Even if someone changes the URL and tries to access someone else's record, they receive a 404.
Let’s say I’m creating a deal and choosing a customer from a select
field — It should only show my customers.
So in the DealCrudController
, I updated the customer_id
field like this:
CRUD::field([
'type' => 'select',
'name' => 'customer_id',
'model' => \App\Models\Customer::class,
'attribute' => 'name',
+ 'options' => (function ($query) {
+ return $query->where('user_id', backpack_user()->id)->get();
+ }),
]);
Clean and user-specific. Do the same for your other components.
We can now add a billing layer to make this a real SaaS product.
I wanted a custom page within the panel. I generated it with:
php artisan backpack:page Subscription
Then edited the files here:
resources/views/admin/subscription.blade.php
app/Http/Controllers/Admin/SubscriptionController.php
I used it to show a basic billing page, but you could put anything here—stats, plans, upgrades, whatever.
I used Laravel Cashier, which is quick to set up. But you can also integrate your own system if needed.
Now that we’ve got billing in place, we need to protect the app. If their subscription has expired or they never subscribed in the first place, they should be gently redirected to the subscription page.
Create a custom middleware that checks the subscription before any route access. If it's expired, redirect to the subscription page.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class Subscribed {
public function handle(Request $request, Closure $next): Response {
if (! $request->user()?->subscribed()) {
// Redirect user to subscription page and ask them to subscribe...
return redirect(backpack_url('subscription'));
}
return $next($request);
}
}
This keeps your app protected and subscription-based in practice.
Let’s now secure the Admin area. We want only Admins to see Users, Roles & Permission CRUD. We don’t want regular users editing each other, so I created a custom User controller.
Use Backpack's Permission Manager to create Admin and User roles.
Now, I wanted all every users to automatically get a “User” role—so they can start using the app right away, and I can hide admin-only features from them.
Here’s how I did that in the User model:
protected static function boot()
{
parent::boot();
static::created(function ($user) {
if (! $user->hasRole('User')) {
$user->assignRole('User');
}
});
}
Now every new sign-up is ready to go with the right permissions.
Create & extend UserCrudController
to allow only Admins to access it:
<?php
namespace App\Http\Controllers\Admin;
use App\Models\User;
use Backpack\PermissionManager\app\Http\Controllers\UserCrudController as CrudController;
use Backpack\CRUD\app\Library\CrudPanel\CrudPanelFacade as CRUD;
class UserCrudController extends CrudController
{
public function setup()
{
CRUD::setModel(User::class);
CRUD::setRoute(config('backpack.base.route_prefix') . '/user');
CRUD::setEntityNameStrings('user', 'users');
// Only allow access to users with Admin role
if (! backpack_user()->hasRole('Admin')) {
CRUD::denyAccess(['list', 'create', 'update', 'delete', 'show']);
}
}
}
Then bind it in your AppServiceProvider
to swap them:
public function register(): void
{
$this->app->bind(
\Backpack\PermissionManager\app\Http\Controllers\UserCrudController::class,
\App\Http\Controllers\Admin\UserCrudController::class
);
}
Now only Admins
can access the user list.
And finally, I cleaned up the sidebar so that regular users don’t even see admin stuff:
@if (backpack_user()->hasRole('Admin'))
<x-backpack::menu-dropdown title="Authentication" icon="la la-user">
<x-backpack::menu-dropdown-item title="Users" icon="la la-user" :link="backpack_url('user')" />
<x-backpack::menu-dropdown-item title="Roles" icon="la la-group" :link="backpack_url('role')" />
<x-backpack::menu-dropdown-item title="Permissions" icon="la la-key" :link="backpack_url('permission')" />
</x-backpack::menu-dropdown>
@endif
Now the UI is clean and focused—no distractions, no confusion.
With just a few tweaks, I turned my CRM into a basic SaaS app:
user_id
to models and restrict visibility.It’s super simple, but a solid base for any SaaS you want to build with Backpack.
Next time, I might try team accounts with custom domain support or multi-database tenancy. But for now, this is a great SaaS starter pack.
Let me know what you build with it 👇
Subscribe to our "Article Digest". We'll send you a list of the new articles, every week, month or quarter - your choice.
What do you think about this?
Wondering what our community has been up to?