← Portal Domain Docs

Architecture

Two applications, one product: a Vue SPA and an Express BFF, shipped together as a single container and deployed once per operator.

The two applications

portal/ is a Vue 3.5 + TypeScript single-page application. portalBackend/ is an Express 4 + TypeScript server, and it is a BFF - Backend for Frontend: it exists only to serve this SPA, never as a general API. In production they are one Docker image (lf-portal-full-express): the built SPA static files are copied into the Express image, and Express serves both the static bundle and the /api surface.

Frontend shape

The BFF's three jobs

  1. Authentication and sessions. Passport strategies (local, Azure AD, Okta) log the user in; the session lives server-side in MySQL (express-session + connect-session-sequelize). The browser holds only a session cookie - no JWT.
  2. Permission-gated reverse proxy. routes.json maps ~1,380 frontend URLs to ~115 upstream DNO services. Keys are matched as literal strings - there is no /:id support. Dynamic segments travel as query params (/xxxById?id=...) and are spliced into the upstream path via pathReplaceTokens. Upstream calls usually ride a service token, not the user's token. Both sides (frontend service + routes.json entry) must be wired, or the call 404s.
  3. System of record for Portal-local state. Tenants, views, roles, permission assignments, and settings live in the Portal's own MySQL through Sequelize. Business data (customers, orders, bills) never lives here - that is the DNO services' job, and the DNO backend knows nothing about Portal users or permissions.

The permission model

Access is layered, and every layer checks independently:

LayerGate
TenantA View (module switch) must be enabled for the tenant before its permissions exist for roles
RoleRead/Write permission keys (e.g. OffersRead) are assigned to roles, roles to users
Sidebar + routerisUserAllowed(...keys) - OR over keys
WidgetsEach v-if evaluated alone - no inheritance, partial pages are normal
API proxyroutes.json permissions array - OR again; UI passing never implies API passing

Write keys forceEnable their Read pair; clearing Read forceDisables Write. AND inside one permission array is not supported - use a composite key or split routes.

Open full screen ↗
Open full screen ↗

Next: Deployment - how this image reaches staging and production.