A SaaS layer your app adds on top of multi-tenancy — plans, subscriptions, and feature gating — so you can charge differently and limit what each tenant can do. Built to sit on ForgeMultiTenant, which it turns into a true multi-plan product.
ForgeSaas gives your app the commercial machinery of a SaaS product: a catalog of plans, a subscription that ties each tenant to one plan, and a way to gate features and enforce usage limits against that plan. It checks what the current tenant is allowed to do and enforces it for you — either automatically at the routing layer or explicitly from your own code.
Prerequisites:
ForgeSaas requires the routing layer
(for its middleware) and
ForgeDatabaseSQL
(for the plan and subscription tables). It
resolves the active tenant through the
tenant attribute that
ForgeMultiTenant
sets on each request, so pair them together.
The two components solve different problems, and you choose how far to take your product:
Multiple customers, one codebase, data kept apart. Every tenant resolves, gets its own scoped data and connection, and your business code works against the current tenant. There are no plans, no subscriptions, no feature limits. Every tenant is implicitly equal.
Adds the business model: a catalog of plans, a subscription linking each tenant to a plan, and enforcement of what that plan allows. This is where "this tenant is on the Free plan, so it can't use SSO" and "the Pro plan allows 25 users" live.
In practice, ForgeSaas is designed to sit directly on ForgeMultiTenant — its middleware consumes the resolved tenant and its subscriptions are keyed by tenant id. But the two are independent decisions:
Limiting a feature does not mean a SaaS: if your app needs to restrict what some users can do — even without multi-tenancy or billing — ForgeSaas still gives you the plan/feature/limit vocabulary to model it. But its natural home is on top of a multi-tenant app.
php forge.php package:install-module --module=ForgeSaas
Installing migrates and seeds the module's tables and registers its middleware on the web route group. Two tables back the component:
saas_plans — the plan catalog,
each with a slug, its feature list (as JSON),
and its usage limits (as JSON).
tenant_subscriptions — one row
(at most) per tenant, linking a tenant id to a
plan with a status and optional trial/period
timestamps.
The component reads these from the app's central database, so plan and subscription data stays shared across every tenant rather than living inside any one tenant's data.
A plan is a named, versioned-free tier with two dimensions that the rest of the system enforces:
api_access,
custom_domain,
sso). A tenant's plan either
has a feature or it doesn't; this is the
"can it do X?" check.
max_users, and
max_storage_gb). A limit of
-1 means unlimited; a resource
with no entry is treated as effectively
unlimited.
Installing seeds three starter plans you can edit or replace with your own:
# saas_plans (seeded)
# Free — no features; limits: max_users=3, max_storage_gb=1
# Pro — advanced_reports, api_access, custom_domain; max_users=25, max_storage_gb=20
# Enterprise — everything + white_label, priority_support, sso; unlimited (–1, –1)
Create your own from code or the command line. Deleting a plan that still has subscriptions is refused; disabling a plan keeps its subscriptions but marks it inactive.
A subscription ties a tenant to a plan. Each
tenant holds at most one, with a status drawn from
a small enum:
active, trial,
past_due, and canceled.
A subscription also carries optional
trial_ends_at and
current_period_ends_at timestamps.
Assign or change a tenant's plan from the command line, or from your code via the subscription manager:
# Assign the Pro plan to a tenant
php forge.php modules:saas:tenant:assign --tenant=upper --plan=plan-pro
A subscription is considered
active when its status is
active or trial. The
manager resolves the current tenant's subscription
at the start of a request, so feature checks
throughout that request see a consistent picture.
Mark a route handler (or a whole controller) with
a gate attribute, and the feature-gate middleware
answers it against the current tenant's plan —
returning a clear 403 when the plan
doesn't allow it:
use Modules\ForgeSaas\Attributes\RequiresFeature;
use Modules\ForgeSaas\Attributes\RequiresPlan;
use Modules\ForgeSaas\Attributes\WithinLimit;
#[RequiresFeature('api_access')]
final class ApiController
{
#[RequiresPlan('enterprise')]
public function export() { /* ... */ }
#[WithinLimit(resource: 'max_users', table: 'users')]
public function invite() { /* ... */ }
}
RequiresFeature
— the tenant's plan must include the named
feature.
RequiresPlan
— the tenant must be on the named plan (by
slug).
WithinLimit
— counts the rows in a tenant-scoped table and
checks it against the plan's limit for that
resource, refusing once the ceiling is reached.
Attributes placed on a method take precedence over the same attribute on the class. When a gate's handler isn't inside a resolved tenant, the middleware lets the request through unchanged — so central-domain routes are unaffected.
Outside the routing layer, a set of small helpers answer the same questions anywhere in your app — controllers, views, or services:
tenant_can('api_access'); // bool — plan includes the feature
tenant_on_plan('enterprise'); // bool — on the named plan
tenant_subscription_active(); // bool — status is active/trial
tenant_within_limit('max_users', 24); // bool — under the plan's ceiling
tenant_limit('max_storage_gb'); // int — the plan's limit, or -1 / unlimited
tenant_plan(); // ?SaasPlan — the active tenant's plan
The underlying
SubscriptionManager (resolvable from
the container as
SubscriptionManagerInterface) exposes
the same checks plus plan and subscription
management. Build the manager for a specific
tenant with forTenant($tenant) and it
loads that tenant's subscription for the rest of
the call:
use Modules\ForgeSaas\Contracts\SubscriptionManagerInterface;
$saas = $container->get(SubscriptionManagerInterface::class);
$manager = $saas->forTenant($tenant);
$manager->hasFeature('sso'); // bool
$manager->limitFor('max_users'); // int
// Manage plans and subscriptions
$manager->getAllPlans();
$manager->createPlan('Premium', 'premium', ['api_access'], ['max_users' => 50]);
$manager->disablePlan('plan-free');
$manager->assignPlanToTenant($tenantId, 'plan-pro');
$manager->deletePlan('plan-free'); // refused while it has subscriptions
Use these same primitives to build your own billing, upgrade, or checkout flow — the component leaves payment itself to you; it models the plan, the subscription, and the enforcement.
Manage plans and assign tenants from the command line:
# Plans
php forge.php modules:saas:plan:list
php forge.php modules:saas:plan:create --name=Premium --slug=premium \
--features=api_access,custom_domain --limits='{"max_users": 25}'
php forge.php modules:saas:plan:disable --id=plan-free
php forge.php modules:saas:plan:delete --id=plan-old
# Assign a plan to a tenant
php forge.php modules:saas:tenant:assign --tenant=upper --plan=plan-pro
Plan creation and assignment accept interactive prompts, so you can run them bare and answer the questions as they come.
There are no required settings. The component picks up the active tenant automatically from the request attribute that the multi-tenant stack sets, and reads plans and subscriptions from the central database. Define your plan catalog and assign tenants — either during onboarding from code or via the CLI — and the rest of the component works from there.
To see the whole flow wired together — tenant resolution, then plan evaluation on top of it — read the ForgeMultiTenant page, which this component extends.