Internal dashboard apps
apps/internal-ops is the one shared app for internal-only, web-first dashboards. New
dashboards are new tabs/screens in this app, not new Expo apps.
Adding a new tab
- Create a screen under
apps/internal-ops/app/screens/. - Register it in
apps/internal-ops/app/navigation/tabs.tsx:tsx { key: "devices", title: "Devices", component: DevicesScreen } - Restrict it with a
rolesarray (user needs at least one). Omitrolesto allow any signed-in internal user:tsx { key: "devices", title: "Devices", roles: [INTERNAL_ROLES.ADMIN], component: DevicesScreen }
Gating part of a view
Use RoleGate to hide a section instead of a whole tab:
<RoleGate roles={[INTERNAL_ROLES.ADMIN]}>
<DangerousBulkActionButton />
</RoleGate>
Adding a new role
Add it to INTERNAL_ROLES in apps/internal-ops/app/auth/roles.ts. It only matters once it
also exists as a Keycloak realm role and is assigned to someone.
Access control
The backend runs a separate internal API and PostgREST instance (internal-postgrest, port
3600, schema internal_api). Its anonymous role has no grants. Requests need a JWT with
role: internal_api_role. See vibrant-platform/docs/architecture/internal-api.md.
This app signs in through the vibrant-internal-web Keycloak client, never
vibrant-mobile/postgrest, and never through AccountsApi (see below).
It uses Authorization Code with S256 PKCE through Expo AuthSession. Keycloak
owns the password and MFA screens.
Two gates exist today, and they are not equally strong:
- App-level, real today:
AuthGaterequires theinternal_staffrealm role to get past sign-in.TabNavigator/RoleGatefilter tabs and sections by role. - Data-level, weak, local-only:
vibrant-internal-web'sroleclaim is hardcoded tointernal_api_rolefor anyone who can sign in with that client, regardless of realm role. Production needs to gate this on a real realm role. That wiring belongs in../vibrant-infra.
Be conservative about who gets a vibrant-internal-web account until that gap is closed.
Granting access means creating or assigning a user in the internal realm. Staff users
inherit internal_staff by default in that realm; assign any elevated roles such as
internal_admin explicitly. There is no self-serve signup.
Authentication configuration
apps/internal-ops/app/config/keycloak.ts is the source of truth for the
Keycloak URL, realm, client ID, and optional deployed callback. The app
derives local callbacks and uses KEYCLOAK_REDIRECT_URI only for the exact
HTTPS callback registered for a deployed web app.
apps/internal-ops/app/services/api/APIFactory.ts passes accountsApiUrl: ""
to disable the customer AccountsApi login path. Its shared session storage is
scoped to internal ops.
MFA policy
The vibrant-internal-web client has standard flow and S256 PKCE enabled and
direct password grants disabled. The running local realm binds its
Internal staff browser flow, which contains an Internal staff TOTP
conditional branch: users with internal_staff must complete TOTP after their
password, while users without that role follow the normal browser flow.
On a staff member's first login, Keycloak prompts for authenticator setup. Scan the QR code with a TOTP application (Google Authenticator, 1Password, Microsoft Authenticator, Authy, etc.) and complete the generated code prompt. Keycloak's TOTP and recovery-code setup actions are enabled. Recovery codes can be generated in the Account Console under Account Security → Signing in, but recovery-code sign-in has not yet been added to the required staff branch; reset a lost authenticator through an administrator until that fallback is explicitly enabled and validated.
The browser-flow policy is live configuration in the existing local Keycloak database; Keycloak does not re-import it into an already-created realm. Keep the local Keycloak data volume when restarting. If the realm is recreated, reapply this flow before testing internal ops.
Production must also replace the local hardcoded PostgREST role mapper with a role-derived mapper.
Where the data comes from
getApiService() in APIFactory.ts points at the internal PostgREST (:3600), not the
public one. Extend internal_api_role's grants in vibrant-platform migrations as dashboards
need more data.
Running it locally
npm run web:internal-ops
Copy apps/internal-ops/.env.example to .env. Local defaults already match: the
vibrant-internal-web client, internal PostgREST on :3600, and
[email protected] / VibrantDemo!1.
Deployment
Not established yet. expo export --platform web produces static assets.