Make It Yours
This template ships as a working demo: service-1 with the auth and
notifications bounded contexts, service-2 with the tasks bounded context,
and the public-page frontend. Here is how to turn it into your own product.
1. Rebrand
The brand name comes from a few places:
package.jsonname(root andfrontend/public-page).- The
APP_NAMEenv var, which the backend uses for the outbound email from-name (defaults toForgeStack). Set it in your service.envfiles. - The frontend copy under
frontend/public-page/messages/*.jsonand the landing page components.
Search the repo for ForgeStack and forgestack and replace what you want.
2. Set your domain and TLS
Local dev needs no domain: everything is served on localhost behind Caddy
with a local CA. For production, set your real domain and registry in
infra/env/infra.env.prod (copy it from infra.env.prod.example):
DOMAINis your apex domain. Caddy serves everything on that one origin: the app atDOMAINand each service under a path prefix (DOMAIN/svc1,DOMAIN/svc2, and so on), with automatic Let's Encrypt certificates. Same-origin routing keeps the auth cookies first-party.DOCKER_REGISTRYis where built images are pushed and pulled (for exampledocker.io/your-usernameorghcr.io/your-org). Any registry works: the deploy script logs in to the registry host it finds in this value.- Fill in the database, Redis, Grafana, and
CADDY_BASIC_AUTH_HASHsecrets in the same file. Caddy refuses to start in production without the basic-auth hash, since Portainer sits behind it.
3. Rename or replace the example code
- Services:
service-1andservice-2are examples. Keep one, rename it, or generate your own withnpx nx g @forgestack/tools:service <name>. - Bounded contexts:
auth,notificationsandtasksare the demo domains. Generate your own withnpx nx g @forgestack/tools:bounded-context <name> --service=<service>(see Adding a Service and the generator). - Framework vs example:
backend/libs/commonmixes reusable framework code with demo-specific code. ItsREADME.mdmarks which is which, so you know what to keep and what to replace.
4. Provision a server (one command)
npm run prod:setup provisions a fresh Ubuntu VPS end to end: it installs
Docker, creates a non-root deploy user (no sudo) with your SSH key, hardens SSH,
enables fail2ban and unattended security upgrades, opens the firewall,
configures Docker log rotation, sets up swap, and registers the daily Mongo
backup cron. It reads SERVER_IP and SERVER_USER from
infra/env/infra.env.prod, so fill that file in first, and prompts for the
server's root password on the first run (or pass ROOT_PASS=... to run it
non-interactively).
Then fill in the service and frontend env files, point DNS at the server,
and deploy with npm run prod:update -- 5. The
first-deploy checklist lists every
step in order, from the env files to the first admin account.
Troubleshooting
dev:startcannot find Node: the script never auto-installs Node. Install the version in.nvmrc(nvm install && nvm use) and re-run.- Browser does not trust the local certificate: run
npm run dev:trust-certto install Caddy's local CA, then restart the browser. - A service returns 502 through Caddy right after
dev:start: on a cold start the service is still installing dependencies and building. Watch it withnpm run dev:logsuntil you see "Nest application successfully started". - Kafka or Mongo fails to start after a recreate: the dev volumes may hold
stale cluster state.
npm run dev:restart:alldoes a full down and up (anddev:startclears stale ZooKeeper state when Kafka crashed before). It keeps the data volumes; if the problem persists, wipe them withdocker compose -f infra/compose/docker-compose.dev.yml down -v(this deletes all local data) and runnpm run dev:startagain. - A handler or controller is missing at runtime: discovery fails loud now, so check the boot logs for a load error or a "matches suffix but exports no class" warning (usually a rename or a wrong export).
- Production deploy reports a service is unhealthy:
prod:updatewaits for health and stops on failure. SSH in and readdocker compose -f compose/docker-compose.yml logs --tail=100.