Dev Quickstart¶
Prerequisites¶
- Node.js >= 24 (
corepack enablefor pnpm) - Docker & Docker Compose
Setup¶
git clone <repo-url> floh && cd floh
cp .env.example .env # defaults work as-is for local dev
pnpm install
Start Infrastructure¶
pnpm docker:infra # Postgres :5432, Redis :6379, MailHog :8025
pnpm migrate:latest # apply database migrations
Default Ports¶
| Port | Service | Env var |
|---|---|---|
| 7070 | API server | PORT |
| 7071 | Portal BFF | PORTAL_PORT |
| 7072 | Admin frontend | — |
| 7073 | Portal frontend | — |
| 7080 | Form-builder (visual editor) | — |
HTTPS dev uses the same ports; enable TLS with TLS_CERT_FILE / TLS_KEY_FILE and the pnpm dev:https / pnpm dev:portal:https scripts. The form-builder dev server (pnpm dev:form-builder) defaults to HTTPS already; use pnpm dev:form-builder:http for the explicit HTTP opt-out.
Run Services (HTTPS — preferred)¶
Generate local certs first (one-time):
The Angular dev-server scripts (pnpm dev:https,
pnpm dev:portal:https, pnpm dev:form-builder) auto-invoke
generate-certs.mjs --quiet as a preflight, so on a fresh clone
the certs appear automatically on first start — pnpm generate-certs
is only required up-front when you want the verbose trust-store and
.env guidance the bare command prints. Run it once now (or skip and
read it on first dev start) so you have the trust-store / NODE_EXTRA_CA_CERTS
hints in your terminal.
Then set TLS_CERT_FILE, TLS_KEY_FILE, and NODE_EXTRA_CA_CERTS in .env:
TLS_CERT_FILE=certs/localhost.crt
TLS_KEY_FILE=certs/localhost.key
NODE_EXTRA_CA_CERTS=certs/localhost.crt
Set FLOH_INTERNAL_URL=https://localhost:7070 when the API serves HTTPS so the portal BFF and other Node clients target the same scheme. For pnpm dev:portal:https, also rely on PORTAL_LISTEN_TLS=true (injected by that script) plus the same TLS_CERT_FILE / TLS_KEY_FILE as the API so the BFF listens on https://localhost:7071.
NODE_EXTRA_CA_CERTS tells Node.js to trust the self-signed certificate, which is required when Node clients (portal BFF, scripts, tests) call https://localhost:7070.
TLS by tier¶
| Tier | TLS enabled by | Required settings |
|---|---|---|
API server (packages/server) |
.env |
TLS_CERT_FILE, TLS_KEY_FILE |
| Node clients to API (portal-bff, scripts) | .env |
NODE_EXTRA_CA_CERTS when API uses self-signed HTTPS |
Admin frontend (packages/web) |
script | pnpm dev:https enables HTTPS UI |
Portal frontend (packages/portal-web) |
script | pnpm dev:portal:https enables HTTPS UI |
Form-builder app (packages/form-builder-app) |
script (HTTPS by default) | pnpm dev:form-builder already serves HTTPS on port 7080; use pnpm dev:form-builder:http for the explicit HTTP opt-out |
Portal BFF listener (packages/portal-bff) |
PORTAL_LISTEN_TLS + TLS_* |
Set PORTAL_LISTEN_TLS=true (done by pnpm dev:portal:https) so the BFF serves HTTPS on port 7071 |
| Portal BFF upstream protocol | FLOH_INTERNAL_URL |
Must match API scheme (https:// preferred) |
| Command | Service | URL |
|---|---|---|
pnpm dev:server |
API server (start first) | https://localhost:7070 (with TLS in .env) |
pnpm dev:https |
Server + admin frontend | https://localhost:7070 / https://localhost:7072 |
pnpm dev:portal:https |
Portal (BFF + frontend) | https://localhost:7071 (BFF) / https://localhost:7073 (SPA) |
pnpm dev:form-builder |
Form-builder (visual editor) | https://localhost:7080 (HTTPS — default) |
The form-builder defaults to HTTPS so its iframe embeds cleanly
inside both http:// and https:// parent pages. Browsers block
mixed-content iframes (http:// inside https://), but never the
inverse. The default formBuilderEmbedUrl in
packages/web/src/environments/environment.ts is
https://localhost:7080/ to match. If you opt out via
pnpm dev:form-builder:http, also flip formBuilderEmbedUrl back
to http://localhost:7080/ for the session.
The portal and admin frontend scripts do not start the API server. Run pnpm dev:server or pnpm dev:https in a separate terminal first.
Or start the main stack in one terminal:
pnpm dev:https starts the main app stack and excludes the MCP server by default.
To run MCP in a separate terminal, first configure auth (FLOH_API_TOKEN, or
FLOH_REFRESH_TOKEN + OIDC_ISSUER + OIDC_CLIENT_ID), then run:
Run Services (HTTP — alternative)¶
| Command | Service | URL |
|---|---|---|
pnpm dev:server |
API server | http://localhost:7070 |
pnpm dev:web |
Admin frontend | http://localhost:7072 |
pnpm dev:portal |
Portal (BFF + frontend) | http://localhost:7071 / http://localhost:7073 |
pnpm dev:form-builder:http |
Form-builder (visual editor) | http://localhost:7080 |
(pnpm dev:form-builder itself runs HTTPS; the :http suffix is the
explicit opt-out, mirroring how the rest of the stack flips between
plain and TLS modes.)
Or start everything at once:
Note: pnpm dev is almost all-HTTP — it fans out to every workspace's
dev script in parallel, which means the form-builder still starts on
HTTPS (port 7080) inside this otherwise-HTTP stack. This is by
design so the iframe URL hardcoded in
packages/web/src/environments/environment.ts
(formBuilderEmbedUrl: "https://localhost:7080/") loads cleanly
regardless of the host SPA's scheme — an HTTP iframe inside an HTTPS
host is mixed-content blocked, but never the other way around. The
auto-cert preflight in form-builder's dev script (PR #381) means
operators don't need to run pnpm generate-certs first; the cert pair
is created on first start. To run a strict all-HTTP stack with no TLS
at all, start the per-package shortcuts manually
(pnpm dev:server, pnpm dev:web, pnpm dev:portal,
pnpm dev:form-builder:http) and flip formBuilderEmbedUrl to
http://localhost:7080/ for the duration of the session.
Useful URLs¶
| URL | What |
|---|---|
| https://localhost:7070/api/docs | Swagger UI (preferred) |
| http://localhost:7070/api/docs | Swagger UI (HTTP-only dev) |
| http://localhost:8025 | MailHog inbox |
Auth¶
OIDC configuration is required in all environments. The server fails fast when any required field is missing:
OIDC_ISSUEROIDC_CLIENT_IDOIDC_CLIENT_SECRETOIDC_REDIRECT_URI
New Environment Variables¶
The following env vars were added as part of the architecture hardening work:
| Env var | Default | Description |
|---|---|---|
OIDC_TOKEN_ISSUER |
OIDC_ISSUER |
Expected iss in tokens (set when discovery uses a CNAME alias) |
ALLOWED_ORIGINS |
FRONTEND_URL |
Comma-separated CORS allowed origins |
DB_POOL_MAX |
10 |
Max database pool connections |
DB_POOL_MIN |
2 |
Min idle database pool connections |
DB_POOL_IDLE_TIMEOUT_MS |
30000 |
Idle connection timeout |
DB_POOL_CONNECTION_TIMEOUT_MS |
5000 |
Connection acquisition timeout |
STUCK_RUN_TIMEOUT_MINUTES |
30 |
Timeout for stuck workflow runs |
These can also be managed via Admin > Security Settings in the web UI (requires settings:manage permission).
CSRF Tokens¶
When OIDC is enabled, the server sets a floh_csrf cookie on login. The frontend automatically sends this as X-CSRF-Token on mutating requests. API clients using Bearer tokens are not affected.
Webhook Configuration¶
Connector webhooks now require HMAC-SHA256 signature verification. Set a webhook secret on the connector and send X-Webhook-Signature: <hmac-sha256-hex> with each webhook request.
MCP Server (AI Integration)¶
To set up the MCP server for Claude Desktop or Cursor, see MCP Setup. For Authifi RBAC configuration, run:
Reporting¶
The admin UI includes a full reporting system at /reports/* with predefined templates, a visual query builder, multi-format export (PDF, Excel, CSV, Markdown), saved reports with sharing and scheduling. See Reporting for details.
Predefined templates are automatically seeded on server startup (migration 035_reporting). PDF export requires Puppeteer; Excel export requires ExcelJS — both are included in dependencies.
Tests¶
pnpm test:unit # server unit (vitest)
pnpm test:integration # server integration (testcontainers)
pnpm test:web # frontend (jest)
pnpm test:e2e:local # local browser E2E (testcontainers + Playwright)
pnpm test # all
The local E2E command owns its own ports (17073 for web, 17074 for API) so it can run
beside the normal dev stack. Install the Playwright browser once with pnpm --filter
@floh/web exec playwright install chromium if prompted.
Troubleshooting¶
Port already in use — if a dev server fails with ELIFECYCLE / exit status 2, a previous process is still holding the port. Find and kill it:
lsof -ti :7070 | xargs kill # server
lsof -ti :7072 | xargs kill # web
lsof -ti :7071 | xargs kill # portal BFF
lsof -ti :7073 | xargs kill # portal web
lsof -ti :17073 | xargs kill # local E2E web
lsof -ti :17074 | xargs kill # local E2E API
Endless "App Update Issue" toast / "Failed to fetch dynamically imported module" / 504 Gateway Timeout for /.angular/cache/.../vite/deps/... — Angular's Vite-based dev-server (@angular/build:dev-server) pre-bundles CommonJS deps into packages/<pkg>/.angular/cache/.../vite/deps/. That cache occasionally wedges (most often after switching builders, upgrading Angular, or interrupting a build mid-optimization), and surfaces as a 504 on a single dep file like primeng_chart.js.
Most cases are now caught automatically by scripts/maybe-clean-vite-cache.mjs which runs as part of dev / dev:https and wipes .angular/cache/ whenever pnpm-lock.yaml, package.json, or angular.json has changed since the last successful start. A separate browser-side guard in GlobalErrorHandler (packages/web/src/app/core/error-handler.ts) prevents the toast/refresh storm even if a wedge slips through.
If you still hit a wedge (e.g. interrupted a build mid-optimization), use the manual escape hatch — stop the dev server (Ctrl+C), nuke the cache, restart:
pnpm --filter @floh/web run dev:https:clean # admin
pnpm --filter @floh/portal-web run dev:https:clean # portal
# or for the full stack:
pnpm dev:https:clean
After restart, in the browser do DevTools → Application → Storage → Clear site data → reload. The first build after a clean takes ~10–20s longer because Vite re-pre-bundles every CJS dep from scratch.