Skip to content

Tenancy Kit documentation

Multi-tenant Laravel 12 SaaS starter · version 1.0.0

  • Requirements
  • Installation
  • Configuration
  • Feature usage
  • FAQ
  • Deploy with Laravel Forge or Ploi
  • Test suite and code quality
  • Working on the kit with AI coding agents
  • Upgrading
  • Changelog

Requirements

Component Requirement
PHP 8.2+
PHP extensions bcmath, ctype, curl, fileinfo, intl, mbstring, openssl, pdo (+ pdo_mysql / pdo_pgsql / pdo_sqlite), tokenizer, xml, zip, sodium
Database MySQL 8.0+, MariaDB 10.6+ or PostgreSQL 15+ in production; SQLite for local use and tests (see Compatibility)
Cache / queue File cache + database queue drained by cron (works on cPanel shared hosting); Redis + Horizon optional on a VPS
Cron * * * * * php artisan schedule:run
Composer & Node Composer 2; Node 20+ only if you rebuild front-end assets (prebuilt assets ship in public/build)
Web server Any PHP host with the web root at public/; wildcard subdomain DNS for {tenant}.yourapp.com; Caddy (on-demand TLS) or Cloudflare for SaaS for tenant custom domains
Payments (optional) Stripe account and/or Paddle Billing account
DB-per-tenant mode Database user with CREATE DATABASE rights

Stack: Laravel 12 · Filament 5 / Livewire 4 · Laravel Cashier (Stripe) 15 · Cashier Paddle 2 · spatie/laravel-permission 6 (teams) · Sanctum · Socialite · Pest · Larastan · Pint.

Compatibility (1.0.0)

Platform Version Status
PHP 8.2 Tested in CI (Pest, both tenancy modes; also Larastan and the browser job)
PHP 8.3 Tested in CI (Pest, both tenancy modes)
PHP 8.4 Tested in CI (Pest, both tenancy modes)
SQLite 3 (bundled with PHP, pdo_sqlite) Tested in CI (all tests run on in-memory SQLite); for local use and tests, not production
MySQL 8.0+ Supported per docs (not in CI)
MariaDB 10.6+ Supported per docs (not in CI)
PostgreSQL 15+ Supported per docs (not in CI)

CI: .github/workflows/ci.yml (PHP from composer.json: ^8.2). Database versions are from docs/install.md. CI runs no MySQL, MariaDB or PostgreSQL job.

Also: Laravel 12.x · Filament 5.x / Livewire 4.x · single-DB and DB-per-tenant tenancy (CI runs both) · cPanel shared hosting, VPS, Laravel Forge, Ploi.

Installation

Option A: Laravel Sail (Docker, optional local dev box)

Docker is not needed to run the kit. Sail is included (compose.yaml: PHP 8.4, MySQL 8, Mailpit) for buyers who prefer a container.

unzip tenancy-kit-1.0.0.zip -d my-saas && cd my-saas
composer install                                # the zip ships production vendor/; this adds Sail, Pest, Larastan, Pint
cp .env.example .env
# .env: DB_HOST=mysql DB_DATABASE=tenancy DB_USERNAME=sail DB_PASSWORD=password MAIL_HOST=mailpit MAIL_PORT=1025 APP_URL=http://localhost
./vendor/bin/sail up -d
./vendor/bin/sail artisan key:generate
./vendor/bin/sail artisan kit:install --demo

Open http://localhost/admin and http://localhost/app. Mail goes to Mailpit at http://localhost:8025. For tenant subdomains locally use northwind.localhost, or path lookup /t/northwind.

Option B: Plain PHP / CLI

cp .env.example .env            # set DB_*, APP_URL, TENANCY_* values
composer install
php artisan key:generate
php artisan kit:install         # interactive

Non-interactive:

php artisan kit:install --no-interaction --name="My SaaS" --url=https://saas.example.com \
  --tenancy=single --admin-email=you@example.com --admin-password='at-least-10-chars' --license=AUCIP-XXXXX-XXXXX-XXXXX-XXXXX [--demo]

The installer checks requirements, writes .env (app name, URL, tenancy mode, central domain), migrates, seeds the Free/Starter/Pro plans, creates the super-admin, optionally seeds demo data, and writes storage/installed.lock. Re-run with --force.

