Skip to main content

Standalone UI ("Fidelity Port") with Multiple Backends

Framework M's UI architecture strictly decouples the Context Port (business logic, entity hierarchy, and permissions) from the Fidelity Port (the visual rendering layer, design system, and user interaction).

Because @framework-m/ui and @framework-m/desk are standard, composable React/Tamagui packages, the frontend can run as a standalone "Fidelity Port" completely decoupled from a single monolithic backend. It can connect to standard Framework M instances, bespoke microservices, or external ERPs (like Frappe) unified behind an authenticating gateway.

Standalone UI Architecture

This guide details the 4 Backend Integration Flavors available when running a standalone UI.


1. Backend Flavor 1: Standard Framework M Backend​

Connecting the standalone UI directly to a Framework M backend supports two authentication mechanisms based on client device capabilities.

This is the standard pattern used by the default Web Desk. It follows the Backend-For-Frontend (BFF) pattern:

  • Login endpoint (POST /api/v1/auth/login) issues an httpOnly, Secure, SameSite=Lax session cookie (Set-Cookie: session=...).
  • The browser automatically transmits the cookie on subsequent API calls (credentials: "include").
  • Security Profile: Highest security for web browsers. JavaScript cannot inspect or exfiltrate the session token, eliminating credential leakage from XSS vectors.
// Default browser setup in @framework-m/desk
import { AuthProvider } from "@framework-m/desk";

export const authProvider = new AuthProvider({
apiUrl: "https://api.example.com/api/v1",
// Relies on native browser cookie jar
});

1B. Bearer Token Model (Mobile / Non-Browser / IoT Devices)​

When building a Fidelity Port for native mobile apps (React Native / Expo), embedded industrial touchscreens, or specialized IoT / kiosk devices, HTTP cookies are often unavailable, awkward to synchronize, or disabled:

  • Login returns an explicit JWT access token: { "access_token": "eyJhbGci...", "token_type": "bearer" }.
  • The client passes the token explicitly in the Authorization: Bearer <token> header.
  • Security Trade-off: Storing Bearer tokens in browser localStorage or sessionStorage carries XSS exfiltration risks. However, on native mobile and specialized IoT hardware, storing tokens in OS-level hardware-backed vaults (iOS Keychain, Android Keystore, or encrypted device storage) provides a safe, standard deployment model.

Backend Configuration (framework_config.toml):​

Ensure the bearer strategy is enabled in your authentication chain:

[auth]
# Priority order: check Bearer header first, fallback to session cookie
strategies = ["bearer", "session"]
jwt_secret = "${JWT_SECRET}"
jwt_algorithm = "HS256"

Frontend Client Configuration:​

import { createForwardAuthProvider } from "@framework-m/desk";
import * as SecureStore from "expo-secure-store"; // Or native keychain

export const mobileAuthProvider = createForwardAuthProvider({
whoamiEndpoint: "https://api.example.com/api/v1/auth/me",
tokenHeaderName: "Authorization",
getToken: async () => {
const token = await SecureStore.getItemAsync("access_token");
return token ? `Bearer ${token}` : null;
},
loginUrl: "/login",
logoutUrl: "/logout",
});

In enterprise federated environments, authentication is offloaded to a gateway like Vouch Proxy (or Traefik ForwardAuth, Nginx auth_request, OAuth2-Proxy).

  1. The user logs in via Vouch Proxy against an upstream IdP (Keycloak, Okta, Google).
  2. Vouch sets a domain cookie (e.g. VouchCookie).
  3. The reverse proxy validates the cookie and injects identity headers before forwarding to Framework M:
    • X-Vouch-User: user@example.com
    • X-Vouch-IdP-Claims-Roles: Manager, Auditor

Backend Configuration (framework_config.toml):​

Configure Framework M to trust reverse-proxy headers via HeaderAuthAdapter:

[auth]
strategies = ["header"]

[auth.header]
header_map = { user_id = "x-vouch-user", email = "x-vouch-user", roles = "x-vouch-idp-claims-roles" }
trusted_proxies = ["10.0.0.0/8", "172.16.0.0/12"]

Frontend Configuration:​

import { createForwardAuthProvider } from "@framework-m/desk";

export const authProvider = createForwardAuthProvider({
whoamiEndpoint: "/api/v1/auth/me",
loginUrl: "https://vouch.example.com/login?url=https://app.example.com",
logoutUrl: "https://vouch.example.com/logout?url=https://app.example.com",
});

A standalone Framework M UI can connect to arbitrary custom backend services (written in Go, Python FastAPI/Litestar, or Node.js) sitting behind Vouch Proxy.

  • The gateway handles authentication and injects user claims into downstream requests.
  • The bespoke service exposes a lightweight session endpoint (e.g. GET /api/whoami).
  • Business data is queried via standard Refine data providers.

Frontend Configuration:​

import { createForwardAuthProvider } from "@framework-m/desk";
import type { DataProvider } from "@refinedev/core";

// 1. Auth Provider bound to the bespoke service's session endpoint
export const authProvider = createForwardAuthProvider({
whoamiEndpoint: "/api/whoami",
loginUrl: "https://vouch.example.com/login",
logoutUrl: "https://vouch.example.com/logout",
});

