Files
afilmory/apps/docs/contents/deployment/saas.mdx
Innei 1c64070a5d feat: add GitHub Actions workflow for building and publishing core Docker image
- Introduced a new GitHub Actions workflow in `core-image.yml` to automate the build and publish process for the core Docker image.
- The workflow triggers on pushes to the main branch and supports manual dispatch.
- Configured steps for checking out the repository, setting up Docker Buildx, logging into the GitHub Container Registry, extracting Docker metadata, and building and pushing the Docker image.

Additionally, updated documentation to reflect changes in the deployment process, including a new section for multi-tenant SaaS deployment.

Signed-off-by: Innei <tukon479@gmail.com>
2025-11-23 20:22:23 +08:00

161 lines
7.3 KiB
Plaintext

---
title: Advanced SaaS Deployment
description: Run the full multi-tenant stack (API + OAuth gateway) with subdomains and custom domains.
createdAt: 2025-11-23T19:50:00+08:00
lastModified: 2025-11-23T20:22:24+08:00
order: 60
---
# Advanced SaaS Deployment
This guide describes how to deploy the multi-tenant stack that serves many galleries from a single installation. It is based on the backend (`be/apps/core`) and the OAuth callback router (`be/apps/oauth-gateway`).
## Components
- **Core API + SPA host (`@afilmory/core`)** — serves the web app, injects `__MANIFEST__`/`__CONFIG__`, and resolves tenants by host.
- **OAuth Gateway (`@afilmory/oauth-gateway`)** — a tiny router that receives all provider callbacks at one domain and forwards them to the correct tenant subdomain.
- **PostgreSQL + Redis** — required by the core service.
- **Object storage** — wherever your photo originals live (S3/B2/GitHub/local, configured via builder/system settings).
## Quick start with Docker (all-in-one core image)
`Dockerfile.core` builds the core API, embeds web + dashboard, installs `exiftool`/`vips`, and runs migrations on start.
```bash
# Build image
docker build -f Dockerfile.core -t afilmory-core .
# Run (Postgres/Redis reachable)
docker run --rm \
-p 1841:1841 \
-e DATABASE_URL="postgres://user:pass@db:5432/afilmory" \
-e REDIS_URL="redis://redis:6379" \
-e HOSTNAME="core.local" \ # optional, affects printed URLs
-e NODE_ENV=production \
afilmory-core
```
First boot: container runs `db:migrate`, provisions the root tenant + superadmin, then prints credentials to stdout, e.g.
```
============================================================
Root dashboard access provisioned.
Dashboard URL: http://localhost:1841/platform/root-login
Alternate URL: http://core.local:1841/platform/root-login
Email: root@local.host
Username: root
Password: w3xF1cC5cE5Zf1l_yhGy6w
============================================================
```
- Email/username come from `DEFAULT_SUPERADMIN_EMAIL/USERNAME` (defaults `root@local.host` / `root`).
- Password is a random 16-byte `base64url` string generated once per fresh boot and **only shown in logs**.
## First-login flow
1. Open `https://root.<baseDomain>/platform/root-login` (or the printed dashboard URL locally).
2. Sign in with the printed root credentials.
3. In Superadmin, set system settings: `baseDomain`, `oauthGatewayUrl`, storage providers, registration rules, billing/storage plans, etc.
4. Create your first tenant (slug cannot be reserved: `root` and the placeholder slug are reserved).
5. Configure OAuth providers (Google/GitHub) with callback pointing to the OAuth Gateway.
## Domain model
- **Base domain** (default `afilmory.art`, configurable in System Settings): tenants are addressed as `<slug>.<baseDomain>`.
- **OAuth callback domain**: usually `auth.<baseDomain>` pointing to the OAuth Gateway.
- **Custom domains**: tenants can bind their own domain via DNS verification (CNAME to base domain *or* TXT containing a verification token).
## DNS layout
1) Point the **core** service to the base domain and its wildcard:
- `A/AAAA` or `CNAME` for `<baseDomain>` → core load balancer.
- `CNAME` `*.<baseDomain>` → core load balancer (enables tenant subdomains).
2) Point the **OAuth Gateway**:
- `CNAME auth.<baseDomain>` → gateway load balancer (or the same host if you co-locate).
3) For **custom domains**:
- Either add `CNAME <custom> -> <baseDomain>` **or**
- Add `TXT <custom> = <verificationToken>` (token issued when the tenant requests binding).
## Runtime configuration
### Core service (`be/apps/core`)
Key system settings (editable via admin UI/API):
- `baseDomain` — used to derive tenant slug from host; defaults to `afilmory.art`.
- `oauthGatewayUrl` — e.g., `https://auth.<baseDomain>`; appended to OAuth provider redirects.
Minimal env (SaaS core):
```
PORT=1841
DATABASE_URL=postgres://localhost:5432/afilmory
REDIS_URL=localhost:6379
CONFIG_ENCRYPTION_KEY=<openssl -hex 64>
```
`CONFIG_ENCRYPTION_KEY` is required to encrypt sensitive settings; generate with `openssl rand -hex 64`.
Set `NODE_ENV=production` in real deployments and add storage/OAuth creds as needed.
Deploy with `pnpm --filter @afilmory/core dev|start` (or containerize) and serve on HTTPS.
### OAuth Gateway (`be/apps/oauth-gateway`)
Env vars (mapped in `src/config.ts`):
```
AUTH_GATEWAY_HOST=0.0.0.0
AUTH_GATEWAY_PORT=8790
AUTH_GATEWAY_BASE_DOMAIN=your-base-domain.com
AUTH_GATEWAY_FORCE_HTTPS=true
AUTH_GATEWAY_CALLBACK_BASE_PATH=/api/auth/callback
AUTH_GATEWAY_ALLOW_CUSTOM_HOST=false
AUTH_GATEWAY_ROOT_SLUG=root
```
Run: `pnpm --filter @afilmory/oauth-gateway dev` (or containerize). Expose HTTPS at `auth.<baseDomain>`.
## Request routing flow
1. **Tenant resolution (core):**
- Try custom domain match (`TenantDomainService`).
- Else derive slug from `<slug>.<baseDomain>` (`extractTenantSlugFromHost`).
- Root/admin paths (`/api/super-admin`, `/api/settings`, `/api/storage/settings`, `/api/builder/settings`) fall back to `root` tenant.
- Missing slug falls back to a placeholder tenant so the app can render an onboarding state.
2. **OAuth callback (gateway):**
- Providers redirect to `https://auth.<baseDomain>/api/auth/callback/{provider}?tenantSlug=<slug>`.
- Gateway rewrites to `https://<slug>.<baseDomain>/api/auth/callback/{provider}` (preserving query), keeping tenant sessions isolated.
3. **Custom domain verification:**
- Tenant requests domain → receives a verification token.
- Verification passes if either CNAME points to `<baseDomain>` or TXT contains the token (checked via DNS `CNAME`/`TXT`).
## Tenant lifecycle
- **Create tenant:** super-admin API `PATCH/POST` paths under `/api/super-admin/tenants` (see dashboard or use API) with `slug` and optional billing/storage plan IDs.
- **Plans & quotas:** `BillingPlanService` and `BillingUsageService` enforce per-tenant limits; override pricing/products in System Settings.
- **Banning/suspension:** super-admin can ban or suspend; requests then raise `TENANT_BANNED`/`TENANT_SUSPENDED` errors.
## Deployment checklist
1. Provision Postgres + Redis; migrate DB via `pnpm --filter @afilmory/core db:migrate` (or your migration flow).
2. Deploy **core** behind HTTPS on `<baseDomain>` with wildcard support.
3. Deploy **OAuth Gateway** at `auth.<baseDomain>`; update System Settings `oauthGatewayUrl`.
4. Set System Settings `baseDomain` to your chosen domain; restart core if env-derived host/ports change.
5. Create a super-admin user and a first tenant (slug not reserved: `root`, placeholder slug are reserved).
6. Test: visit `https://<slug>.<baseDomain>` → login via OAuth → manifest injected → gallery loads.
7. Test custom domain: request binding, add CNAME/TXT, run verification; ensure HTTPS certs via your proxy/ingress.
## Licensing notice (commercial use)
The SaaS build includes billing (Creem, plan enforcement, usage tracking). The open-source license does **not** grant commercial rights to run this as a paid service. For commercial use or partnerships, contact the project owner for authorization.
## Notes
- `DEFAULT_BASE_DOMAIN` is `afilmory.art`; override in settings for your SaaS.
- Placeholder tenant slug is generated to let the UI show onboarding before a real slug exists.
- The gateway is stateless; you can scale it horizontally behind any load balancer.