Track HiveGrid Cloud planning reference

This commit is contained in:
david committed 2026-06-26 20:48:01 +02:00
1 parent b3d5ea6764
commit e341c5938a
5784 files changed
+1104929 -3

No files matched your search

@@ -0,0 +1,274 @@
# Deployment: boiler.saas-ctrl.com
Reference for the public boilerplate-demo deployment. The values below
are **what to configure in Coolify**, not what to commit. Real secrets
(`SESSION_SECRET`, `CRYPTO_SECRET`, etc.) are generated once and live only
in Coolify's encrypted env store and the optional `.env` on the host.
## Hostnames
Single web-facing host with split admin/api subdomains so the cookie-scope
helper in `@toolctrl/config` can still produce canonical URLs.
| Var | Value |
| ----------------- | ---------------------------------------------------------------- |
| `APP_BASE_DOMAIN` | `saas-ctrl.com` |
| `WEB_APP_HOST` | `boiler.saas-ctrl.com` |
| `ADMIN_APP_HOST` | `admin-boiler.saas-ctrl.com` |
| `API_HOST` | `api-boiler.saas-ctrl.com` |
| `APP_PROTOCOL` | `https` |
| `PORT` | `8080` (Dockerfile default; Coolify routes 443 → container 8080) |
| `API_PORT` | `8080` (api service runs separately if api host is wired) |
DNS records (A or CNAME to the Coolify ingress IP):
- `boiler.saas-ctrl.com`
- `admin-boiler.saas-ctrl.com`
- `api-boiler.saas-ctrl.com`
For a single-host minimal demo the admin/api hosts can resolve to the
same IP — the runtime checks the `Host:` header to pick the surface.
## Required core secrets
Generate once with `openssl rand -hex 32` and store in Coolify:
- `SESSION_SECRET` — cookie signing
- `CRYPTO_SECRET` — AES key for stored workflow credentials (separate from SESSION_SECRET)
- `API_ACCESS_TOKEN` — `openssl rand -base64 32`
- `JWT_SECRET` — `openssl rand -hex 32`
- `ANALYTICS_TOKEN` — `openssl rand -hex 24`
## Brand
`BRAND_*` overrides set the public face. For the demo:
```
BRAND_PRODUCT_NAME=ToolCTRL Boilerplate
BRAND_SITE_URL=https://boiler.saas-ctrl.com
BRAND_SUPPORT_EMAIL=info@tool-ctrl.de
BRAND_SUPPORT_FROM_NAME=ToolCTRL Boilerplate
BRAND_LEGAL_IMPRINT_URL=https://saas-ctrl.com/impressum
BRAND_LEGAL_PRIVACY_URL=https://saas-ctrl.com/datenschutz
BRAND_LEGAL_TERMS_URL=https://saas-ctrl.com/agb
```
## Database
Provision via Coolify's managed Postgres (or external):
- `DATABASE_URL` — `<pooled-postgres-url>` for application/runtime traffic
- `DIRECT_URL` — optional `<direct-postgres-url>` for migrations against the primary DB
## Mail (optional but expected for boilerplate demo)
```
MAILJET_API_KEY=<from Mailjet>
MAILJET_FROM_EMAIL=noreply@saas-ctrl.com
# Deployment policy/evidence marker only; the mail provider does not read it.
MAILJET_SECRET_KEY=<mailjet-secret-key>
MAILJET_WEBHOOK_SECRET=<mailjet-webhook-secret>
```
The managed `worker-mail` Coolify readiness check verifies that the mail queue
runtime can boot, reach Redis/Postgres, and, when fail-loud mail mode is
enabled, see the required Mailjet environment variables. Treat actual
transactional mail delivery as ready only after sender-domain/provider
verification and a real delivery smoke have passed.
## Seed (production bootstrap)
```
SEED_ADMIN_EMAIL=admin@saas-ctrl.com
SEED_ADMIN_PASSWORD=<one-time bootstrap; rotate on first login>
SEED_DEMO_DATA=false
NODE_ENV=production
```
## Storage (optional)
If file uploads should work in the demo:
```
STORAGE_ENDPOINT=https://s3.eu-central-1.wasabisys.com
STORAGE_REGION=eu-central-1
STORAGE_BUCKET=boiler-saas-ctrl-com-uploads
STORAGE_ACCESS_KEY_ID=<from Wasabi>
STORAGE_SECRET_ACCESS_KEY=<from Wasabi>
STORAGE_PUBLIC_BASE_URL=https://uploads.boiler.saas-ctrl.com
STORAGE_FORCE_PATH_STYLE=false
```
## Workers
The active Gitea/Coolify deploy path expects separate worker applications
for general, mail, import, webhook, and plugin queue groups.
Create five persistent Coolify worker applications from the same worker
image. They are internal runtime apps only: do not configure public FQDNs
for any worker app.
This deployment-specific table is a summary. The canonical worker topology,
queue ownership, concurrency, and scaling guide is
[Worker queue groups](../ops/worker-queues.md).
| Coolify app | Required group | Health port | Admin/metrics port |
| ---------------- | ---------------------------- | ----------- | ------------------ |
| `worker-general` | `WORKER_QUEUE_GROUP=general` | `9101` | `9201` |
| `worker-mail` | `WORKER_QUEUE_GROUP=mail` | `9102` | `9202` |
| `worker-import` | `WORKER_QUEUE_GROUP=import` | `9103` | `9203` |
| `worker-webhook` | `WORKER_QUEUE_GROUP=webhook` | `9104` | `9204` |
| `worker-plugin` | `WORKER_QUEUE_GROUP=plugin` | `9105` | `9205` |
Keep all five services present in staging and production-like environments.
`worker-mail` is a permanent service for transactional mail and mail-backed
outbox/webhook work; it must not be treated as a manual workaround started only
after mail backlog appears.
`worker-plugin` is also permanent topology, but production plugin execution
remains blocked until external WASM sandbox signoff and an explicit
`PLUGIN_RUNTIME_PRODUCTION_ENABLED=true` rollout. Before that flip, a healthy
`worker-plugin` only proves the idle service can boot; it is not tenant plugin
runtime approval. During the rollout, run `pnpm run verify:plugin-runtime-live`
inside both `worker-general` and `worker-plugin` before enabling tenant plugins.
`worker-import` is also permanent topology but stays idle until import/export
processing is fully wired. Leave `IMPORT_QUEUE_ENABLED` and
`IMPORT_QUEUE_RUNTIME_READY` unset on `worker-import` until the worker bootstrap
injects import, export, and scan services; the service may then report healthy
with an empty queue list instead of consuming jobs it cannot process.
Each worker needs the same database and queue connection, plus its own queue group/ports:
```
WORKER_ENABLED=true
DATABASE_URL=<pooled-postgres-url>
BULLMQ_REDIS_URL=<redacted-redis-url>
WORKER_QUEUE_GROUP=<general|mail|import|webhook|plugin>
WORKER_HEALTH_PORT=<9101|9102|9103|9104|9105>
WORKER_ADMIN_PORT=<9201|9202|9203|9204|9205>
```
`EXPORT_RETENTION_QUEUE_ENABLED=true` is a legacy standalone-worker fallback.
Do not set it as part of the Coolify queue-group contract; when
`WORKER_QUEUE_GROUP` is valid, export-retention ownership comes from
`packages/queue/src/groups.ts`.
Add these only on `worker-general` in staging/production so outbox dispatch and
workflow trigger fanout fail loud instead of appearing healthy while rows pile
up:
```
OUTBOX_DISPATCHER_ENABLED=true
OUTBOX_DISPATCHER_REQUIRED=true
WORKFLOW_TRIGGER_OUTBOX_ENABLED=true
WORKFLOW_TRIGGER_OUTBOX_REQUIRED=true
WORKFLOW_TRIGGER_EXECUTOR_MODE=http
WORKFLOW_TRIGGER_EXECUTOR_URL=<app-origin> # or APP_URL/WEB_ORIGIN=<app-origin>
API_ACCESS_TOKEN=<same value as web API_ACCESS_TOKEN>
```
Keep this unset or false on `worker-mail` until seed data, persisted stats
aggregation, Playwright E2E, Mailjet delivery evidence/reconciliation,
scheduler/deploy evidence, and the deployment record approve production
delivery. The bundled renderer and `@toolctrl/mail` Mailjet adapter exist,
but the sequence subscribers do no work unless the value is explicitly `true`;
stale-lead/trial-expiring cron runner functions are not production
scheduler-registered yet:
```
MARKETING_SEQUENCES_ENABLED=false
```
Add this only on `worker-mail` in staging/production so transactional mail
outbox bootstrap fails loud when Redis, database, or mail-outbox wiring is not
ready. The worker-group provisioner sets and drift-checks this value for
managed `worker-mail` Coolify applications:
```
TRANSACTIONAL_MAIL_OUTBOX_REQUIRED=true
```
`scripts/coolify-ensure-worker-groups.mjs` treats missing `DATABASE_URL` or
`BULLMQ_REDIS_URL` on any managed worker group (`worker-general`,
`worker-mail`, `worker-import`, `worker-webhook`, `worker-plugin`) as
deployment drift. It also treats missing `worker-general` workflow-trigger
bridge configuration (`WORKFLOW_TRIGGER_EXECUTOR_URL`, `APP_URL`, or `WEB_ORIGIN` plus `API_ACCESS_TOKEN`) and missing mail outbox fail-loud configuration as drift. If
`WORKFLOW_TRIGGER_EXECUTOR_MODE=native` is selected instead of `http`, keep that
choice explicit in Coolify and verify the workflow graph uses only the
worker-native safe-core blocks.
Use `WORKFLOW_TRIGGER_EXECUTOR_MODE=hybrid` to canary native safe-core execution
while routing unsupported workflow graphs through the signed HTTP bridge.
Coolify worker readiness checks should target `/readyz` on
`WORKER_HEALTH_PORT`; `/healthz` remains the lightweight liveness probe for
manual inspection. This confirms queue/runtime readiness for the selected
worker group, including Redis and database reachability. In fail-loud mail mode,
managed `worker-mail` readiness also checks the required Mailjet env
presence. It still does not prove sender-domain verification or live provider
delivery. Admin/metrics endpoints, when enabled, must remain on
`WORKER_ADMIN_PORT` and must not be exposed publicly.
## Observability (opt-in)
```
SENTRY_DSN=<from Sentry>
SENTRY_ENVIRONMENT=production
# OTEL_EXPORTER_OTLP_ENDPOINT=http://otel.internal:4318/v1/traces
OTEL_SERVICE_NAME=boiler-saas-ctrl-com
```
## Coolify configuration checklist
1. Resource type: Docker Image applications for the published web, api, and worker images.
2. Source: this Gitea repository, `main` branch.
3. Domain: `boiler.saas-ctrl.com` (Coolify handles Let's Encrypt).
4. Port: container `8080` → public `443`.
5. Healthcheck: web/api use `GET /healthz`; worker apps use `GET /readyz` on `WORKER_HEALTH_PORT`. Container HEALTHCHECK directives cover liveness; Coolify readiness should wait for the service-specific endpoint.
6. Web resource command slots: `pre_deployment_command` runs `cd /myapp && pnpm exec prisma migrate deploy --config prisma.config.ts`; `post_deployment_command` runs `cd /myapp && NODE_ENV=production node packages/db/dist/production-seed.server.js` (staging adds `SEED_DEMO_DATA=true ALLOW_STAGING_DEMO_SEED=true`). Do not combine the seed into the pre-deploy slot.
7. Env: paste the block above.
8. Worker apps: create `worker-general`, `worker-mail`, `worker-import`, `worker-webhook`, and `worker-plugin` as separate persistent Coolify applications with no public FQDN. Set `WORKER_QUEUE_GROUP`, `WORKER_HEALTH_PORT`, and `WORKER_ADMIN_PORT` per the worker table, and point the Coolify health check at `/readyz`. Do not rely on the worker image's standalone `WORKER_QUEUE_GROUP=general` default for grouped Coolify apps; missing per-app overrides make non-general apps drain the wrong queues.
9. Mail delivery: after `worker-mail` is healthy, verify `MAILJET_API_KEY` and `MAILJET_FROM_EMAIL` exist on the mail-capable runtime and run a real transactional-mail smoke. A green worker health check alone is not mail-provider readiness.
## Trigger a deploy
- Staging: push to `main` builds and pushes images in Gitea CI, patches the staging Coolify image tags, then triggers staging deploys.
- Production: use the Gitea `workflow_dispatch` promote action with a reviewed `staged_sha` and `backup_confirmed=true`. Promotion retags the verified staging images; do not treat a plain `main` push as a production deploy.
- Manual emergency deploy: Coolify dashboard → resource → **Deploy**, only after image tags and migration command state are verified against the runbook.
## Verify the deploy
After Coolify reports "healthy":
```sh
curl -fsS https://boiler.saas-ctrl.com/healthz
curl -fsS https://api-boiler.saas-ctrl.com/healthz
# Image-signing — optional, with cosign installed:
cosign verify --key cosign.pub {registry}/{namespace}/web:<sha>
```
If web/API `/healthz` returns the expected `revision`, verify Coolify also
reports `worker-general`, `worker-mail`, `worker-import`, `worker-webhook`,
and `worker-plugin` healthy. The Gitea deploy workflow must also show that
each worker passed Coolify execute-based `node apps/worker/dist/health.js`
readiness with the expected `revision`, `WORKER_QUEUE_GROUP`, and queue list.
`worker-import` may have an empty queue list while its import runtime gates are
unset; if it reports `toolctrl.imports` or `toolctrl.exports`, both import gates
and service wiring must be intentional.
Image alias/tag output is configuration evidence only; it is not an acceptable
fallback for worker health. If Coolify execute is unavailable, empty, or
non-JSON, fix execute access and rerun the workflow before accepting the
worker rollout. Then log into `boiler.saas-ctrl.com` with the seeded admin
credentials and confirm the tenant switcher loads.
## Current deploy latency levers
Gitea CI pre-builds immutable `web`, `api`, and `worker` images and
Coolify deploys those published tags. Production promotion retags the
verified staging images and does **not** rebuild them.
Known remaining latency levers:
- Docker target builds are already launched in parallel with PID aggregation and per-target cache refs; remaining CI-side gains likely come from cache warmup, runner CPU/network capacity, or splitting buildx work across dedicated runners.
- Registry cache is already enabled through `--cache-from/--cache-to` with stable target-specific cache refs, so cache misses usually mean a cold registry cache or Dockerfile layer invalidation.
- Coolify pre-pull is best-effort and bounded by the helper timeout; a slow host can still spend time pulling layers during deploy.
- Staging and production dependent resources are deployed separately so web health gates worker/API rollout; this is safer, but not the fastest possible rollout.
- Optional post-deploy throughput probes remain in `scripts/run-load-tests.mjs`.