// 2. Custom DataProvider querying the bespoke API
export const customDataProvider: DataProvider = {
getList: async ({ resource, pagination, filters }) => {
const response = await fetch(`/api/${resource}?page=${pagination?.current || 1}`, {
credentials: "include", // Transmits VouchCookie
});
const data = await response.json();
return { data: data.items, total: data.total };
},
getOne: async ({ resource, id }) => {
const response = await fetch(`/api/${resource}/${id}`, { credentials: "include" });
return { data: await response.json() };
},
// ... implement create, update, delete as needed
};

You can use @framework-m/ui or @framework-m/desk as a modern, high-density frontend in front of an existing Frappe ERP backend. Depending on whether your Fidelity Port runs in a web browser or on a native mobile device, you have two authentication integration paths:

When users access the standalone UI via a desktop or mobile web browser behind a reverse proxy gateway (Vouch Proxy / Traefik):

  1. Install castlecraft/vouch_frappe_auth on your Frappe bench:
    bench get-app https://github.com/castlecraft/vouch_frappe_auth
    bench --site your-site.local install-app vouch_frappe_auth
  2. The app registers an auth_hook in Frappe (hooks.py):
    • When an HTTP request reaches Frappe, the hook inspects HTTP_X_VOUCH_USER.
    • It matches the email or username with an existing User record in Frappe and initializes the authenticated session automatically without requiring Frappe's native login screen.

4B. Mobile & Native Clients: Upstream IdP Bearer Tokens via castlecraft/cfe​

On native mobile apps (React Native / Expo) or specialized field devices, managing reverse-proxy cookie handshakes is often awkward. Mobile clients authenticate directly with an upstream Identity Provider (Keycloak, Auth0, Okta, Authentik) using standard OAuth2/OIDC PKCE flows and obtain a JWT access token.

To allow Frappe to accept and validate these upstream IdP Bearer tokens directly:

  1. Install and configure castlecraft/cfe on the Frappe bench.
  2. cfe enables Frappe to validate incoming Authorization: Bearer <jwt_token> against the configured upstream IdP public keys / JWKS.
  3. Frappe resolves the authenticated user context per request without needing browser session cookies.

Frontend Setup (@framework-m/desk & @framework-m/desk/frappe):​

import { createForwardAuthProvider } from "@framework-m/desk";
import { frappeDataProvider, createFrappeDataProvider } from "@framework-m/desk/frappe";

// Option A: Browser Web using Vouch session cookie
export const frappeWebAuthProvider = createForwardAuthProvider({
whoamiEndpoint: "/api/method/frappe.auth.get_logged_user",
loginUrl: "https://vouch.example.com/login",
logoutUrl: "https://vouch.example.com/logout",
});

// Option B: Mobile App using upstream IdP Bearer token (via castlecraft/cfe)
import * as SecureStore from "expo-secure-store"; // Or native OS keychain

export const frappeMobileAuthProvider = createForwardAuthProvider({
whoamiEndpoint: "https://erp.example.com/api/method/frappe.auth.get_logged_user",
tokenHeaderName: "Authorization",
getToken: async () => {
const token = await SecureStore.getItemAsync("idp_access_token");
return token ? `Bearer ${token}` : null;
},
loginUrl: "/login",
logoutUrl: "/logout",
});

// Built-in Frappe Data Provider from @framework-m/desk/frappe
export const dataProvider = frappeDataProvider;

// Or configure with a custom API URL:
export const customFrappeProvider = createFrappeDataProvider({
apiUrl: "https://erp.example.com",
});
Built-in Frappe Subsystem in Desk

The @framework-m/desk/frappe subpath is a self-contained module providing:

  • frappeDataProvider: Out-of-the-box Refine integration with Frappe filter operator mapping (eq, ne, contains, in), pagination (limit_start/limit_page_length), sorting, and custom() RPC calls.
  • useFrappeDocType: Hook for dynamic client-side Frappe DocType schema discovery (/api/method/frappe.desk.form.load.getdoctype).
  • FrappeListView & FrappeDocTypeView: Pre-built components that render dynamic Frappe forms and lists with Tamagui styles.

5. Architectural Comparison Matrix​

Integration FlavorAuthentication MechanismTarget EnvironmentKey Security Advantage
1A: Framework M (Session)httpOnly, Secure cookieDesktop & Mobile Web BrowsersImmune to browser XSS token exfiltration (BFF model).
1B: Framework M (Bearer)Authorization: Bearer <jwt>Native Mobile (Expo), IoT, Hardware KiosksFlexible on headless hardware; stored in OS Keychains.
2: Framework M (Vouch)Domain cookie (VouchCookie) + X-Vouch-* headersCorporate SSO / Federated GatewaysCentralized OIDC/SAML at gateway; zero backend auth code.
3: Bespoke API (Vouch)Domain cookie + Header claimsCustom Go/Python/Node microservicesDecouples business logic; unifies telemetry and data.
4A: Frappe Backend (Vouch)vouch_frappe_auth hookWeb Browser on Frappe / ERPNextTransparent SSO at reverse proxy; zero backend login UI.
4B: Frappe Backend (Bearer)castlecraft/cfe (Upstream IdP Bearer)Native Mobile Apps (React Native / Expo)Direct IdP JWT validation; eliminates cookie friction on mobile.