docs(devops): security policy, Caddy and DigitalOcean self-hosting, dev container, API debugging
Merged
EV
14d ago+2777+2777+0123456789012345678901234567890123456789-20-20-01234567890123456789
Summary
Six DevOps / docs issues, each an addition next to what exists (nothing removed, no default changed):
- Security reporting (#7010): a
SECURITY.mdpolicy; reports go through GitHub private vulnerability reporting (or security@ever.co) instead of huntr; the issue template chooser links it. - Cal button (#6769): a "Book a call" Cal.com button and link in the README contact section.
- Deploy to DigitalOcean (#9156): a README button and its App Platform template (
.do/deploy.template.yaml): API, web UI and worker from the published GHCR images, an internal Redis, a dev Postgres, one origin with/api,/graphql,/publicand/swg(Swagger UI) routed to the API, every secret asked for at deploy time. - Caddy + docker compose (#7323):
docker-compose.caddy.yml+.deploy/caddy/Caddyfile+.env.caddy.sample: automatic HTTPS for one domain, Postgres, the API (REST, GraphQL, uploads, Swagger UI) and web UI behind one origin, Redis and the worker that deliver events. - Dev Containers (#5570):
.devcontainer/devcontainer.json(Node 26, Docker-in-Docker, GitHub CLI, ports 3000/4200), Codespaces-aware API/web UI URLs, README badges for Codespaces and Dev Containers. - Debugging startup files (#9573): the root cause fixed (copied package source maps pointed at files that do not exist), working VS Code configurations, and
start:api:debug/start:worker:debugto break in startup code.
Why
- #7010: huntr no longer takes reports for this project; reporters had no current private channel, and the repository had no security policy.
- #6769: requested contact option.
- #9156, #7323: there was no one-click or single-command way to self-host with HTTPS. Both templates refuse to start without real secrets rather than shipping defaults (the API itself refuses default JWT/session secrets and seed passwords in production).
- #5570: contributors on Windows/macOS spend a long time on toolchain setup; a dev container makes
yarn bootstrap && yarn startreproducible, and enables Codespaces. - #9573: the API and worker builds copy every compiled
@gauzy/*package intodist/apps/<app>/node_modules/@gauzy, three levels deeper thandist/packages, but the copied.mapfiles kept sources relative to the old place. They resolved todist/apps/<app>/packages/...(which does not exist), so breakpoints inpackages/*/srcnever bound and mapped stack traces pointed nowhere. The old launch configurations also pointed at files that do not exist (start:gauzy,src/main.ts).
What changed
Repository root / docs
SECURITY.md(new): supported versions, scope, how to report privately (GitHub advisory form preferred, security@ever.co), what to include, test only your own installation, what happens next (fix on develop, GHSA + CVE, credit).README.md: security paragraph and contact list point to private reporting andSECURITY.md; Cal.com button + link; Deploy to DO button and explanation (existing DO bullet kept); new sections "Self-hosting with Caddy (automatic HTTPS)", "Dev Container / GitHub Codespaces", "Debugging the API in VS Code"; Codespaces and Dev Containers badges next to the Gitpod one..github/ISSUE_TEMPLATE/config.yml: first contact link "Report a security vulnerability" (advisory form, with security@ever.co as the email alternative).huntrstays in.cspell.json.
Self-hosting templates
.do/deploy.template.yaml(new):api(ghcr.io/ever-co/gauzy-api, health check/api/health),webapp(gauzy-webapp),worker(gauzy-worker, a worker component),redis(Docker Hubredis:alpine, an internal-only service atredis:6379; App Platform dev databases are PostgreSQL only), dev Postgresdb; DB settings bound from${db.*}with TLS on (DB_CA_CERT: ${db.CA_CERT});REDIS_ENABLED=trueon the API and worker;API_BASE_URL/CLIENT_BASE_URL/ALLOWED_ORIGINS=${APP_URL}; heap capped for the instance size (the images default to 12288 MB, as prod's k8s manifest also caps); ingress/api,/graphql,/public,/swg,/swg-json,/swg-yaml→ api (path kept),/→ webapp. The four JWT/session secrets are app-level (the API and worker share them) and the threeDEMO_*_PASSWORDvalues are on the API, alltype: SECRETwith no value.docker-compose.caddy.yml,.deploy/caddy/Caddyfile,.env.caddy.sample(new): only Caddy publishes ports (80, 443 tcp+udp);/api*,/graphql(incl. the graphql-ws upgrade),/public/*and the Swagger UI (/swg*,/swg-json,/swg-yaml) go to the API, the rest to the web UI (the Scalar reference at/docsis left out on purpose: its page loads its script from a CDN, which here would run on the web app's origin; the Caddyfile says how to add it); Caddy drops a client-sentCF-Connecting-IP;TRUST_PROXY=1(the API is reachable only through Caddy); every secret andDB_PASSis${KEY:?...}, so compose refuses to start without it. Redis and the worker always run withREDIS_ENABLED=true, as in docker-compose.yml: without them the API registers no outbox dispatch, so webhooks, the search index, entitlement grants and GraphQL subscriptions would receive no events.- Generation hints:
openssl rand -hex 64for the JWT/session secrets and the database password,openssl rand -hex 24for theDEMO_*_PASSWORDvalues (the sign-in form takes at most 50 characters). .gitignore,.dockerignore:.env.caddy(the filled-in copy holds secrets).
packages/config
database-helpers.ts:decodeCaCertificate()—DB_CA_CERTkeeps accepting base64 and now also accepts the PEM text itself (what DigitalOcean and other managed databases hand out). Detection is by the PEM header, which base64 can never contain..env.samplecomment updated, with how to quote PEM text in an env file (unquoted, dotenv reads only its first line).database-tls-options.spec.ts(new).
apps/api / apps/worker build
apps/api/config/source-map-rebase.js(new): rewrites the relativesourcesof each copied.mapfor its new directory (absolute and URL sources untouched, relativesourceRootfolded in). Never throws: a map it cannot read is copied as is.apps/api/config/webpack-package-copy-patterns.js: every package copy uses it (used by both the API and the worker builds). Cached per file content (copy-webpack-plugin transform cache) for every package except@gauzy/config, whose existing environment selection already runs uncached and now composes with it.apps/api/config/source-map-rebase.test.mjs(new, node:test).
Dev tooling
.vscode/launch.json: all three entries kept and fixed in place. Debug Server (attach 9229) now continues an--inspect-brkprocess, skips Node internals and resolves maps underdist/apps/*/node_modules/@gauzy. Launch Program builds and starts the API in watch mode under the debugger (calls Nx directly:yarn ngreplacesNODE_OPTIONS, through which js-debug attaches). Debug Nest Framework runs the last builtdist/apps/api/main.jsunder the debugger. WSL note (localRoot/remoteRoot).package.json:start:api:debug,start:worker:debug(--inspect=inspect-brk). Existing scripts unchanged..devcontainer/devcontainer.json(new) and.devcontainer/codespaces-env.sh(new): in a codespace, the container shells setAPI_BASE_URLto the forwarded API URL (https://<codespace>-3000.<domain>) andCLIENT_BASE_URL/ALLOWED_ORIGINSto the web UI's (-4200). Your own value wins: the shell's, then one in.env.local, then one in.env(anything but the localhost default). Whichever value is used is exported and listed inENV_LOCAL_NO_OVERRIDE(below), so the web UI, the API and the worker agree on it. Nothing changes outside Codespaces. README explains that port 3000 must be Public for the browser to call it.
apps/api / apps/worker env loading
load-env.ts(both):ENV_LOCAL_NO_OVERRIDE, a comma-separated list of variables whose value in the starting environment the tracked.env.localdoes not override. Opt-in: without it nothing changes, and a list written in.envor.env.localhas no effect. Needed because both load.env.localwith override, which replaced the forwarded Codespaces URLs with localhost.load-env.spec.ts(new, both).
CI (static-checks.yml, platform-gates, each step if: always())
node --test apps/api/config/source-map-rebase.test.mjsnode --test .devcontainer/codespaces-env.test.mjstools/scripts/self-hosting-templates-check.mjs(new gate) + its cases. Secrets: reads the required secrets frompackages/core/src/lib/bootstrap/validate-secrets.tsand checks that the DO template, the compose file and its env sample ask for each and ship none (each sample assignment matched as a whole: only nothing, or empty quotes optionally followed by a comment, counts as empty, so a base64 value ending in=andKEY= # note, which compose reads as# note, count as values). Routes: reads the REST prefix, the GraphQL path, the uploaded files'serveRootand the Swagger UI path from the API source and checks that the Caddyfile and the DO ingress send each to the API (DO withpreserve_path_prefix) and the web app's paths to the web app: to thewebappcomponent on DO, and through the catch-allhandle { reverse_proxy webapp:... }in the Caddyfile. Each path is resolved by its proxy's own precedence: App Platform takes the longest prefix. For Caddy, the gate readshandle,handle_pathandrouteblocks and sorts them as Caddy'ssortRoutesdoes, so a scoped or shadowing block is caught and a prefix stripped byhandle_pathis reported. Node built-ins only, like the other gates.
Flags / settings and defaults
- Nothing existing changes behaviour. All new deployment files are opt-in.
ENV_LOCAL_NO_OVERRIDE(new, API and worker): unset by default, which keeps today's precedence (.env.localover the environment over.env). Only the dev container sets it, in a codespace.DB_CA_CERT: base64 works exactly as before; PEM text is additionally accepted.start:api/start:workerunchanged; the:debugvariants are opt-in.- Build output: only the copied
.mapfiles change (theirsourcespaths); emitted JavaScript is byte-identical. - Caddy compose:
GAUZY_DOMAIN(defaultlocalhost), optionalGAUZY_PUBLIC_URL,GAUZY_HTTP_PORT/GAUZY_HTTPS_PORT(80/443),GAUZY_IMAGE_TAG(latest).
Tests
All run locally on this branch (Windows, Node 26.10.0):
NODE_OPTIONS=--experimental-vm-modules npx jest -c packages/config/jest.config.ts --rootDir packages/config database-tls-options→ 9 passed. Wholepackages/configsuite → 10 suites, 158 tests passed.npx tsc -p packages/config/tsconfig.lib.json --noEmit→ exit 0.node --test apps/api/config/source-map-rebase.test.mjs→ 16 pass, 0 fail (includes a CONTROL case showing the map tsc writes does not resolve once copied; each run uses its own temporary directory).- Real webpack + copy-webpack-plugin with the new patterns over compiled
core,config,contracts(ad-hoc script, not committed): 3341 maps copied, every rebased source resolves to the same file as the original map's (3340 exist; 1 is missing for the original too in that checkout); second build from webpack's memory cache identical. Over all compiled packages: 9141 maps / 9677 sources, 0 mismatches, transform cost ~0.8 s for a full copy. node --test tools/scripts/self-hosting-templates-check.test.mjs→ 45 pass, 0 fail.node tools/scripts/self-hosting-templates-check.mjs→ PASSED. Controls on mutated copies: a new required secret,${DB_PASS:-default}and a filled-in sample → 5 problems, exit 1;/graphqldropped from both templates andserveRootmoved in the API source → 4 problems, exit 1; the Swagger routes dropped from the Caddyfile and/swg-jsonfrom the DO ingress → 5 problems, exit 1. Mutation controls on the gate: the previous empty-value pattern, and a web app check that only rejectsapieach fail the new case written for it. For the Caddy reader, 10 mutations each fail at least one case: skipping the sort, Caddy's matcher rule or its length rule; not readinghandle_pathorroute; not marking the stripped prefix; taking a snippet for a site; counting nestedreverse_proxylines; treating*as a path; and never closing blocks.node --test .devcontainer/codespaces-env.test.mjs→ 8 pass, 0 fail (values read as dotenv reads them: inside quotes, before a # comment, trimmed; outside a codespace nothing is set; inside, the forwarded URLs; the tracked.env.local's localhost values count as defaults; a shell value, then.env.local, then.envwins; every value used is listed inENV_LOCAL_NO_OVERRIDE, once, also from a nested shell). Controls: the file check disabled → 1 fail; the previous versions of the script → 3 fail, then 1 fail. Gate: the old unanchored empty-value check → 2 fail.NODE_OPTIONS=--experimental-vm-modules npx jest -c apps/api/jest.config.ts load-env plugins.matrix -t "loadEnv|keptOverEnvLocal|API boot order"→ 13 passed (the 7 new cases and the existing boot-order cases throughpreload-env). Workerload-env.spec.ts→ 3 passed. Control with the restore line disabled → 1 fail.- End to end:
codespaces-env.shsourced in bash with Codespaces variables, then the API's ownload-env.ts(Node type stripping) in a workspace holding the tracked.env.local→API_BASE_URL/CLIENT_BASE_URL/ALLOWED_ORIGINSare the forwarded URLs,DB_TYPEstill comes from.env.local; a custom URL in.env→ that URL; a URL already in the shell → that URL; without the Codespaces variables →http://localhost:3000as before. node tools/scripts/gate-wiring-check.mjs→ PASSED (33 gates wired, eachif: always()).docker compose -f docker-compose.caddy.yml --env-file <filled test env> config -q→ exit 0 (Docker Compose v5.4.0). With the blank.env.caddy.sample→ exit 1, "required variable … is missing a value: set … in .env.caddy". Services: db, redis, api, webapp, worker, caddy; only caddy publishes ports;REDIS_ENABLED=trueon api and worker; the API waits for a healthy Redis..devcontainer/devcontainer.jsonvalidated against the officialdevContainer.base.schema.json(Ajv + jsonc-parser): valid (including the object-formpostCreateCommand); a deliberately invalid control is rejected. Image tag and both features exist in their registries..do/deploy.template.yaml: strict YAML parse (unique keys) plus checks: no duplicate env keys per component, valid scopes, no SECRET with a value; the GHCR images it references are pullable anonymously..vscode/launch.jsonvalidated against js-debug 1.104.0's ownnodelaunch/attach schema (unknown properties rejected): all three configurations valid (the previous Debug Nest Framework entry was not:protocolwas removed from js-debug).- Prettier
--checkon every new/changed file except README.md (develop's README is not Prettier-formatted; the additions follow its existing style). ESLint on the changed JavaScript: 0 errors.
Manual verification still needed
- Deploy to DO button: one click-through on a DigitalOcean account, before #9156 is closed. Known risk: DigitalOcean's button page lists four combinations (one service or one static site, each with or without a Dev Database), and its examples all build from a git repository. This template has three services (API, web app, internal Redis) and a worker, all from prebuilt GHCR / Docker Hub images, plus a Dev Database. The button may refuse it. If it does, the follow-up (owner decision) is a smaller button template, or manual App Platform steps.
doctlwas not available here. Worth checking in the same run: the internalredisservice (internal ports only) and the worker reaching it atredis:6379, and GraphQL subscriptions over/graphql. - Caddy stack:
docker compose -f docker-compose.caddy.yml --env-file .env.caddy up -don a host with a DNS name (no Docker daemon here, so the stack andcaddy adaptwere not run; the compose file was validated client-side only). - Dev container / Codespaces: one container build, and in a browser codespace the web UI reaching the API once port 3000 is Public (also whether the Angular dev server accepts the forwarded host name).
- VS Code debugging: a breakpoint in
packages/core/src/lib/bootstrap/index.tswith Debug Server +yarn start:api:debug, and with Launch Program. No VS Code here; the configurations were validated against js-debug's schema and js-debug's attach behaviour was read from its source.
Risk and rollback
- Low. All deployment files and scripts are new and opt-in;
.vscode/launch.jsononly affects VS Code users. The one runtime change,ENV_LOCAL_NO_OVERRIDEin the API and worker env loaders, does nothing unless that variable is set. - The build change touches only
.mapfiles of the copied packages. In the Docker images, where TypeScript sources are not shipped, the source paths shown in mapped stack traces change text (they did not resolve to a file before either); line and column numbers are unchanged. - The DO template now runs four components plus the dev database (API 2 GB, web UI 0.5 GB, worker 1 GB, Redis 0.5 GB), so a click costs more than the API + web UI alone; without the worker and Redis, events are never delivered.
- Rollback: revert the PR, or individual commits.
- GitGuardian's three "Generic Password" findings are false positives: they are the
DEMO_*_PASSWORD: ${DEMO_*_PASSWORD:?set … in .env.caddy}lines ofdocker-compose.caddy.yml, required-variable references with an error message and no value (theself-hosting-templates-checkgate enforces exactly that shape).
Decisions (owner answers)
SECURITY.md: no fixed acknowledgement time or disclosure window for now ("we aim to acknowledge your report promptly"). Wrapped at 80 columns for markdownlint MD013.- DO template keeps the dev database (
production: false). - If the button rejects image components, switching to source builds is a follow-up.
- Docs-site pages for Caddy self-hosting and debugging are a follow-up.
Closes #7010
Closes #6769
Closes #7323
Closes #5570
Closes #9573
Refs #9156 (implemented; close after the click-test above)
🤖 Generated with Claude Code