Skip to content

Dev Quickstart

Prerequisites

  • Node.js >= 24 (corepack enable for 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):

pnpm generate-certs

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

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:

pnpm run dev:mcp

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:

pnpm dev

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_ISSUER
  • OIDC_CLIENT_ID
  • OIDC_CLIENT_SECRET
  • OIDC_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:

node scripts/setup-authifi-resource-server.mjs --help

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.

Stop Infrastructure

pnpm docker:down