Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

111 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Restaurant SaaS Platform

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.


Table of Contents

  1. Technology Stack
  2. Architecture Overview
  3. Getting Started
  4. Project Structure
  5. Modules & Features
  6. Authentication Flows
  7. Provisioning Flow
  8. Subscription Lifecycle
  9. Billing & Invoicing
  10. Plans & Quotas
  11. Queue Jobs & Scheduler
  12. Database Schema
  13. API Routes
  14. Frontend Pages
  15. Mail
  16. Docker Setup

Technology Stack

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

Architecture Overview

Dual-Database Model

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

Route Domain Separation

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.

Auth Guards

Guard Provider Model
web users (platform DB) App\Platform\Models\User
tenant tenants (tenant DB) App\Tenant\Models\User

Getting Started

Prerequisites

  • Docker + Docker Compose
  • No local PHP/Node required — everything runs inside containers

1. Clone and configure

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.

2. Start containers

docker compose up -d
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)

5. Build frontend assets

pnpm dev       # development with HMR
pnpm build     # production build

6. Start the queue worker

php artisan queue:work --queue=default

Project Structure

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

Modules & Features

The platform is organised around composable modules. Each module contains feature flags; plans enable combinations of modules and flags.

Module Catalogue

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):

  1. Check tenant_feature_overrides for an explicit override (admin can grant or deny any feature)
  2. Fall back to plan_features — check if the tenant's active plan includes that feature flag
  3. Return true / false

Authentication Flows

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

Platform Admin Login

POST /login
  └─ Fortify authenticateUsing (web guard)
       └─ queries platform users, checks is_active

LoginResponse → /admin/dashboard

Tenant User Login

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

Provisioning Flow

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)

Backup Flow

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}

Subscription Lifecycle

Statuses

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

State Transitions

[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]

Grace Period

  • 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:

  1. requestChange($subscription, $newPlan, $effectiveMode) — creates subscription_plan_changes row
  2. Effective modes:
    • immediate — applied right away; proration calculated
    • period_end — applied at subscription.ends_at by nightly scheduler
  3. validateChange() — checks current usage against new plan's limits; sets validation_status = 'over_limit' if blocked
  4. applyChange() — updates subscription plan_id, recalculates ends_at, initialises new limit states

Billing & Invoicing

Invoice Lifecycle

draft ──(issueInvoice)──► unpaid ──(recordPayment, full amount)──► paid
                                 ──(due_at passes, no payment)────► overdue
                         ──(markVoid)──────────────────────────────► void

Invoice Structure

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

Payment Recording

BillingService::recordPayment($invoice, $paymentData):

  • Creates subscription_payments row with gateway, transaction_id, status
  • If status = 'success' → calls markPaid($invoice) → sets invoice to paid

Coupon / Discount System

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

Plans & Quotas

Pricing

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

Quotas

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

Limit Enforcement

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.


Queue Jobs & Scheduler

Queue Jobs

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

Database Schema

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

API Routes

Platform Admin Routes (/admin/...)

All require auth:web + verified + platform.user middleware.

Tenants

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

Plans

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

Subscriptions

Method URI Name
GET /admin/subscriptions subscriptions.index
GET /admin/subscriptions/create subscriptions.create
POST /admin/subscriptions subscriptions.store

Subscription Invoices

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

Coupons

Method URI Name
GET /admin/coupons coupons.index
GET /admin/coupons/create coupons.create
POST /admin/coupons coupons.store

API Keys

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

Other Platform Resources

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

Public / Website Routes

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

Frontend Pages

Platform Admin (resources/js/platform/pages/)

Authentication

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

Platform Admin Panel

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

Public Website

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

Mail

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.


Docker Setup

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

Useful Commands

# 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.

About

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.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages