N
HeadlessCoreHeadless Core
Back to Home

Hosted SaaS (auto-provisioned tenants)

Optional, off by default. Lets you sell HeadlessCore as a subscription: a customer pays a flat recurring price and gets their own live WordPress + Next.js site, provisioned automatically on infrastructure you control (a Coolify-managed server).

Status: Phase 1 (manual-trigger provisioning) only. The Stripe subscription webhook, suspension/cancellation lifecycle, and customer-facing signup page described in the plan are not built yet — this doc covers what exists today: provisioning a tenant by hand, for testing the pipeline.

Architecture

scripts/provision-tenant.ts (manual, for now)
   → src/lib/provisioning.ts: provisionTenant()
      → mints a pro license key (src/lib/license.ts)
      → src/lib/coolify.ts: creates a WordPress Docker Compose app
      → src/lib/coolify.ts: creates a Next.js Nixpacks app
      → polls both until healthy
      → src/lib/platformDb.ts: records the tenant row (Postgres)

This is separate infrastructure from the multi-tenant feature (Multi-Tenant) — that feature routes one Next.js deployment to multiple WordPress backends by domain. This feature provisions an entirely separate WordPress + Next.js pair per customer.

Why a real database

Every other feature in this project deliberately avoids a database (WordPress's own MySQL, or Stripe object metadata, is always the datastore instead). This is the first feature that needs genuine mutable state — tracking tenant status, Coolify resource IDs, and subscription IDs — independent of any single tenant's own WordPress being reachable. It's a dedicated Postgres instance, not shared with any tenant's content database.

Resource limits (noisy-neighbor guard)

Every per-tenant container is capped so one tenant's traffic spike can't starve every other tenant on the same server: docker-compose.saas-tenant.yml sets deploy.resources.limits (512MB / 0.5 vCPU) on both db and wordpress; provisioning.ts calls coolify.setResourceLimits() with the same cap on the Next.js app. That's ~1.5GB RAM / 1.5 vCPU reserved per tenant — use this figure for capacity planning (a server's usable RAM, minus ~2GB for Coolify + the platform Postgres, divided by 1.5GB, is roughly how many tenants it supports before you need to scale up).

Setup (Phase 0 — manual, one-time)

Before provisioning a real tenant:

  1. Server capacity. Each tenant needs its own WordPress + MySQL + Next.js containers. A small (2GB) VPS supports maybe one or two tenants at that density — either upgrade the server, or point each tenant's WordPress at a remote/managed MySQL instead (adapt docker-compose.saas-tenant.yml the same way docker-compose.prod.remote-db.yml does for the single-tenant case) to cut each tenant's footprint.
  2. Create a Coolify API token with resource-creation scope.
  3. Note your Coolify server UUID and project UUID.
  4. Point *.{your-platform-domain} DNS at your server, and configure a DNS-01-capable provider in Coolify's Traefik settings so a wildcard TLS cert can be issued (HTTP-01 can't issue wildcards — this is what makes instant per-tenant subdomains work without a per-tenant ACME round trip).
  5. Provision a Postgres resource in Coolify for the platform database.
  6. If your repo is private, connect Coolify's GitHub App integration.

Env vars

See .env.example's "Hosted SaaS provisioning" section: PLATFORM_DATABASE_URL, COOLIFY_BASE_URL, COOLIFY_API_TOKEN, COOLIFY_SERVER_UUID, COOLIFY_PROJECT_UUID, PLATFORM_DOMAIN, SAAS_GIT_REPOSITORY. Also requires LICENSE_SIGNING_PRIVATE_KEY_PEM (same one used by Stripe Licensing).

Migrate the platform database

PLATFORM_DATABASE_URL="postgres://..." node scripts/migrate-platform-db.js

Creates the tenants table. Safe to re-run.

Provision a tenant by hand

cd nextjs-starter-kit
npm run provision-tenant -- --email="jane@example.com"

This bypasses Stripe entirely (fake subscription IDs) — it's purely for exercising the Coolify provisioning path end to end. On success it prints the tenant's WordPress URL, Next.js URL, and license key.

Domain scheme

  • {slug}.{PLATFORM_DOMAIN} — the tenant's Next.js frontend
  • cms-{slug}.{PLATFORM_DOMAIN} — the tenant's WordPress admin/GraphQL

Both are one level under the wildcard domain, so both are covered by the same TLS cert.

What's NOT built yet (see the project plan for the full design)

  • Stripe subscription checkout + webhook wiring — provisioning only triggers manually today, not automatically on payment.
  • Suspension on past_due/cancellation (headlesscore_maintenance_mode is the intended mechanism — see Licensing — but nothing calls it yet for SaaS tenants).
  • Customer-facing /hosted signup page, welcome email, and Stripe Billing Portal self-serve link.
  • saas_tenant WordPress-side audit CPT.
  • Reconciliation for tenants stuck mid-provisioning after a process restart.

Known unverified assumptions (confirm before relying on them)

  • Exact Coolify API field names for setting env vars in bulk and for the Nixpacks app's monorepo base directory — src/lib/coolify.ts is written against Coolify's public API docs, not yet exercised against a live instance.
  • Whether Coolify's per-resource Docker Compose project scoping actually namespaces docker-compose.saas-tenant.yml's bare volume names (db_data, wp_data, wp_uploads) across tenants — inspect docker volume ls on the server after a second tenant provisions, before trusting it for a third.