6.9 KiB
FORGE — setup & sync
1. Database
FORGE needs a Postgres database named forge. Either:
- Bundled (compose):
docker compose up -d dbstartspostgres:16-alpinewith database/user/passwordforgeon a named volume.DATABASE_URLin.env.examplealready points at it. - Your own Postgres: create a
forgedatabase and setDATABASE_URL=postgresql://USER:PASS@HOST:5432/forgein.env.
The schema self-bootstraps on boot (initDB() in server/db.ts) — no migrations.
On the very first boot, if the tickets table is empty, the bundled archives in
server/data/ are seeded (idempotent upsert).
Seed data
The board ships pre-populated from server/data/active_archive.json +
closed_archive.json, seeded on the first boot of an empty tickets table.
Those archives are generated from a Let it Snow chrome.storage.local dump
(the sn_tickets, analytics_meta_cache, and jira_status_map keys):
# 1. drop the raw dump at repo root (gitignored — it contains real PII)
# storage-dump.json
node scripts/dump-to-archives.mjs # or: npm run dump-to-archives
# → rewrites server/data/{active,closed}_archive.json
# 2. reload the DB from the regenerated archives (destructive: TRUNCATE + reseed)
npm run reseed
A ticket number can appear in both the active list and the closed cache; the
loader applies closed first, then active, so the live active board wins over
the stale closed snapshot. npm run reseed also works standalone whenever you
want to reset the DB to the bundled archives.
The raw
storage-dump.jsonis a dev artifact and is gitignored. The derivedserver/data/*.jsonstill contain real ticket text and are baked into the Docker image — keep the repo internal, or point the seed at a mounted volume.
2. Run
| command | notes | |
|---|---|---|
| Dev | npm run dev |
server :3000 + Vite client :5173 (proxies /api) |
| Build | npm run build |
tsc + client production build |
| Serve | npm start |
serves the built client from the Express server |
| Docker | docker compose up -d --build |
app on host :3099, bundled DB |
3. Log in (read-API auth)
The whole read UI (/api/tickets, /api/stats, …) is gated behind a
username/password login; /healthz and the token-authed /api/sync are not.
- Set
AUTH_USER/AUTH_PASSand aSESSION_SECRETin.env(see.env.example).SESSION_SECRETis required in production. - On first boot the account is seeded into
app_userswith a bcrypt hash. Sessions are stored in Postgres (user_sessions, auto-created), so they survive restarts. Cookies arehttpOnly+securein production (HTTPS via the reverse proxy). - Changing
AUTH_PASSlater does not update an already-seeded account — delete theapp_usersrow and reboot to re-seed, or update the hash directly.
The database role is least-privilege: DATABASE_URL uses a forge_app role
that owns the app's tables but is not a Postgres superuser.
4. Mint a sync token
The Chrome extension authenticates with a bearer token (fg_<id>_<secret>,
SHA-256 verifier stored server-side — the raw token is shown once).
CLI (recommended):
npx tsx server/mint-token.ts "my laptop"
Over HTTP (only if ADMIN_KEY is set in the environment):
curl -X POST https://forge.mycloud.dp.ua/api/tokens \
-H "x-admin-key: $ADMIN_KEY" -H 'content-type: application/json' \
-d '{"label":"my laptop"}'
Copy the fg_… value — it is not recoverable later.
5. Install the Chrome extension
chrome://extensions→ enable Developer mode → Load unpacked → select theextension/folder.- Open the extension's options (Server & token settings). Enter:
- Server URL — e.g.
https://forge.mycloud.dp.ua(orhttp://localhost:3000) - Sync token — the
fg_…value from step 4 - Click Test connection (hits
/healthz), then Save. Chrome will ask to grant access to the server origin — accept it.
- Server URL — e.g.
- Sign in to
https://rbassist.service-now.comin the same browser. - Click the extension toolbar icon → Sync now.
What the sync does
The service worker runs a collector in the ServiceNow page context, so it
reuses your live session cookie and CSRF token (g_ck). It pages the
sc_req_item Table API for active=true tickets in Marketing Web Presence
groups, maps them into FORGE's ticket shape, and POSTs them to /api/sync in
chunks of 100 with Authorization: Bearer <token>. The server upserts by ticket
number, so re-syncing is safe and never duplicates.
6. Deploy to the Synology NAS
FORGE ships to the NAS as a docker-compose stack (bundled Postgres + app), fronted
by the Synology reverse proxy (forge.mycloud.dp.ua → the published app port 3089).
Two scripts (ported from the Husky template):
scripts/push-to-nas.sh(local,npm run deploy) — preflight (pinned SSH key,IdentitiesOnly) → test gate → rsync the tree to the NAS (excludes node_modules/dist/secrets/storage-dump.json) → rundeploy.shover SSH. Falls back to tar-over-ssh if macOS's openrsync is the only rsync.scripts/deploy.sh(on the NAS) — Synology PATH+sudo handling → optionalgit pull(--pull) →compose build→up -d --remove-orphans→ poll the app container until Docker reports healthy.--freshtears down first but never passes-v, so theforge-dbvolume (your ticket data) is preserved.
One-time, on the NAS: create ${NAS_PATH}/.env with the production secrets —
SESSION_SECRET, AUTH_USER, AUTH_PASS, POSTGRES_PASSWORD (compose sets
DATABASE_URL/PORT/NODE_ENV itself). See .env.example. Secrets live only on
the NAS and are never synced.
Config is baked into push-to-nas.sh (defaults: NAS_HOST=mycloud.dp.ua,
NAS_USER=d.tkachenko, NAS_PORT=2323, NAS_PATH=/volume1/docker/forge,
NAS_KEY=~/.ssh/id_ed25519). Override via env vars or an optional .deploy.env
(see .deploy.env.example). Key-based SSH to the NAS must already work.
npm run deploy # test → sync → build → up → health
npm run deploy -- --fresh # recreate the stack (db volume kept)
npm run deploy -- --pull # git pull on the NAS first
SKIP_TESTS=1 npm run deploy # emergency bypass of the local test gate
The reverse-proxy mapping is configured once in DSM, not by the script.
Security notes
GATSBY_-style client exposure does not apply here, but the same rule holds: the API token andg_ckare never logged. Only the SHA-256 of the token secret is stored;token_idis a non-secret locator.POST /api/syncis rate-limited (60 req/min) and rejects any request without a valid, unrevoked, unexpired token (401).server/data/*.jsoncontains real ticket data (names, comments). It ships with the repo for the demo seed — treat the repo as internal, or delete the archives and rely on live syncs only (the app runs fine with an empty seed).