Skip to content

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:

  1. Portal BFF (packages/portal-bff) — a stateless HTTP proxy
  2. 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 to PORTAL_FRONTEND_URL, so the internal server knows to redirect back to the portal after authentication
  • X-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:

  1. Pending Invitations — invitations awaiting the user's response, with a "Respond" link
  2. Active Tasks — the user's assigned tasks (up to 5), with a link to the full task inbox
  3. 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/callback after 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 Secure flag based on the portal URL scheme

When PORTAL_FRONTEND_URL is configured, invitation emails link to the portal instead of the admin frontend. This is controlled by:

const baseUrl = this.config.portalFrontendUrl || this.config.frontendUrl;

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

docker compose -f docker/docker-compose.portal.yml build

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 compose -f docker/docker-compose.portal.yml up -d

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

  1. Minimal attack surface — the BFF has no database, Redis, or OIDC connections; it only forwards whitelisted HTTP requests
  2. Route whitelisting — only user-facing endpoints are accessible; admin APIs return 404
  3. Scope enforcementscope=all is stripped, preventing privilege escalation
  4. Origin validation — the internal server validates X-Portal-Origin against an allowlist before using it for redirects
  5. Rate limiting — the BFF limits request rates to prevent abuse
  6. CORS — strict origin policy locked to the portal frontend URL
  7. CSP — Content Security Policy via helmet restricts script/style/image sources
  8. Cookie securitySecure flag is set based on the target URL scheme
  9. 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