Skip to content

ADR 33: Frontend Modular Monolith and Package Boundaries

Status: Accepted
Date: 2026-06-02
Last Updated: 2026-06-11 (pds-portal sibling app per ADR 42)

Context

Substratum ships one primary web dashboard (apps/file-explorer) for SaaS, self-hosted edge, and cloud-connected deployments. We need environment-specific navigation and copy (SaaS plan limits vs home-server operator messaging) without adopting runtime micro-frontends (separate deployables, module federation, or iframe remotes).

The backend is already a modular monolith (ADR 01). The frontend should mirror that pattern: one build, one origin, strict internal boundaries—not multiple SPAs coordinated at runtime.

Constraints from ADR 08:

  • Mithril.js for a lightweight dashboard.
  • Self-hosted installs stage a single static bundle beside the edge (ADR 23).
  • AT Protocol OAuth requires a stable canonical app origin per deployment.

Decision

  1. No micro-frontend architecture (v1). We do not use runtime module federation, import-map remotes, or iframe-embedded sub-apps. We reject independent frontend deploy cadences until a documented need outweighs operational cost.

  2. Modular frontend monolith. The web UI remains one Vite application (apps/file-explorer) with logical feature areas expressed as workspace packages or clear src/ boundaries, not separate hosted apps.

  3. Package separation (Nx / pnpm). Shared and feature-specific code lives in libraries; apps are thin hosts:

    Package / areaResponsibility
    @substratum/ui-kitPresentational primitives only (buttons, layout chrome, settings inputs). No routing, session, or API calls. Shared across all Mithril web apps.
    @substratum/api-clientGenerated OpenAPI client.
    apps/file-explorerCustomer dashboard on app.*: route table, resolvers, feature pages, services, i18n catalogs.
    apps/landingMarketing on substratum.cloud (ADR 36); ui-kit + own Lingui catalogs — not file-explorer routes.
    apps/adminOperator UI on admin.* (ADR 38); ui-kit + own Lingui catalogs — not file-explorer routes.
    apps/pds-portalIdentity UI on pds.* (ADR 42); sign-in + OAuth consent; ui-kit + own Lingui catalogs — not file-explorer routes.
    apps/installer-guiSeparate desktop product (Tauri); shares ui-kit and theme, not the web route graph.
    apps/substratum-explorerThin desktop webview shell to the self-hosted edge URL.

    Future extraction (optional): libs/platform (session, bootstrap, deployment context), libs/shell (nav manifest + route registration API)—consumed by file-explorer and/or sibling apps via workspace imports only, never by copying apps/file-explorer/src into another app.

  4. Application shell (in-process). “Shell” means shared layout and navigation inside the SPA: header, sidenav, locale picker, gateway status, logout. Nav items may be filtered by deployment context from gateway bootstrap (Glossary: deployment context). This is not a micro-frontend host loading foreign bundles.

  5. Environment customization uses:

    • Server: deployment_mode / bootstrap payload (authoritative).
    • Build: optional thin entry wrappers only if branding diverges severely (default: one artifact).
    • Client: nav manifest + copy keys—not separate applications.
  6. Alignment with backend. Feature folders follow the same bounded-context idea as ingress handlers: explorer (drives/tree/upload), account (plan/limits/settings), auth (login/signup)—as directories and services under apps/file-explorer, not deployable MFEs.

  7. External auth surfaces (not in file-explorer). One origin applies to apps/file-explorer on app.* only. These are separate origins with separate login flows — link out, do not embed:

    SurfaceOriginAuthIn file-explorer
    Customer appapp.*AT Proto OAuth → substratum_sessionYes (host)
    Support (Discourse)support.*OIDC → id.support.* bridge → PDS (ADR 39)External link only
    Operator adminadmin.*PDS OAuth → substratum_ops_session (ADR 38)No
    Operated PDS identitypds.*PDS session + OAuth consent on portal (ADR 42)No (redirect only)
    Marketingsubstratum.cloudNone / checkoutNo

    apps/file-explorer MAY show support seat status on /account and link to support.*; it MUST NOT host OIDC bridge callbacks, Discourse iframes, or support SSO (ADR 39 R13).

  8. Shared ui-kit, isolated app code. Sibling web apps (apps/landing, apps/admin, and future hosts) reuse @substratum/ui-kit and Midnight Gallery design tokens for visual consistency (ADR 14, ADR 36). Application code stays isolated:

    • Each app is its own Nx/Vite package: own src/, routes, services, auth wiring, and Lingui catalogs.
    • Apps MAY depend on @substratum/ui-kit, @substratum/api-client, and future shared libs/*MUST NOT import from apps/file-explorer/src (no shared route modules, no re-exporting explorer pages into admin).
    • App-specific composed screens (e.g. operator entitlement lookup table) live in that app; promote to ui-kit only when a second app needs the same presentational primitive.
    • support.* (Discourse) uses Discourse’s UI; Substratum does not fork forum chrome into file-explorer. Optional bridge error/status HTML MAY use ui-kit in a minimal isolated package if needed (ADR 39).

Rejected alternatives

AlternativeWhy rejected (for now)
Runtime micro-frontends (Vite federation, etc.)Weak Mithril ecosystem for federation; OAuth origin and version skew; conflicts with single-bundle self-hosted staging.
iframe remotesPoor UX, cookie/session friction, accessibility cost.
Copy file-explorer routes into admin/landingDuplicates auth and coupling; violates isolated-app rule (§8). Use ui-kit + app-local pages.
Duplicate Button/Layout in each appDefeats ui-kit; use @substratum/ui-kit with string props from each app’s Lingui.

Revisit micro-frontends only if independent teams need independent release trains and the above costs are accepted explicitly in a superseding ADR.

Consequences

Positive

  • Sibling apps (landing, admin) share look and feel via ui-kit without sharing file-explorer bundles or OAuth origins.
  • One OAuth origin per customer web app (file-explorer on app.*); operator and marketing ship separate artifacts.
  • Self-hosted and SaaS share the same staged dist/; installer packaging stays simple.
  • Clear place to grow (libs/shell, feature subfolders) without operational sprawl.

Negative

  • All web features ship in one version line until we split builds deliberately.
  • Discipline required: ui-kit must not absorb app routing or gateway calls.

Neutral

  • Desktop installer, landing, and apps/admin are separate apps by origin or form factor—not a violation of the file-explorer modular monolith.