REST API backend for a multi-tenant SaaS platform built with Laravel 12, Sanctum, and PostgreSQL.
composer install
cp .env.example .env
php artisan key:generate
php artisan migrate
php artisan db:seed
php artisan serveAPI documentation: http://127.0.0.1:8000/api/documentation
cp .env.example .env
php artisan key:generate # or generate after first container start
docker compose up --build
docker compose exec app php artisan migrate
docker compose exec app php artisan db:seed- app — API at http://localhost:8000
- queue —
php artisan queue:workfor async data migrations - db — PostgreSQL 15 (
pgdatavolume); overridesDB_HOSTfrom.envto use the local container
Stack: Neon (PostgreSQL) + Hetzner Cloud (API + queue) + Caddy (HTTPS).
1. Hetzner server — Ubuntu 24.04, e.g. CX22 (2 vCPU / 4 GB). Add your SSH key. In Firewalls, allow inbound 22 (your IP), 80, 443.
2. On the server
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER
# log out and back in
mkdir -p ~/faizan
cd ~/faizan
git clone https://github.com/YOUR_USER/YOUR_REPO.git .
cp .env.production.example .env
nano .env # Neon credentials, APP_URL=http://YOUR_HETZNER_IP:8000Or from your PC: scp .env.production root@YOUR_IP:~/faizan/.env
3. Start containers
docker compose -f docker-compose.prod.yml up -d --build
docker compose -f docker-compose.prod.yml exec app php artisan key:generate
docker compose -f docker-compose.prod.yml exec app php artisan migrate --force
docker compose -f docker-compose.prod.yml exec app php artisan db:seed --force
docker compose -f docker-compose.prod.yml exec app php artisan config:cache
curl http://127.0.0.1:8000/api/health4. HTTPS (Caddy on host) — point DNS A record to the server IP, then:
sudo apt install -y caddy/etc/caddy/Caddyfile:
api.yourdomain.com {
reverse_proxy 127.0.0.1:8000
}sudo systemctl reload caddy5. Scheduler — included as the scheduler service (php artisan schedule:work). No host cron required for schedule:run.
Redeploy (manual): ./scripts/deploy.sh or git pull && docker compose -f docker-compose.prod.yml up -d --build
The API listens on 127.0.0.1:8000 only; Caddy handles public HTTPS. EXPORTS_DISK=local is fine on a single VPS.
CI — .github/workflows/ci.yml runs tests on push/PR (PHP 8.4 + PostgreSQL).
CD — .github/workflows/deploy.yml deploys to Hetzner after CI passes on main/master (or run manually from the Actions tab).
cd /root/Faizan/MultiTenantSaas
git remote -v # must point at your GitHub repoAdd a deploy SSH key on the server (~/.ssh/authorized_keys) and configure GitHub repo Settings → Secrets → Actions:
| Secret | Example |
|---|---|
DEPLOY_HOST |
`` |
DEPLOY_USER |
`` |
DEPLOY_SSH_KEY |
private key (full PEM) |
DEPLOY_PATH |
`` |
DEPLOY_BRANCH |
main (optional) |
Generate a key pair for GitHub Actions only:
ssh-keygen -t ed25519 -C "github-actions-deploy" -f deploy_key -N ""
# Add deploy_key.pub to server authorized_keys
# Paste deploy_key contents into DEPLOY_SSH_KEY secretEnsure the server can git fetch from GitHub (deploy key or HTTPS token on the server remote).
| Password | Role | |
|---|---|---|
superadmin@platform.local |
password |
Super-admin |
admin@acme.local |
password |
Tenant admin (Acme Corp) |
member@acme.local |
password |
Tenant member |
- Auth — register, login, tokens, password reset
- Tenant — settings, members, ownership transfer, soft delete
- IAM — system + custom roles/permissions, middleware enforcement
- Admin — tenant CRUD, dashboard, platform settings, impersonation
- Features — catalog management, tenant overrides, usage limits
- Usage — reporting with feature limit enforcement
- Integrations — webhook CRUD and connectivity test
- Data — export/import, queued migrations, cross-tenant migration
- Audit & Compliance — logs, GDPR, bulk export, retention archive
- Observability — health, status, metrics
Copy phpunit.xml.dist to phpunit.xml for local overrides (gitignored). DB credentials come from your .env file.
composer testTests use DatabaseTransactions against the configured PostgreSQL database.
Migrations run asynchronously via the database queue:
php artisan queue:workAudit log retention (default 90 days):
php artisan audit:archiveSchedule in production via cron: * * * * * php artisan schedule:run
Key variables in .env:
DB_CONNECTION=pgsql
QUEUE_CONNECTION=database
EXPORTS_DISK=local
AUDIT_RETENTION_DAYS=90
Send Authorization: Bearer {token} on protected routes. Tokens are issued via /api/auth/login or /api/auth/register.