Deployment
Run and deploy the Aether Labs SSO stack — API, Postgres, and the three frontends.
Deployment
Aether Labs SSO is a small stack: one API, an external Postgres, and three static
frontends (accounts-web, myaccount-web, admin-web), each served by nginx with
SPA fallback.
Services and ports
| Service | Container port | Dev host port | Production domain |
|---|---|---|---|
api | 3000 | 4000 | https://sso.aetherlabs.agency |
accounts-web | 80 | 3001 | https://accounts.aetherlabs.agency |
myaccount-web | 80 | 3002 | https://myaccount.aetherlabs.agency |
admin-web | 80 | 3003 | https://admin.sso.aetherlabs.agency |
API host vs login host
The API is sso.aetherlabs.agency. accounts.aetherlabs.agency is the login
UI. Point VITE_API_URL at the API host, not the login host.
Run locally with Docker
cp .env.example .env # optional for dev; override secrets/ports here
docker compose up --build
# API → http://localhost:4000 (health: /health)
# Login → http://localhost:3001
# Profile → http://localhost:3002
# Admin → http://localhost:3003On first boot the API creates the auth schema, syncs its tables, and seeds the
default roles. There is no bundled database — point DATABASE_URL at a Postgres
you control.
Run the API directly
cd api
cp env.example .env # fill in real values; never commit real env files
pnpm install
pnpm run dev # boots on :4000 (needs a reachable Postgres with an auth schema)Environment variables
| Variable | Purpose |
|---|---|
DATABASE_URL | Postgres connection string. No bundled DB. |
JWT_ACCESS_SECRET | Signs access tokens. Must match any consumer that verifies locally. |
JWT_REFRESH_SECRET | Signs refresh tokens. |
JWT_ISSUER | Advertised by the OAuth discovery document (aetherlabs-sso). |
COOKIE_DOMAIN | localhost in dev, .aetherlabs.agency in production. |
ALLOWED_ORIGINS | Comma-separated CORS allowlist of frontend origins. |
LOGIN_REDIRECTS | Optional key=url overrides for the post-login destination allowlist. |
BCRYPT_ROUNDS | Password hashing cost (default 12). |
LOG_LEVEL | Logging verbosity. |
Frontend variables are baked in at build time by Vite:
| Variable | Used by | Purpose |
|---|---|---|
VITE_API_URL | all three | The API base URL. |
VITE_ACCOUNTS_URL | myaccount-web, admin-web | Login UI to redirect to when signed out. |
VITE_MYACCOUNT_URL | accounts-web | Where to send the user after login. |
Rebuild required
Changing any VITE_* value requires rebuilding that frontend image
(docker compose build). Editing the env alone has no effect.
Production notes
NODE_ENV=production
COOKIE_DOMAIN=.aetherlabs.agency
ALLOWED_ORIGINS=https://accounts.aetherlabs.agency,https://myaccount.aetherlabs.agency,https://admin.sso.aetherlabs.agency- In production the cookies are
secureandsameSite=none, so HTTPS is mandatory. The reverse proxy (Traefik on Dokploy) terminates TLS. - Secrets must be strong and identical across every consumer that verifies tokens
locally:
openssl rand -hex 32. - With
docker-compose.ymlalone there are no published host ports — services are reached only through the proxy. The dev host ports come fromdocker-compose.override.yml.
Bootstrap the production admin
Register a user, then grant super_admin inside the running API container:
node dist/scripts/bootstrapAdmin.js --email you@example.comDeploying to Dokploy
For a step-by-step walkthrough — Traefik domains, TLS, volume persistence, and the
build-time env caveat — see the repository's
docs/dokploy-deployment-guide.md.
Keep secrets out of docs
Never commit real .env files or paste live JWT_ACCESS_SECRET, database URLs,
or passwords into documentation. Reference variable names only.
See also
- Same-domain SSO — app-side configuration.
- Roles & permissions — bootstrapping an admin.
- API reference — the health endpoint and everything else.