Option C: cPanel / shared hosting (no SSH, Docker, Redis or root)

Tested profile: single-database tenancy, MySQL, file cache/session, database queue drained by cron, path and wildcard-subdomain tenant lookup.

  1. Upload: cPanel → File Manager → upload tenancy-kit-1.0.0.zip and Extract. Recommended: extract to e.g. /home/USER/tenancy-kit and set the domain's Document Root to /home/USER/tenancy-kit/public (cPanel → Domains → Manage). If you can't change the document root, extract into public_html; the bundled root .htaccess + index.php forward to public/ and deny access to code, .env, vendor/ and storage/. Make sure storage/ and bootstrap/cache/ are writable.

  2. PHP: cPanel → Select PHP Version / MultiPHP Manager: PHP 8.2+ with intl, sodium, zip, bcmath, pdo_mysql, fileinfo.

  3. Database: cPanel → MySQL Databases: create a database and user, and grant the user All privileges.

  4. Web installer: open https://your-domain/install (a fresh upload redirects there and creates .env + APP_KEY). Enter your licence key, app name/URL, the database and the super-admin. If the licence server is unreachable the install still finishes and the key is retried daily (14-day grace); a rejected key stops the install. The installer locks itself afterwards.

  5. Cron (one line, every minute):

    cd /home/USER/tenancy-kit && /usr/local/bin/php artisan schedule:run >> /dev/null 2>&1
    

Use your host's PHP path (e.g. /opt/cpanel/ea-php82/root/usr/bin/php). This drains the database queue, re-verifies the licence daily and re-checks custom domains hourly. 6. Tenant URLs: * Path: https://your-domain/t/{slug} and the tenant panel at /app/{slug}. These work immediately. * Subdomains: create the domain *.your-domain in cPanel on the same document root, plus a wildcard DNS record *. For HTTPS use AutoSSL if your host issues wildcard certificates, otherwise Cloudflare proxy. * Custom domains: after the tenant's TXT record verifies, add the domain as an Alias/Parked domain in cPanel (AutoSSL covers it), or use Cloudflare for SaaS. 7. Storage link: if public/storage is missing, run php artisan storage:link from cPanel Terminal, or use S3-compatible storage.

Public demo build

tenancy-kit-1.0.0-demo.zip contains a DEMO_PACKAGE marker: the installer skips the licence step, writes DEMO=true and always seeds demo data. Demo mode hides the licence banner and makes Plans, Tenants and Users read-only. The cron resets the demo data hourly.

Disable the web installer later with AUCIP_WEB_INSTALLER=false.

Demo logins (--demo)

Role Email Password
Tenant owner (Northwind Studio, Harborlane Logistics) owner@demo.test password
Member (Northwind Studio) member@demo.test password
Super-admin the email/password you entered in the installer

After install

  • Run a queue worker (php artisan horizon or queue:work), or keep QUEUE_VIA_CRON=true on shared hosting, and add the scheduler cron.
  • Configure billing (see Configuration) and optional Google/GitHub login.
  • Activate your licence under Admin → Licence.

Upgrading

See Upgrading.

Configuration

Tenancy (.env / config/tenancy.php)

