A multi-tenant Software-as-a-Service platform for restaurant businesses. Each restaurant (tenant) gets an isolated PostgreSQL database and operates at its own subdomain. The platform layer manages tenants, billing plans, subscriptions, invoicing, and provisioning.
Technology Stack
Architecture Overview
Getting Started
Project Structure
Modules & Features
Authentication Flows
Provisioning Flow
Subscription Lifecycle
Billing & Invoicing
Plans & Quotas
Queue Jobs & Scheduler
Database Schema
API Routes
Frontend Pages
Mail
Docker Setup
Layer
Technology
Version
PHP
PHP-FPM
^8.3 (Docker: php:8.4-fpm)
Framework
Laravel
^13.0
Auth
Laravel Fortify
^1.34
Multitenancy
spatie/laravel-multitenancy
^4.1
Frontend
React + TypeScript
^19.2.0
SPA Bridge
Inertia.js
@inertiajs/react ^2.3.7
Database
PostgreSQL
16.2
Cache / Queue
Database driver (Redis available)
—
Build Tool
Vite
laravel-vite-plugin ^3.0.0
Routing (PHP→JS)
laravel/wayfinder
^0.1.14
UI Components
shadcn/ui + Radix UI + Tailwind CSS v4
—
Container
Docker + Docker Compose
—
Two named database connections are used throughout the application:
Connection
Database
Used by
platform
restaurant (central PostgreSQL)
All platform models: users, tenants, plans, subscriptions, invoices
tenant
Per-tenant PostgreSQL database
Tenant app models: tenant users, sessions, caches, jobs
The tenant connection credentials are swapped at runtime by Spatie's SwitchTenantDatabaseTask whenever a tenant is resolved from the request.
Subdomain-Based Tenant Resolution
Domain: {tenant-code}.{CENTRAL_DOMAIN}, e.g. acme.restaurant.test
SubdomainTenantFinder strips the central domain suffix, extracts the subdomain, and queries tenants WHERE code = '{subdomain}'
On match, Tenant::makeCurrent() is called, which triggers SwitchTenantDatabaseTask to reconfigure the tenant DB connection from the tenant_databases row
Domain
Prefix
Routes
Purpose
{CENTRAL_DOMAIN}
(none)
routes/website/web.php
Public marketing site + checkout
{CENTRAL_DOMAIN}
/admin
routes/platform/web.php + routes/platform/settings.php
Platform admin panel
{tenant}.{CENTRAL_DOMAIN}
(none)
routes/tenant/web.php (includes fortify.php + settings.php)
Per-tenant restaurant app
Fortify Context Switching
ConfigureFortifyForRequest middleware runs on every request and dynamically configures Fortify:
Tenant request → fortify.guard = 'tenant', auth.guard = 'tenant'
Platform request → fortify.guard = 'web', auth.guard = 'web'
This allows a single Fortify installation to power both platform login and per-tenant login.
Guard
Provider
Model
web
users (platform DB)
App\Platform\Models\User
tenant
tenants (tenant DB)
App\Tenant\Models\User
Docker + Docker Compose
No local PHP/Node required — everything runs inside containers
git clone < repo-url> restaurant
cd restaurant
cp .env.example .env
Edit .env — key variables:
APP_URL = http://restaurant.test
CENTRAL_DOMAIN = restaurant.test
DB_CONNECTION = platform
DB_HOST = restaurant-postgres
DB_PORT = 5432
DB_DATABASE = restaurant
DB_USERNAME = postgres
DB_PASSWORD = your_password
MAIL_MAILER = log # or smtp / mailgun etc.
Service
URL
Application
http://restaurant.test:9100
Adminer (DB GUI)
http://localhost:9999
3. Install dependencies inside the app container
docker compose exec restaurant-app bash
composer install
pnpm install
4. Run migrations and seed data
php artisan migrate --path=database/migrations/platform
php artisan db:seed
Seed data includes:
8 currencies (USD, EUR, GBP, INR, AED, SAR, SGD, AUD)
5+ countries with currency mappings
4 modules + 5 feature flags
3 billing plans (Starter $19/mo, Professional $79/mo, Enterprise $999/yr)
Test user (test@example.com)
pnpm dev # development with HMR
pnpm build # production build
6. Start the queue worker
php artisan queue:work --queue=default
app/
├── Common/ # Shared across platform + tenant contexts
│ ├── Http/
│ │ ├── Controllers/ # Base Controller
│ │ ├── Middleware/ # ConfigureFortifyForRequest, HandleInertiaRequests, etc.
│ │ └── Responses/ # LoginResponse, RegisteredUserResponse, LogoutResponse
│ ├── Multitenancy/ # SubdomainTenantFinder
│ └── Providers/ # AppServiceProvider (Verified event listener)
│
├── Platform/ # Platform/admin layer
│ ├── Actions/Fortify/ # CreateNewUser (Fortify contract)
│ ├── Http/
│ │ ├── Controllers/ # All platform admin controllers
│ │ └── Requests/ # Form request validation classes
│ ├── Jobs/ # All queue jobs
│ ├── Models/ # 35+ Eloquent models (platform DB)
│ └── Services/ # Domain services (Billing, Provisioning, Subscription, Tenant, Usage)
│
├── Tenant/ # Per-tenant app layer
│ ├── Http/Controllers/ # Tenant dashboard + settings controllers
│ ├── Models/User.php # Tenant-side user model
│ └── Actions/Fortify/ # Tenant-context Fortify actions
│
└── Mail/ # Mailable classes
├── TenantInvitationMail.php
└── TenantPaymentRequestMail.php
database/
├── migrations/platform/ # 18 migration files (platform DB)
├── migrations/tenant/ # 4 migration files (each tenant DB)
└── seeders/ # Geography, modules, plans seeders
resources/
├── js/
│ ├── common/ # Shared React components, hooks, utilities
│ ├── platform/ # Platform admin SPA (pages, layouts, components)
│ ├── tenant/ # Tenant app SPA (pages, layouts, components)
│ ├── actions/ # Wayfinder-generated typed route wrappers
│ └── routes/ # Wayfinder route type definitions
└── views/
├── app.blade.php # Inertia root template
├── mail/ # Markdown email templates
├── platform/ # Platform-specific Blade views (if any)
├── tenant/ # Tenant-specific Blade views
└── website/ # Public website Blade views
routes/
├── website/web.php # Public + checkout routes
├── platform/web.php # Platform admin routes
├── platform/settings.php # Platform settings routes
├── tenant/web.php # Tenant app routes
├── tenant/fortify.php # Tenant auth routes (prefixed with tenant.)
├── tenant/settings.php # Tenant settings routes
└── console.php # Scheduled commands
The platform is organised around composable modules. Each module contains feature flags; plans enable combinations of modules and flags.
Module Code
Description
Features
platform.core
Core tenancy infrastructure
core.tenancy.enabled, core.audit_logs.enabled
platform.catalog
Menu and product management
catalog.variants.enabled
platform.orders
Order processing and KDS
orders.kds.enabled
platform.billing
Online payment integration
billing.online_payments.enabled
Restaurant Modules (plan-gated)
Feature
Starter
Professional
Enterprise
Inventory Management
✅
✅
✅
QR Ordering
✅
✅
✅
Reservations
—
✅
✅
Delivery Management
—
✅
✅
Advanced Reports
—
✅
✅
Loyalty Programme
—
✅
✅
Kitchen Display (KDS)
—
✅
✅
API Access
—
✅
✅
Custom Domain
—
✅
✅
Central Kitchen
—
—
✅
Franchise Module
—
—
✅
White Label
—
—
✅
Feature Entitlement Resolution
EntitlementService::hasFeature($tenant, $featureCode):
Check tenant_feature_overrides for an explicit override (admin can grant or deny any feature)
Fall back to plan_features — check if the tenant's active plan includes that feature flag
Return true / false
Self-Registration — Trial Path
GET /register
└─ register.tsx (Step 1: plan selection → Step 2: account + business + billing)
POST /register
└─ CreateNewUser::create()
└─ TenantRegistrationService::registerTrial()
├─ TenantCreationService::createAll() [DB transaction]
│ ├─ creates User (platform)
│ ├─ creates Tenant
│ ├─ creates TenantBillingProfile
│ ├─ creates TenantLocale
│ ├─ creates TenantDomain
│ └─ creates TenantDatabase record (status: provisioning)
├─ SubscriptionService::createTrialSubscription() [status: trial]
└─ User::sendEmailVerificationNotification()
RegisteredUserResponse → redirect to GET /email/verify (verification.notice)
GET /email/verify/{id}/{hash} (click link in email)
└─ Verified event fires
└─ AppServiceProvider listener
└─ TenantRegistrationService::onEmailVerified()
├─ tenant.is_verified = true
└─ ProvisioningService::startProvisioning() [queued]
Self-Registration — Paid Path
POST /register
└─ TenantRegistrationService::registerPaid()
├─ TenantCreationService::createAll()
├─ SubscriptionService::createPaidSubscription() [status: past_due]
└─ CheckoutService::buildInitialInvoice() [invoice: unpaid]
RegisteredUserResponse → redirect to GET /checkout/{invoice}
POST /checkout/callback (payment gateway callback)
└─ TenantCheckoutController::paymentCallback()
└─ TenantRegistrationService::onPaymentSuccess()
├─ SubscriptionService::activateSubscription() [status: active]
└─ ProvisioningService::startProvisioning() [queued]
GET /checkout/thank-you
Admin-Initiated Tenant Creation
Three modes available in the platform admin panel at POST /admin/tenants:
Mode
What happens
Trial
Creates user + tenant, provisions immediately, sends invitation email
Paid (manual)
Creates user + tenant, marks invoice paid inline, provisions immediately, sends invitation email
Paid (pending)
Creates user + tenant, issues unpaid invoice, sends payment link email, waits for payment
POST /login
└─ Fortify authenticateUsing (web guard)
└─ queries platform users, checks is_active
LoginResponse → /admin/dashboard
POST {tenant-subdomain}/login
└─ Fortify authenticateUsing (tenant guard)
└─ queries tenant DB users
LoginResponse → /dashboard
Two-Factor Authentication
Enabled for both platform and tenant contexts via Fortify with:
TOTP (Google Authenticator / Authy)
Recovery codes
Password confirmation required before enabling/disabling 2FA
When a tenant's database needs to be created (after email verification for trial, or after payment for paid), the following async pipeline runs:
ProvisioningService::startProvisioning($tenant)
├─ Creates provisioning_jobs row {status: pending}
└─ Dispatches ProvisionTenantDatabaseJob (queue)
━━━ ProvisionTenantDatabaseJob ━━━━━━━━━━━━━━━━━━━━━━━━━━ (tries: 3, backoff: 60s)
1. Updates provisioning_jobs {status: running}
2. Connects to PostgreSQL maintenance DB (postgres) via superuser PDO
3. CREATE DATABASE "{db_name}" (idempotent)
4. CREATE USER "{db_user}" WITH PASSWORD '{32-char random}'
5. GRANT ALL PRIVILEGES ON DATABASE "{db_name}" TO "{db_user}"
6. Connects to new DB:
GRANT ALL ON SCHEMA public TO "{db_user}"
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO "{db_user}"
7. Stores encrypted credentials on tenant_databases
{db_username, db_password = encrypt($pass)}
8. Registers dynamic named connection 'tenant_prov_{uuid}'
9. Artisan::call('migrate', --database=tenant_prov_{uuid},
--path=database/migrations/tenant)
10. Dispatches SeedTenantDatabaseJob (queue)
━━━ SeedTenantDatabaseJob ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ (tries: 3, backoff: 60s)
1. Decrypts tenant DB credentials
2. Registers dynamic named connection 'tenant_seed_{uuid}'
3. Inserts tenant owner into tenant DB users table
4. Creates tenant storage directory (storage/app/tenants/{code}/)
5. ProvisioningService::markSuccess()
├─ provisioning_jobs {status: success}
├─ tenant_databases {status: active}
└─ tenant {is_onboarded: true, status: subscription.status}
6. LimitService::initializeLimitStates($tenant, $plan)
ProvisioningService::createBackup($tenant)
└─ Dispatches CreateTenantBackupJob (queue)
━━━ CreateTenantBackupJob ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ (tries: 2)
1. Decrypts tenant DB credentials
2. Runs: pg_dump --host --port --username --file={path} {db_name}
(password injected via PGPASSWORD env var — never in CLI args)
3. Saves file to storage/app/backups/tenant_{code}_{timestamp}.sql
4. Updates tenant_database_backups {status, file_path, file_size}
Status
Description
trial
Active free trial; provisioning starts after email verification
past_due
Initial state for paid subscriptions, or when renewal fails
active
Fully active and provisioned
grace
Period expired but within grace window; tenant retains access
suspended
Grace expired or manual admin action; access denied
cancelled
Cancelled by tenant or admin
expired
Terminal state
[trial] ──(email verified + provisioned)─────────────────► [active]
[past_due] ──(payment confirmed)────────────────────────────► [active]
[active] ──(next_billing_at passes, no payment) nightly──► [past_due]
[active] ──(period ends, no renewal)──────────────────────► [grace]
[grace] ──(grace_ends_at passes) nightly────────────────► [suspended]
[suspended]──(admin reactivates, fresh billing)─────────────► [active]
[active/trial/grace] ──(cancel_at_period_end flag)──────────► [cancelled]
Duration drawn from Plan.grace_period_days (Starter: 7d, Professional: 7d, Enterprise: 15d)
When grace starts: grace_starts_at = now(), grace_ends_at = now() + grace_period_days
Nightly SuspendExpiredSubscriptionsJob queries subscriptions WHERE status='grace' AND grace_ends_at < now() and suspends each
Plan Changes (Upgrades / Downgrades)
PlanChangeService manages the workflow:
requestChange($subscription, $newPlan, $effectiveMode) — creates subscription_plan_changes row
Effective modes:
immediate — applied right away; proration calculated
period_end — applied at subscription.ends_at by nightly scheduler
validateChange() — checks current usage against new plan's limits; sets validation_status = 'over_limit' if blocked
applyChange() — updates subscription plan_id, recalculates ends_at, initialises new limit states
draft ──(issueInvoice)──► unpaid ──(recordPayment, full amount)──► paid
──(due_at passes, no payment)────► overdue
──(markVoid)──────────────────────────────► void
SubscriptionInvoice
├─ billing_period_start / billing_period_end
├─ invoice_no (auto-generated)
├─ subtotal, discount_total, tax_total, grand_total
├─ currency_id + currency_code_snapshot
├─ status, issued_at, due_at, paid_at
└─ Lines (subscription_invoice_lines)
├─ fixed_plan — base plan price
├─ addon — per-addon charges
├─ discount — coupon / manual discount
└─ tax — calculated from TaxRate
BillingService::recordPayment($invoice, $paymentData):
Creates subscription_payments row with gateway, transaction_id, status
If status = 'success' → calls markPaid($invoice) → sets invoice to paid
CheckoutService::applyCouponToSubscription($subscription, $couponCode):
Validates: active, not expired, redeemed_count < max_redemptions
Applies percent_off or amount_off discount
Creates subscription_coupon_redemptions row
Adds a discount line to the invoice
Checkout Flow (Self-Signup, Paid Path)
GET /checkout/{invoice}
└─ TenantCheckoutController@show → renders website/checkout.tsx
(shows: invoice_no, amount, gateway selector)
POST /checkout/callback
└─ TenantCheckoutController@paymentCallback
fields: invoice_id, amount, transaction_id, gateway, status, failure_reason
└─ on status=success:
TenantRegistrationService::onPaymentSuccess()
├─ SubscriptionService::activateSubscription()
└─ ProvisioningService::startProvisioning()
GET /checkout/thank-you
Plan
Code
Billing
Price
Model
Starter
starter-monthly
Monthly
$19 / mo
Prepaid
Professional
professional-monthly
Monthly
$79 / mo
Prepaid
Enterprise
enterprise-yearly
Yearly
$999 / yr
Postpaid
Limit
Starter
Professional
Enterprise
Outlets
1
5
Unlimited
Users
5
30
Unlimited
Orders / month
3,000
30,000
Unlimited
Languages
1
5
Unlimited
Countries
1
3
Unlimited
Storage
10 GB
100 GB
Unlimited
API calls / month
50,000
300,000
Unlimited
Trial period
14 days
14 days
30 days
Grace period
7 days
7 days
15 days
LimitService::canCreate($tenant, $metricCode) is called before creating a new resource:
Metric
Enforcement Mode
outlets, users, languages, countries
block_create_only — request blocked when at limit
orders, api_calls, storage_gb
warn — allowed but warning issued
Limits are stored in tenant_limit_states and refreshed nightly by RefreshTenantLimitStatesJob.
Job
Queue
Tries
Backoff
Description
ProvisionTenantDatabaseJob
default
3
60s
Creates PostgreSQL DB + user, runs tenant migrations
SeedTenantDatabaseJob
default
3
60s
Seeds tenant DB, creates owner account, marks provisioned
CreateTenantBackupJob
default
2
—
Runs pg_dump, saves to storage/app/backups/
ApplyPlanChangeJob
default
3
—
Validates usage then applies plan upgrade/downgrade
GenerateInvoiceJob
default
3
—
Generates recurring billing cycle invoice
CaptureUsageSnapshotJob
default
3
—
Aggregates usage events into period snapshots
RefreshTenantLimitStatesJob
default
3
—
Recalculates and persists tenant_limit_states
SuspendExpiredSubscriptionsJob
default
1
—
Batch-suspends grace-expired tenants (nightly)
Scheduled Commands (routes/console.php)
Command
Schedule
Action
suspend-expired-subscriptions
Daily
Runs SuspendExpiredSubscriptionsJob — suspends all tenants where grace_ends_at < now()
apply-pending-plan-changes
Daily
Dispatches ApplyPlanChangeJob for all due subscription_plan_changes
mark-past-due-subscriptions
Daily
Bulk-updates subscriptions with expired next_billing_at to past_due
Platform Database (connection: platform)
Tenant & Identity tables
Table
Description
users
Platform admin users
tenants
Restaurant workspaces
tenant_domains
Custom domains per tenant
tenant_billing_profiles
Billing name, address, tax number
tenant_locales
Supported languages per tenant
tenant_countries
Enabled countries per tenant
tenant_databases
DB credentials (encrypted) + provisioning status
tenant_database_backups
pg_dump backup records
tenant_payment_methods
Stored payment methods
tenant_sessions
Active user sessions
tenant_feature_overrides
Per-tenant feature flag overrides
tenant_limit_states
Live quota status (current vs allowed)
tenant_usage_events
Raw usage event log
tenant_usage_snapshots
Aggregated period usage
api_keys
Tenant API keys (hashed)
Plan & Catalogue tables
Table
Description
plans
Billing plans with quotas and feature flags
plan_features
N:M plan ↔ feature_flags
plan_usage_rates
Overage pricing per metric per plan
modules
Module catalogue
feature_flags
Individual toggle-able features
countries
Country reference data
currencies
Currency codes and symbols
country_currencies
Which currencies are valid per country
tax_rates
Configurable tax rates
usage_metrics
Metric types tracked (outlets, users, orders, etc.)
coupons
Discount codes
Subscription & Billing tables
Table
Description
subscriptions
Active subscription per tenant
subscription_addons
Optional add-on charges
subscription_billing_periods
Billing cycle history
subscription_plan_changes
Upgrade/downgrade request history
subscription_change_validations
Usage validation results at change time
subscription_coupon_redemptions
Coupon usage log
subscription_invoices
Invoices per billing period
subscription_invoice_lines
Line items on each invoice
subscription_payments
Payment records per invoice
invoice_taxes
Tax breakdown per invoice
provisioning_jobs
Async provisioning job status
PostgreSQL Views
View
Description
subscription_invoice_summaries
Aggregated invoice data with tenant + plan info
tenant_access_summaries
Quick overview: tenant status, plan, provisioning state
Tenant Database (connection: tenant)
Each tenant database is created by ProvisionTenantDatabaseJob and migrated from database/migrations/tenant/:
Table
Description
users
Restaurant staff accounts (owner seeded from platform)
password_reset_tokens
Password reset tokens
sessions
Authenticated tenant user sessions
cache / cache_locks
Per-tenant cache storage
jobs / job_batches / failed_jobs
Per-tenant queue tables
Platform Admin Routes (/admin/...)
All require auth:web + verified + platform.user middleware.
Method
URI
Name
Action
GET
/admin/tenants
tenants.index
List all tenants
GET
/admin/tenants/create
tenants.create
Create tenant form
POST
/admin/tenants
tenants.store
Create tenant (trial / paid / pending)
POST
/admin/tenants/{tenant}/provision
tenants.provision
Start provisioning
POST
/admin/tenants/{tenant}/retry-provision
tenants.retry-provision
Retry failed provisioning
POST
/admin/tenants/{tenant}/suspend
tenants.suspend
Suspend tenant
POST
/admin/tenants/{tenant}/reactivate
tenants.reactivate
Reactivate tenant
POST
/admin/tenants/{tenant}/verify-email
tenants.verify-email
Mark email verified
POST
/admin/tenants/{tenant}/backup
tenants.backup
Create DB backup
Method
URI
Name
GET
/admin/plans
plans.index
GET
/admin/plans/create
plans.create
GET
/admin/plans/{plan}/edit
plans.edit
POST
/admin/plans
plans.store
PATCH
/admin/plans/{plan}
plans.update
DELETE
/admin/plans/{plan}
plans.destroy
Method
URI
Name
GET
/admin/subscriptions
subscriptions.index
GET
/admin/subscriptions/create
subscriptions.create
POST
/admin/subscriptions
subscriptions.store
Method
URI
Name
GET
/admin/subscription-invoices
subscription-invoices.index
GET
/admin/subscription-invoices/create
subscription-invoices.create
POST
/admin/subscription-invoices
subscription-invoices.store
GET
/admin/subscription-invoices/{id}/edit
subscription-invoices.edit
PATCH
/admin/subscription-invoices/{id}
subscription-invoices.update
DELETE
/admin/subscription-invoices/{id}
subscription-invoices.destroy
Method
URI
Name
GET
/admin/coupons
coupons.index
GET
/admin/coupons/create
coupons.create
POST
/admin/coupons
coupons.store
Method
URI
Name
GET
/admin/api-keys
api-keys.index
GET
/admin/api-keys/create
api-keys.create
POST
/admin/api-keys
api-keys.store
GET
/admin/api-keys/{id}/edit
api-keys.edit
PATCH
/admin/api-keys/{id}
api-keys.update
DELETE
/admin/api-keys/{id}
api-keys.destroy
Resource
Routes
Plan Usage Rates
index, create, store
Subscription Plan Changes
index, create, store
Subscription Billing Periods
index, create, store
Subscription Coupon Redemptions
index, create, store
Tenant Feature Overrides
index, create, store
Tenant Limit States
index, create, store
Tenant Usage Events
index, create, store
Tenant Usage Snapshots
index, create, store
Usage Metrics
index, create, store
Provisioning Jobs
index
Tenant Sessions
destroy
Platform Settings (/admin/settings/...)
Method
URI
Name
GET
/admin/settings/profile
profile.edit
PATCH
/admin/settings/profile
profile.update
DELETE
/admin/settings/profile
profile.destroy
GET
/admin/settings/security
security.edit
PUT
/admin/settings/password
user-password.update
GET
/admin/settings/appearance
appearance.edit
Method
URI
Name
Description
GET
/
website.home
Marketing landing page
GET
/plans
plans.public
Public plan listing
GET
/checkout/{invoice}
checkout.show
Payment checkout
POST
/checkout/callback
checkout.callback
Payment gateway callback
GET
/checkout/thank-you
checkout.thank-you
Post-payment confirmation
Tenant App Routes ({tenant-code}.{CENTRAL_DOMAIN}/...)
Method
URI
Name
Description
GET
/
tenant.home
Redirects to /login or /dashboard
GET
/dashboard
tenant.dashboard
Main restaurant dashboard
GET/POST
/login
tenant.login
Tenant user login
POST
/logout
tenant.logout
Tenant logout
GET
/email/verify
tenant.verification.notice
Email verification notice
GET
/settings/profile
tenant.profile.edit
Profile settings
GET
/settings/security
tenant.security.edit
Password / 2FA settings
(+ all Fortify routes)
—
tenant.*
Register, forgot-password, reset-password, 2FA
Platform Admin (resources/js/platform/pages/)
Page
Route
auth/login.tsx
GET /login
auth/register.tsx
GET /register (two-step: plan selection → account creation)
auth/forgot-password.tsx
GET /forgot-password
auth/reset-password.tsx
GET /reset-password/{token}
auth/verify-email.tsx
GET /email/verify
auth/confirm-password.tsx
GET /user/confirm-password
auth/two-factor-challenge.tsx
GET /two-factor-challenge
Page
Route
Description
welcome.tsx
/
Unauthenticated welcome screen
dashboard.tsx
/admin/dashboard
Admin overview
plans/index.tsx
/admin/plans
Plan management table
plans/create.tsx
/admin/plans/create
Create billing plan
plans/edit.tsx
/admin/plans/{id}/edit
Edit billing plan
tenants/index.tsx
/admin/tenants
Tenant list + workflow actions
tenants/create.tsx
/admin/tenants/create
Admin tenant creation (trial/paid/pending)
subscriptions/index.tsx
/admin/subscriptions
All subscriptions
subscriptions/create.tsx
/admin/subscriptions/create
Create subscription manually
subscription-invoices/index.tsx
/admin/subscription-invoices
Invoice list
subscription-invoices/create.tsx
/admin/subscription-invoices/create
Create invoice
subscription-invoices/edit.tsx
/admin/subscription-invoices/{id}/edit
Edit / record payment
subscription-plan-changes/index.tsx
/admin/subscription-plan-changes
Plan change history
subscription-plan-changes/create.tsx
/admin/subscription-plan-changes/create
Request plan change
subscription-billing-periods/index.tsx
/admin/subscription-billing-periods
Billing period history
subscription-coupon-redemptions/index.tsx
/admin/subscription-coupon-redemptions
Coupon redemption log
coupons/index.tsx
/admin/coupons
Coupon management
coupons/create.tsx
/admin/coupons/create
Create coupon
feature-overrides/index.tsx
/admin/tenant-feature-overrides
Per-tenant feature overrides
feature-overrides/create.tsx
/admin/tenant-feature-overrides/create
Create override
usage-metrics/index.tsx
/admin/usage-metrics
Usage metric catalogue
plan-usage-rates/index.tsx
/admin/plan-usage-rates
Overage pricing
tenant-limit-states/index.tsx
/admin/tenant-limit-states
Live quota status
tenant-usage-events/index.tsx
/admin/tenant-usage-events
Raw usage events
tenant-usage-snapshots/index.tsx
/admin/tenant-usage-snapshots
Aggregated usage
provisioning-jobs/index.tsx
/admin/provisioning-jobs
Job queue monitor
api-keys/index.tsx
/admin/api-keys
API key management
settings/profile.tsx
/admin/settings/profile
Admin profile
settings/security.tsx
/admin/settings/security
Password + 2FA
settings/appearance.tsx
/admin/settings/appearance
Theme toggle
Page
Route
Description
website/index.tsx
/
Marketing landing page with hero, features, pricing CTA
website/plans.tsx
/plans
Public plan comparison and selection
website/checkout.tsx
/checkout/{invoice}
Payment form (gateway + invoice details)
website/thank-you.tsx
/checkout/thank-you
Post-payment confirmation
Tenant App (resources/js/tenant/pages/)
Page
Route
Description
auth/login.tsx
{tenant}/login
Restaurant staff login
auth/register.tsx
{tenant}/register
Staff registration
auth/forgot-password.tsx
{tenant}/forgot-password
Password reset request
auth/reset-password.tsx
{tenant}/reset-password/{token}
Password reset form
auth/verify-email.tsx
{tenant}/email/verify
Email verification notice
auth/confirm-password.tsx
{tenant}/user/confirm-password
Sudo confirmation
auth/two-factor-challenge.tsx
{tenant}/two-factor-challenge
2FA code entry
dashboard.tsx
{tenant}/dashboard
Restaurant main dashboard
settings/profile.tsx
{tenant}/settings/profile
User profile
settings/security.tsx
{tenant}/settings/security
Password + 2FA
settings/appearance.tsx
{tenant}/settings/appearance
Theme
Mailable
Trigger
Subject
Template
TenantInvitationMail
Admin creates trial or paid tenant (provisioning complete)
"Your workspace is ready — {tenant.name}"
resources/views/mail/tenant-invitation.blade.php
TenantPaymentRequestMail
Admin creates pending-payment tenant
"Complete your payment to activate {tenant.name}"
resources/views/mail/tenant-payment-request.blade.php
Verification emails are handled by Laravel's built-in sendEmailVerificationNotification() on the User model, which dispatches the standard Fortify email verification mail.
Services (docker-compose.yml)
Container
Image
Ports
Purpose
restaurant-app
Built from Dockerfile
—
PHP-FPM application
restaurant-webserver
nginx:alpine
9100:80, 9143:443
Nginx reverse proxy
restaurant-postgres
postgres:16.2-alpine
5434:5432
PostgreSQL database
restaurant-adminer
adminer:latest
9999:8080
Database GUI (default server: restaurant-postgres)
All services are connected via the restaurant-services bridge network.
Application Container (Dockerfile)
Base: php:8.4-fpm
PHP extensions: pdo, pdo_pgsql, pgsql, mbstring, exif, pcntl, bcmath, gd, zip
Tools: Composer, Node.js, pnpm (via corepack)
App user: restaurant (uid 1000, member of www-data)
Working directory: /var/www
# Start all services
docker compose up -d
# Open a shell in the app container
docker compose exec restaurant-app bash
# Run migrations
php artisan migrate --path=database/migrations/platform
# Seed reference data
php artisan db:seed
# Build frontend
pnpm dev # dev server with HMR
pnpm build # production
# Run test suite
php artisan test
# Start queue worker
php artisan queue:work
# Run scheduler (for testing locally)
php artisan schedule:run
Environment Variables Reference
Variable
Description
Example
APP_URL
Full URL of the central domain
http://restaurant.test:9100
CENTRAL_DOMAIN
Root domain for tenant subdomains
restaurant.test
DB_HOST
PostgreSQL host
restaurant-postgres
DB_PORT
PostgreSQL port
5432
DB_DATABASE
Platform database name
restaurant
DB_USERNAME
DB superuser (for migrations)
postgres
DB_PASSWORD
DB superuser password
—
QUEUE_CONNECTION
Queue driver
database
MAIL_MAILER
Mail driver
log / smtp
MAIL_FROM_ADDRESS
Sender address
noreply@restaurant.test
Built with Laravel 13, React 19, Inertia.js, spatie/laravel-multitenancy, PostgreSQL 16, Tailwind CSS v4.