A cashier can log in, create an invoice, and print a receipt. That is a useful feature. Turning it into a SaaS product introduces another set of questions:
Which business owns the invoice? Can a branch manager issue a refund? What happens when a subscription expires while an export is running?
Those questions define the system's boundaries. Leaving them implicit can make an otherwise clean Laravel application difficult to extend.
Here is a practical checklist for a business-management SaaS. The examples describe an illustrative application with multiple businesses, branches, and staff roles.
1. Define the tenant before choosing the tenancy package
Start with a sentence: A tenant is the business that owns the data and subscription.
Then test that definition against the product:
- Can one business operate several branches?
- Can one person belong to more than one business?
- Can a business have several legal entities or billing accounts?
- Does a branch need an independent subscription, or only restricted visibility?
A branch is not automatically a tenant. Making both concepts interchangeable can complicate reporting, billing, and access rules later.
For a shared-database design, resolve the active tenant through authenticated membership. A tenant identifier supplied by the browser is a selection request, not proof of access.
An illustrative record lookup might look like this:
use Illuminate\Support\Facades\Gate;
// $tenant has already been resolved through authenticated membership.
$invoice = Invoice::query()
->where('tenant_id', $tenant->id)
->findOrFail($invoiceId);
Gate::authorize('view', $invoice);
The query limits the data boundary; the policy decides whether this user may perform this action. The example assumes an Invoice model and a registered policy. It is not a complete tenancy implementation, and the same boundary must cover writes, relationships, exports, and background work.
Laravel's authorization documentation explains how gates and policies organize action checks.
2. Define permissions as business actions
"Admin" and "staff" are usually too broad to describe a business workflow.
A cashier might create sales but need manager approval for refunds. A warehouse employee might view stock without seeing payroll. A manager might see one branch while the owner sees the entire business.
Write a permission matrix before building the navigation:
| Action | Cashier | Branch manager | Business owner |
|---|---|---|---|
| Create a sale | Allowed | Allowed | Allowed |
| Approve a refund | Not allowed | Assigned branch | All branches |
| View consolidated reports | Not allowed | Not allowed | Allowed |
This is an example policy, not a universal rule. Confirm the actual workflow with the business.
Enforce permissions on the server. Hiding a button improves the interface; it does not prevent a direct request to the endpoint.
3. Define subscription states beyond active or inactive
A subscription can move through trial, active, payment overdue, grace period, suspended, and cancelled states. The exact states depend on the product.
For each state, decide what remains available:
- Can staff continue entering transactions during a grace period?
- Can an owner read or export existing records after suspension?
- Does a downgrade affect existing branches, new branches, or both?
- What happens to already queued work?
Keep plan entitlements separate from staff permissions. A business may have access to a feature while a particular staff member does not.
Treat billing events as inputs to an explicit state transition. Verify the event's origin, record its identifier, and design duplicate delivery so that it does not apply the same change twice.
4. Define what must commit together
Creating a sale may involve an invoice, stock movements, and accounting records. Decide which records must succeed or fail together.
For related changes within one database transaction, Laravel provides database transactions. An exception in the transaction closure triggers rollback.
An email is a separate concern. It should not announce a sale that later rolls back.
For a queued job dispatched during a database transaction, Laravel supports waiting for commit:
// Illustrative job: implement the queued handler in your application.
SendInvoiceEmail::dispatch($tenant->id, $invoice->id)->afterCommit();
The queue documentation describes this behavior. After-commit dispatch controls timing; it does not make email delivery exactly-once or guarantee delivery after every infrastructure failure. Decide how important work is recorded, retried, and reconciled.
5. Carry the tenant boundary outside HTTP requests
Background workers do not automatically inherit the browser's current business selection.
Pass the tenant identifier into an export or notification job. Resolve that tenant again and scope every record lookup inside the job. Include the requesting user when the operation needs their permissions, and define whether permissions must be rechecked when the job runs.
Apply the same thinking to caches and files. A cache key such as dashboard:today can mix businesses if it is shared. A more explicit pattern is tenant:{tenant_id}:dashboard:{date}, with additional user or branch dimensions where the result varies by permission.
Private exports also need controlled downloads and expiry rules. A tenant-named folder alone does not authorize access to a file.
6. Define how operations remain explainable
When a refund or stock adjustment changes a report, someone will eventually ask why.
Identify the business events worth recording: who requested a change, which tenant and branch it affected, what changed, and when it happened. Distinguish a requested action from an approved or completed action.
Avoid placing passwords, tokens, or entire sensitive request payloads in logs. Record enough context to investigate the workflow without turning diagnostics into an unnecessary copy of business data.
Also define backup ownership and recovery expectations. A full database backup is useful, but recovering one tenant's records without disturbing others may require a different procedure. Rehearse the recovery process.
7. Test boundaries, not only happy paths
A successful sale proves that one authorized workflow works. It does not prove that tenant isolation works.
Include failure cases such as:
- A user changes the invoice ID to one owned by another tenant.
- A cashier calls the refund endpoint directly.
- A branch-restricted manager requests an all-branch export.
- A worker handles an export after the requesting user loses access.
- A billing event arrives twice.
- An email fails after the sale commits.
- A plan downgrade takes effect while a job is queued.
- A tenant recovery procedure is executed in a test environment.
For each case, specify the expected result and the records that must remain unchanged. That makes the test useful to both engineering and product teams.
A useful starting deliverable
Before implementing the full SaaS, prepare five small documents: a tenant definition, a permission matrix, a subscription-state table, a transaction-and-job map, and a boundary-test checklist.
They will not replace engineering work. They make the decisions visible before those decisions spread across controllers, workers, reports, and support procedures.
Which boundary has caused the most rework in your SaaS project: tenancy, permissions, subscriptions, or background jobs?
Published by LionCoders, a SaaS company and custom software development agency. Explore our work at lion-coders.com.
Top comments (0)