Key Default Meaning
TENANCY_MODE single single: one database, tenant_id scoping. database: one database per tenant (tenant_{id}). Choose before you have production data.
TENANCY_CENTRAL_DOMAINS APP_URL host Hosts that serve the marketing site; tenants get {slug}.{first host}.
TENANCY_FALLBACK central When no tenant matches: central (marketing site), session (user's current org) or abort.
INVITATION_EXPIRE_HOURS 72 Hours before a team invitation expires.
TENANCY_DB_PREFIX tenant_ DB-per-tenant database name prefix.

Tenant identification order: verified custom domain → {slug}.{central domain} → /t/{slug}. The Filament tenant panel lives at /app/{slug}.

Tenant isolation bootstrappers: database connection (DB-per-tenant mode), cache prefix tenant_{id}_, filesystem roots tenants/{id} for the local/public disks (S3 key prefix), and spatie permission team id.

Billing

The tenant (organization) is the billable entity.

Stripe (primary): STRIPE_KEY, STRIPE_SECRET, STRIPE_WEBHOOK_SECRET, STRIPE_PRICE_STARTER, STRIPE_PRICE_PRO, optional metered STRIPE_PRICE_API_CALLS (Billing Meter event STRIPE_METER_EVENT=api_calls), CASHIER_CURRENCY. Webhook URL: https://your-app/webhooks/stripe. Enable the customer portal and Smart Retries in Stripe.

Paddle Billing: PADDLE_SANDBOX, PADDLE_SELLER_ID, PADDLE_CLIENT_SIDE_TOKEN, PADDLE_API_KEY, PADDLE_WEBHOOK_SECRET, PADDLE_PRICE_STARTER, PADDLE_PRICE_PRO. Webhook URL: https://your-app/webhooks/paddle. Gateway per tenant via tenants.billing_gateway; default BILLING_GATEWAY=stripe.

Plans live in the plans table, editable in Admin → Plans (seeded: Free $0, Starter $9/seat, Pro $29/seat, 14-day trials on paid plans). Each plan has features (e.g. api, custom_domains) and limits (projects, seats; empty = unlimited). Gate routes with feature: / limit: middleware or the Features helper.

Webhook secrets are required

/webhooks/stripe and /webhooks/paddle answer 403 and log a warning until STRIPE_WEBHOOK_SECRET / PADDLE_WEBHOOK_SECRET is set, in every environment. Requests with a missing or wrong signature are also rejected. For local testing, stripe listen --forward-to your-app.test/webhooks/stripe prints a whsec_… secret for .env.

Adding a plan, feature or limit

  1. Plan: Admin → Plans → New plan: name, slug, monthly price, trial days, per-seat, and the Stripe/Paddle price IDs. The marketing pricing page and the tenant Billing page read the plans table, so the new plan appears immediately.
  2. Feature gate: add a key (e.g. exports) to the plan's features, then protect routes with ->middleware('feature:exports') or check Features::allows($tenant, 'exports') in code and Blade. Tenants without it get a 402 "Your plan does not include this feature."
  3. Limit: add a key to the plan's limits (e.g. "reports": 10; empty = unlimited), register a counter in config/billing.php → counters ('reports' => [Counters::class, 'reports']), and add ->middleware('limit:reports') to the create route, or check Features::withinLimit().

Proxies and allowed hosts

  • TRUSTED_PROXIES (default empty): no proxy is trusted, so visitors can't spoof X-Forwarded-Host/-Proto/-For to redirect password-reset links or switch tenants. That's right for cPanel/Apache. Behind Caddy/Nginx on the same server use 127.0.0.1; behind Cloudflare list its published IP ranges; behind a private load balancer use its CIDR. Use * only if the app can't be reached except through the proxy.
  • TRUSTED_HOSTS : requests are accepted only for the APP_URL host, central domains and their tenant subdomains, and verified custom domains; anything else gets 400. Add extra hostnames (staging, a health-check IP) comma separated. APP_ENV=local skips the check.

Social login

GOOGLE_CLIENT_ID/SECRET, GITHUB_CLIENT_ID/SECRET. Buttons show on the tenant login form.

Mail

Standard Laravel MAIL_* settings. Invitations, dunning, receipts and trial-ending emails use them.

Custom domains & SSL

  • Plans with the custom_domains feature get the Custom domains page.
  • Tenants add a TXT record _aucip-verify.<domain> = aucip-verify=<token> and a CNAME to your app host.
  • Caddy: use deploy/Caddyfile. On-demand TLS asks GET /caddy/ask?domain=…, which only approves verified custom domains and existing tenant subdomains.
  • Cloudflare for SaaS: add Custom Hostnames per tenant domain; the TXT check stays the ownership proof.

Licence & updates

AUCIP_PRODUCT, AUCIP_PUBLIC_KEY, AUCIP_WEB_INSTALLER, AUCIP_DEMO. Activate the key in the installer or Admin → Licence. The licence is re-verified once a day by the scheduler (cron); web requests never call home. If verification fails, a 14-day grace period starts (counted from the last successful check), with a banner in the admin panel; network errors are retried automatically. After 14 days, admin settings (Plans) and Updates lock until the licence verifies again. Tenants, users, billing, webhooks, the audit log and the tenant-facing app never lock, and no customer data is touched. To move domains: Admin → Licence → Deactivate, then activate on the new domain. Updates are free while licensed; support is 6 months, renewable.

Feature usage

Marketing site

/ (landing page) and /pricing render plans straight from the plans table. Tenant homes are served on custom domains, subdomains or /t/{slug}.

Tenant panel (/app)

  • Register / log in: email + password, email verification, password reset, optional Google/GitHub login, and TOTP two-factor authentication with recovery codes (Profile).
  • Create organization: /app/new; switch between organizations from the sidebar menu.
  • Members: invite by email (expiring links), change roles (Owner / Admin / Member / custom roles), remove members, transfer ownership, see pending invitations.
  • Billing: current plan, seats and metered API usage; choose / swap plans, cancel and resume; open the billing portal.
  • Custom domains: add a domain, see the TXT record, "Verify now". Verification is re-checked hourly; after 3 failures a domain is un-verified.
  • API tokens: create organization-scoped Sanctum tokens.
  • Projects: an example tenant-owned resource (with plan limits) to copy for your own models.
  • Organization settings: name and billing email.

Super-admin panel (/admin)

  • Dashboard: MRR, active subscriptions, churn (30 days), organization count.
  • Tenants, Users, Plans: manage records; edit plan prices, trials, features and limits.
  • Impersonation: log in as a user from Users ; a banner shows during impersonation and every start/stop is written to the Audit log.
  • Audit log: events with actor, impersonator, organization and properties.
  • Licence and Updates pages.
  • Two-factor authentication available for admins.

REST API

/api/v1 with a Sanctum token bound to an organization:

  • GET /api/v1/me: current user and tenant
  • GET|POST|PUT|DELETE /api/v1/projects: example resource (enforces the projects limit)

Billing lifecycle

Checkout (trial if the plan has trial_days) → webhook creates the subscription → plan sync → seat quantity follows member join/remove on per-seat plans → swap → cancel (grace period) → resume → invoice.payment_failed sends a dunning email per attempt → customer.subscription.deleted drops the tenant to the Free plan. Webhooks are processed once (webhook_events table).

Tenant-aware code

use App\Tenancy\Tenancy;

app(Tenancy::class)->run($tenant, function () {
    SendReport::dispatch();   // queued job runs in $tenant's context
});

$schedule->command('tenants:run', ['cmd' => 'reports:weekly'])->weekly(); // once per tenant

Add BelongsToTenant to a model to get a global tenant_id scope, auto-fill on create and blocked cross-tenant writes.

Pitfalls

  • Return nothing from the run() closure when dispatching: write SendReport::dispatch(); as a statement. fn () => SendReport::dispatch() returns a PendingDispatch that is sent after the tenant context ends, so the job runs without a tenant.
  • Removing a member revokes access immediately through the per-request membership check and deletes their API tokens. With the default SESSION_DRIVER=file, their existing session files aren't deleted, but they can't open the organization any more. Use the database or redis session driver if you need sessions removed too.

Artisan commands

Command Purpose
kit:install Install wizard (CLI twin of /install)
tenants:migrate Run database/migrations/tenant for each tenant DB
tenants:run {cmd} Run an artisan command for every tenant
domains:verify Re-check custom domains (also scheduled hourly)
demo:reset Wipe and re-seed the public demo

Testing and AI agents

See Test suite and code quality and Working on the kit with AI coding agents.

FAQ

Single database or database per tenant? Both. single (default) scopes rows by tenant_id; database gives each tenant its own database. Pick before production data exists; switching later needs a manual export and import.

Which payment providers? Stripe (Cashier 15) and Paddle Billing (Cashier Paddle 2), chosen per tenant.

Can I run it on shared hosting? Yes, on plain cPanel (see Installation → cPanel). Use the /install web installer and one cron line; with QUEUE_VIA_CRON=true the scheduler drains the database queue every minute.

How do tenant custom domains get SSL? Caddy on-demand TLS (with the built-in /caddy/ask guard) or Cloudflare for SaaS.

Is Filament 's tenancy enough? The tenant panel uses Filament's native tenancy, and the kit adds tenant-aware jobs, cache and storage, DB-per-tenant mode, billing and custom domains.

Does it contact Aucip? Only licence and update checks are sent to aucip.com: the key is activated in the installer, then re-verified once a day by the scheduler (php artisan schedule:run cron). One-click updates also use it. Web requests never call home.

What if verification fails? You get a 14-day grace period (counted from the last successful check), with a banner in the admin panel. Network problems are retried automatically. After 14 days, admin settings (Plans) and Updates lock until the licence verifies again. Tenants, users, billing, webhooks, the audit log and the tenant-facing app never lock, and no customer data is touched.

What support and updates are included? Free updates and 6 months support, renewable.

Which licence do I need? Regular licence: use in one end product that end users are not charged for. Free updates while licensed and 6 months of support (renewable). Extended licence: use in one end product that end users are charged for (for example a paid SaaS or paid membership). Free updates while licensed and 6 months of support (renewable). One licence = one end product. Free local installs (localhost/.test) plus up to 3 staging installs on staging subdomains per licence; staging doesn't count against your production domain. Each additional end product needs another licence. One licence per client end product: Regular, or Extended when that product's end users are charged. Cannot be resold as a starter kit. Installation service: paid add-on, sold separately.

Is there a React/Vue front end? Not in 1.0. The UI is Blade + Filament/Livewire.

Where do I add my own features? Copy the Projects example: a tenant-owned model using BelongsToTenant, a Filament resource and an API controller.

How it compares

Why not build multi-tenancy myself? You can, but the work isn't the tenant_id column. It's making queued jobs, cache keys, file storage, scheduled tasks, roles and API tokens tenant-aware, failing closed when no tenant is set, and proving it with tests. The kit ships all of that plus an isolation test suite that CI runs in both tenancy modes.

Single database or database per tenant?

| Single DB (default) | DB per tenant
---|---|---
Isolation | tenant_id global scope; writes to another tenant refused | separate database per tenant
Hosting | any host, including cPanel | DB user needs CREATE DATABASE
Migrations | one php artisan migrate | php artisan tenants:migrate across all tenant DBs
Backups / per-customer export | filter by tenant_id | dump one database
Best for | most B2B SaaS, many small tenants | fewer, larger customers with data-residency or export needs
Same application code works in both; pick before you have production data. | |

How is this different from a hosted SaaS boilerplate or platform? You buy the full source once, host it wherever you like (shared hosting included) and own every line. There is no per-seat fee and no vendor platform between you and your customers. Only licence and update checks go to aucip.com.

How is it different from single-tenant Laravel starters? Those give you auth and a subscription per user. Here the organization is the billable entity, with teams, roles, per-tenant domains, impersonation and an audit log built in.

What about Filament 's own tenancy? The tenant panel uses it, and the kit adds what it leaves to you: tenant-aware jobs, cache and storage, DB-per-tenant mode, billing and custom domains.

Deploy with Laravel Forge or Ploi

Both panels provision a standard PHP server, so the steps are the same. Menu names differ slightly between the two; the setting each step needs is given in brackets.

1. Server

  • Create a server with PHP 8.2 or newer , MySQL 8 (or PostgreSQL 15+) and, optionally, Redis.
  • Extensions the kit checks for: bcmath, ctype, curl, fileinfo, intl, mbstring, openssl, pdo, tokenizer, xml, zip, sodium. These are standard on both panels' PHP builds.

2. Site

  • Create a site for your central domain, e.g. saas.example.com, with web directory /public.
  • Add the alias *.saas.example.com (Forge: Aliases ; Ploi: Site aliases / wildcard) so tenant subdomains reach the app.
  • DNS: an A record for saas.example.com and a wildcard * record, both pointing at the server.

3. Code

  • Connect your Git repository (push the kit to your own private repo first), or upload the zip contents over SFTP.
  • Create the database and user in the panel.

4. Environment

Edit .env in the panel's environment editor. At minimum:

APP_ENV=production
APP_DEBUG=false
APP_URL=https://saas.example.com
TENANCY_CENTRAL_DOMAINS=saas.example.com
DB_CONNECTION=mysql
DB_DATABASE=…  DB_USERNAME=…  DB_PASSWORD=…
QUEUE_CONNECTION=database   # or redis
QUEUE_VIA_CRON=false        # a daemon runs the queue (step 6)
TRUSTED_PROXIES=            # empty unless something proxies in front (see Configuration)
STRIPE_WEBHOOK_SECRET=…     # webhooks answer 403 until set

5. First install (SSH, once)

php artisan key:generate
php artisan kit:install            # app name, URL, tenancy mode, super-admin, licence key

6. Scheduler and queue

  • Scheduler: enable the panel's scheduler (Forge: Scheduler → php artisan schedule:run every minute; Ploi: Cron jobs). It runs the daily licence check, the hourly custom-domain re-check and any tenants:run tasks.
  • Queue worker daemon: php artisan queue:work --tries=3 (Forge: Daemons / Queue ; Ploi: Queue workers / Daemons). If you install Horizon (composer require laravel/horizon), run php artisan horizon instead and set QUEUE_CONNECTION=redis.

7. Deploy script

cd $SITE_PATH                        # the panel sets the site directory
git pull origin $BRANCH
composer install --no-dev --optimize-autoloader --no-interaction
php artisan migrate --force
php artisan tenants:migrate          # DB-per-tenant mode; harmless in single-DB mode
php artisan optimize
php artisan queue:restart            # or: php artisan horizon:terminate

Assets ship prebuilt in public/build; add npm ci && npm run build only if you change front-end code.

8. TLS

  • Central domain and subdomains: request a Let's Encrypt certificate for saas.example.com and *.saas.example.com. Wildcard certificates need DNS validation, so connect your DNS provider in the panel, or put Cloudflare in front.
  • Tenant custom domains: panels issue certificates per domain you add manually. To make it automatic, either run Caddy with on-demand TLS in front (deploy/Caddyfile; the kit's /caddy/ask endpoint approves only verified domains and set TRUSTED_PROXIES=127.0.0.1), or use Cloudflare for SaaS custom hostnames.

9. After deploy

  • Log in at /admin, check Admin → Licence.
  • Set billing keys and webhook endpoints (/webhooks/stripe, /webhooks/paddle).
  • Visit a tenant subdomain to confirm wildcard DNS and TLS.

Test suite and code quality

The buyer zip includes the Pest test suite, the tenant-isolation suite, Larastan and Pint configuration, and a GitHub Actions workflow. The zip ships production vendor/, so install the dev tools first:

composer install                       # adds Pest, Larastan, Pint, Sail
cp .env.example .env && php artisan key:generate   # if you haven't installed yet
Command What it runs
composer test Pest suite (single-DB mode)
TENANCY_MODE=database composer test same suite in DB-per-tenant mode
vendor/bin/pest --group=isolation tenant-isolation suite: queries, cache keys, jobs, files, API tokens and roles cannot cross tenants
composer analyse Larastan (phpstan.neon, level 6)
composer lint Pint in check mode (pint.json); vendor/bin/pint fixes

Tests run on SQLite and need no Stripe/Paddle accounts (gateways are mocked; webhooks use fixtures).

CI: .github/workflows/ci.yml runs Pint and Larastan, the Pest suite with a 70% coverage floor on PHP 8.2, 8.3 and 8.4 in both tenancy modes, and a browser job (Playwright + axe accessibility scan and a 375px overflow check, tests/Browser/a11y.mjs). Push the kit to your own GitHub repo and it runs as-is.

Keep all of these green in both tenancy modes when you change the kit.

Working on the kit with AI coding agents

These rules keep Claude Code, Cursor, Copilot, Codex and similar agents from breaking tenant isolation. Copy this page into an AGENTS.md (or CLAUDE.md, .cursor/rules) at the root of your project so your agent reads it on every task.

Rules for the agent

  1. Tenant-owned models useApp\Tenancy\Concerns\BelongsToTenant, and their migrations live in database/migrations/tenant/ with $table->unsignedBigInteger('tenant_id')->index(). Never put tenant-owned tables in database/migrations/.
  2. Never bypass the tenant scope in tenant code (withoutGlobalScopes(), raw DB::table('projects')). Cross-tenant reads belong only in the super-admin panel, on purpose.
  3. Central tables are not tenant-scoped: users, tenants, tenant_user, domains, plans, invitations, subscriptions, audit_logs, personal_access_tokens, roles. Filter them by tenant_id yourself.
  4. Run work for a tenant with app(Tenancy::class)->run($tenant, fn () => …); read the current one with tenant(). Outside a tenant context, tenant models return no rows and refuse writes.
  5. Jobs: dispatch inside a tenant context and the job runs in that tenant (tenant_id travels in the payload). Use statement form, MyJob::dispatch();. Do not return the dispatch from the closure (fn () => MyJob::dispatch()): the PendingDispatch is sent after the context ends and the job loses its tenant.
  6. Cache and storage are namespaced per tenant automatically. Don't build "tenant:{$id}:…" keys by hand.
  7. Billing goes throughApp\Billing\BillingManager. The tenant is the billable entity, never the user. Use Features::plan($tenant), Features::allows(), Features::withinLimit() and the feature:<name> / limit:<key> middleware.
  8. Security-relevant actions callApp\Support\Audit::log().
  9. User-facing strings go through__().
  10. No outbound HTTP from web requests except Stripe/Paddle (a test enforces this). The licence API is called only by the installer, the admin Licence/Updates screens and the daily aucip:license cron.

Where things live

Concern Path
Tenant context and bootstrappers app/Tenancy/
Tenant identification (domain → subdomain → path) app/Tenancy/TenantResolver.php, middleware tenant
Teams, invitations, roles, ownership app/Services/TeamService.php, app/Support/TenantRoles.php
Billing and webhooks app/Billing/, app/Http/Controllers/Webhooks/
Custom domains, Caddy ask app/Domains/, deploy/Caddyfile
Impersonation app/Services/ImpersonationService.php
Super-admin panel (/admin) app/Filament/Admin/
Tenant panel (/app/{tenant}) app/Filament/App/
Installer, updater, demo mode packages/aucip-starter/
Brand tokens and theme app/Support/Brand.php, resources/css/brand/tokens.css, resources/css/filament/theme.css

Recipes to give the agent

  • New tenant-owned resource: copy app/Models/Project.php, the database/migrations/tenant/*projects* migration and app/Filament/App/Resources/Projects/. Keep $isScopedToTenant = false on the Filament resource (scoping comes from BelongsToTenant, which also works in DB-per-tenant mode).
  • New plan feature: add the key to a plan's features in Admin → Plans, then protect routes with ->middleware('feature:my_feature') or check Features::allows($tenant, 'my_feature').
  • New plan limit: add it to the plan's limits, register a counter in config/billing.php → counters, and add ->middleware('limit:my_key').
  • Per-tenant scheduled task: $schedule->command('tenants:run', ['cmd' => 'my:command'])->daily();

Definition of done for every agent change

composer test
TENANCY_MODE=database composer test
vendor/bin/pest --group=isolation
composer analyse
composer lint

Ask the agent to add or extend a test in tests/Feature/Isolation/ whenever it touches tenant data access.

Upgrading

One-click (recommended)

Admin → System → Updates → "Check for updates" → Install. Updates are free while your licence is valid. The updater takes a code backup first, puts the site in maintenance mode, copies the new files (keeping .env and storage/), runs central and tenant migrations, and restores the code backup if a step fails (database changes are not rolled back, so back up your database before updating). You can also upload a release zip on the same screen.

Manual (SSH)

Extract the new zip over the app, keeping .env and storage/, then:

php artisan migrate --force
php artisan tenants:migrate
php artisan optimize:clear

Using Git (Forge/Ploi): merge the new release into your repo and run the deploy script.

Per-release notes

1.0.0

First public release. No upgrade steps.

Future releases list any breaking changes, new .env keys and manual steps here.

Changelog

1.0.0

  • Initial release.
  • In-house tenancy layer: single-DB and DB-per-tenant modes; tenant-aware queue, cache, storage, scheduler (tenants:run) and migrations (tenants:migrate).
  • Tenant identification by custom domain, subdomain and /t/{slug} path with configurable fallback.
  • Teams: organizations, org switcher, expiring email invitations, Owner/Admin/Member + custom roles, ownership transfer.
  • Per-tenant billing with Cashier Stripe and Cashier Paddle: plans, trials, per-seat quantity, metered API-calls example, swap/cancel/resume, billing portal, idempotent webhooks, dunning emails.
  • Custom domains with DNS TXT verification, hourly re-check, plan gating, Caddy on-demand TLS ask endpoint.
  • Filament 5 super-admin panel (tenants, users, plans, MRR/churn, audit log, impersonation with banner) and tenant panel.
  • Auth: email/password, verification, TOTP 2FA with recovery codes, Google/GitHub login, tenant-scoped Sanctum tokens.
  • kit:install CLI + web installer, demo seeder, Sail, Pest, Larastan, Pint, CI, AGENTS.md.

Aucip Tenancy Kit · aucip.com