Public Portal¶
The public portal enables external users to interact with Floh without direct access to the firewalled admin interface. It provides a minimal, user-facing experience for accepting invitations, completing tasks, uploading documents, and managing approvals.
Architecture¶
┌────── Firewall ──────┐
│ │
┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ ┌──────────┐
│ Portal SPA │──▶│ Portal BFF │──▶│ Floh Internal │──▶│ DB │
│ (Angular) │ │ (Fastify) │ │ Server (Fastify) │ │ Redis │
│ port 4201 │ │ port 3001 │ │ port 3000 │ │ SMTP │
└──────────────┘ └──────────────┘ └──────────────────────┘ └──────────┘
Public Public Private
The portal consists of two public-facing services:
- Portal BFF (
packages/portal-bff) — a stateless HTTP proxy - Portal SPA (
packages/portal-web) — a minimal Angular frontend
Both sit outside the organization's firewall. The internal Floh server, database, Redis, and OIDC provider remain inside the firewall, accessible only to the BFF.
Portal BFF¶
The Backend-for-Frontend is a lightweight Fastify application that acts as a pure HTTP proxy. It has no direct connections to the database, Redis, or OIDC provider, minimizing the attack surface of the publicly exposed service.
Route Whitelisting¶
Only a predefined set of user-facing API routes are forwarded to the internal server. All other requests receive a 404 Not Found response.
| Category | Routes |
|---|---|
| Auth | GET /api/auth/config, GET /api/auth/login, GET /api/auth/callback, POST /api/auth/logout, GET /api/auth/me, GET /api/auth/session, POST /api/auth/session/extend |
| Invitations | GET /api/invitations/verify/:token, POST /api/invitations/respond/:token, GET /api/invitations/pending |
| Tasks | GET /api/tasks, GET /api/tasks/:id, POST /api/tasks/:id/complete |
| Approvals | GET /api/approvals, GET /api/approvals/:id, POST /api/approvals/:id/decide |
| Documents | GET /api/documents, GET /api/documents/:id, GET /api/documents/:id/download, POST /api/documents/upload |
| Document Templates | GET /api/document-templates/:id, GET /api/document-templates/:id/download |
| Runs (read-only) | GET /api/runs/:id |
| Health | GET /api/health |
Administrative routes (workflows, users, roles, connectors, audit, reports, config-transfer, etc.) are blocked.
Scope Enforcement¶
The BFF strips the scope query parameter from /api/tasks and /api/approvals requests. This ensures portal users always see only their own assignments, even if a client attempts to request scope=all.
Header Injection¶
On every proxied request, the BFF injects:
X-Portal-Origin— set toPORTAL_FRONTEND_URL, so the internal server knows to redirect back to the portal after authenticationX-Forwarded-For— the original client IP address
Security Features¶
| Feature | Implementation |
|---|---|
| Rate limiting | @fastify/rate-limit — configurable max requests per window |
| Security headers | @fastify/helmet with strict Content Security Policy |
| CORS | Restricted to PORTAL_FRONTEND_URL only |
| Body size limits | Configurable via MAX_UPLOAD_SIZE (default 10 MB) |
Configuration¶
| Variable | Default | Description |
|---|---|---|
PORTAL_PORT |
3001 |
BFF listen port |
PORTAL_HOST |
0.0.0.0 |
BFF listen host |
FLOH_INTERNAL_URL |
http://localhost:3000 |
Internal Floh server URL |
PORTAL_FRONTEND_URL |
http://localhost:4201 |
Portal SPA URL (for CORS, header injection) |
MAX_UPLOAD_SIZE |
10485760 |
Maximum request body size in bytes |
RATE_LIMIT_MAX |
100 |
Max requests per rate limit window |
RATE_LIMIT_WINDOW_MS |
60000 |
Rate limit window in milliseconds |
LOG_LEVEL |
info |
Log level |
Portal SPA¶
The portal SPA is a stripped-down Angular application derived from the main Floh frontend. It includes only the components and routes external users need.
Routes¶
| Path | Component | Auth Required | Description |
|---|---|---|---|
/welcome |
WelcomeComponent | No | Landing page with login button |
/dashboard |
PortalDashboardComponent | Yes | Pending invitations, tasks, approvals |
/tasks |
TaskInboxComponent | Yes | Task inbox (tasks and approvals) |
/invitations/respond |
InvitationRespondComponent | No* | Accept or decline invitations |
/auth/callback |
AuthCallbackComponent | No | OIDC callback handler |
*Invitations require authentication to respond, but the page itself loads without auth to verify the token and prompt login.
Removed Features¶
Compared to the main Floh frontend, the portal SPA does not include:
- Sidebar navigation
- Workflow designer / definition management
- User / role / organization management
- Connector management
- Audit log viewer
- Reports / analytics
- Admin panel
- Permission override controls
- Project and workflow set filters
Dashboard¶
The portal dashboard displays three cards:
- Pending Invitations — invitations awaiting the user's response, with a "Respond" link
- Active Tasks — the user's assigned tasks (up to 5), with a link to the full task inbox
- Pending Approvals — approvals awaiting the user's decision (up to 5)
Internal Server Changes¶
The portal requires two small changes to the internal Floh server:
Portal-Aware Auth Redirects¶
The resolveRedirectBase function in packages/server/src/modules/auth/routes.ts checks for the X-Portal-Origin header on incoming requests. If the header value matches one of the configured ALLOWED_PORTAL_ORIGINS, the server redirects to the portal frontend instead of the admin frontend.
This affects:
- OIDC callback — redirects to
{portalUrl}/auth/callbackafter login - Account deleted — redirects to
{portalUrl}/auth/callback?error=account_deleted - Logout — uses the portal URL as the
post_logout_redirect_uri - Cookie security — sets the
Secureflag based on the portal URL scheme
Notification Invitation Links¶
When PORTAL_FRONTEND_URL is configured, invitation emails link to the portal instead of the admin frontend. This is controlled by:
in packages/server/src/modules/notifications/service.ts.
Internal Server Configuration¶
| Variable | Default | Description |
|---|---|---|
ALLOWED_PORTAL_ORIGINS |
(empty) | Comma-separated list of allowed portal frontend URLs |
PORTAL_FRONTEND_URL |
(empty) | Portal frontend URL for invitation email links |
Development¶
Starting the Portal¶
# Start infrastructure (if not already running)
docker compose -f docker/docker-compose.yml up -d postgres redis mailhog
# Run migrations
pnpm migrate:latest
# Start the internal server + portal
pnpm dev:server &
pnpm dev:portal
Or start everything together:
pnpm dev # starts server + admin web
pnpm dev:portal # starts portal BFF + portal web (in a separate terminal)
Environment Setup¶
Add these variables to your .env file:
# Portal BFF
PORTAL_PORT=3001
FLOH_INTERNAL_URL=http://localhost:3000
PORTAL_FRONTEND_URL=http://localhost:4201
# Internal server (portal support)
ALLOWED_PORTAL_ORIGINS=http://localhost:4201
Running Tests¶
pnpm test:portal-bff # Portal BFF tests (vitest)
pnpm test:portal-web # Portal frontend tests (jest)
Docker Deployment¶
Building¶
Running¶
# Start everything (main + portal)
docker compose -f docker/docker-compose.yml -f docker/docker-compose.portal.yml up -d
Or run only the portal stack (assumes the internal server is already running):
Docker Services¶
| Service | Image | Port | Description |
|---|---|---|---|
portal-bff |
Dockerfile.portal-bff |
3001 | Stateless proxy |
portal-web |
Dockerfile.portal-web |
4201 | Angular SPA via nginx |
Network Topology¶
┌─────────── Public Network ───────────┐
│ portal-web ──▶ portal-bff │
└──────────────────┬───────────────────┘
│
┌──────────────────▼───────────────────┐
│ portal-bff ──▶ server │
│ Internal Network │
│ server ──▶ postgres, redis │
└──────────────────────────────────────┘
The portal-bff and portal-web services are on the public network. The BFF also joins the internal network to reach the server. The server, database, and Redis are on the internal network only, never exposed publicly.
Security Considerations¶
- Minimal attack surface — the BFF has no database, Redis, or OIDC connections; it only forwards whitelisted HTTP requests
- Route whitelisting — only user-facing endpoints are accessible; admin APIs return 404
- Scope enforcement —
scope=allis stripped, preventing privilege escalation - Origin validation — the internal server validates
X-Portal-Originagainst an allowlist before using it for redirects - Rate limiting — the BFF limits request rates to prevent abuse
- CORS — strict origin policy locked to the portal frontend URL
- CSP — Content Security Policy via helmet restricts script/style/image sources
- Cookie security —
Secureflag is set based on the target URL scheme - No secrets in the BFF — the BFF only needs the internal server URL and portal frontend URL; no database credentials, session secrets, or OIDC secrets