Auth and RPC Layers
This document explains how requests authenticate from clients through the API gateway down to PostgreSQL roles and database schemas.
The Big Picture (How Auth Works)
- Authentication: A user or application logs in via Keycloak and receives a signed JSON Web Token (JWT). The token contains the user's ID (
sub) and an assigned database role claim (role). - Routing: The API gateway routes requests to either PostgREST (direct data queries) or FastAPI (multi-step workflows).
- Database Authorization:
- PostgREST: Validates the JWT signature against Keycloak's public keys. It temporarily switches the database connection to the JWT's
roleand sets session variables (request.jwt.claims). PostgreSQL Row-Level Security (RLS) then automatically ensures callers only see data they are permitted to access. - FastAPI: Validates the token in Python dependencies (
get_current_principalorrequire_enterprise_identity). For trusted server-side writes, it connects directly via PostgreSQL pools (asyncpg) using a constrained service login. When querying PostgREST on behalf of a user, it forwards the user's bearer token.
Client Surfaces & Role Mapping
The platform isolates clients across three symmetric surfaces:
[Public Clients] ──► Keycloak (vibrant) ──► role: authenticated ──► schema: api
[Enterprise Clients] ──► Keycloak (vibrant) ──► role: enterprise_portal_role ──► schema: enterprise_api
[Internal Staff/Ops] ──► Keycloak (internal) ──► role: internal_api_role ──► schema: internal_api
1. Public Surface
- Audience: Mobile app users, residential customers, and contractors.
- Keycloak Realm & Client:
vibrantrealm;vibrant-mobileclient (customerandcontractorroles). - Database Role:
authenticated(unauthenticated requests fall back toanon). - Services:
vibrant_platform_api(port 3400) and public PostgREST (port 3300) exposing theapischema.
2. Enterprise Surface
- Audience: Commercial building managers, site maintenance teams, and enterprise partners.
- Keycloak Realm & Client:
vibrantrealm;vibrant-enterprise-webclient. - Database Role:
enterprise_portal_role(anonymous requests fall back toenterprise_portal_anon). - Services:
vibrant_platform_enterprise_api(port 3800) and enterprise PostgREST (port 3850) exposing theenterprise_apischema.
3. Internal Surface
- Audience: Internal staff, factory operators, and internal automation.
- Keycloak Realm & Client:
internalrealm;vibrant-internal-web(staff browser with MFA/TOTP) andvibrant-internal(service-to-service). - Database Role:
internal_api_role(internal_anonhas zero grants and fails closed). - Internal Realm Roles:
internal_staff,internal_manufacturing,internal_manufacturing_admin,internal_admin. - Services:
vibrant_platform_internal_api(port 3500) and internal PostgREST (port 3600) exposing theinternal_apischema.
Database Roles & Helpers
Helper Functions (auth.*)
SQL views and RLS policies inspect caller identity using helper functions in infrastructure/db/migrations/20260831000100_shared_foundation.sql:
* auth.uid() — Returns the UUID subject (sub) of the authenticated user.
* auth.role() — Returns the current database role claim.
* auth.jwt() — Returns the complete JSON payload of the JWT.
* auth.claim(key) — Retrieves a specific claim from the JWT.
Authenticator Roles vs. Application Roles
authenticator,enterprise_portal_authenticator, andinternal_postgrest_authenticatorare connection roles used by PostgREST instances. They possess no table privileges; they only have permission toSET ROLEto authorized application roles.platform_api_role,enterprise_api_role, andinternal_api_roleare constrained application roles used by the FastAPI services when connecting directly to PostgreSQL via asyncpg.
Rules for Adding Views & RPC Functions
- Expose Contracts, Not Tables: Keep domain tables private. Expose stable views and stored procedures (
FUNCTION) inapi,enterprise_api, orinternal_api. - Explicit Privilege Revocation: Always execute
REVOKE EXECUTE ON FUNCTION ... FROM PUBLIC;before granting to authorized roles (authenticated,enterprise_portal_role, orinternal_api_role). - Security Invoker by Default: Views must declare
WITH (security_invoker = true)so caller RLS policies are evaluated. UseSECURITY DEFINERonly when bypassing RLS is explicitly required, and always pinSET search_path = pg_catalog. - Hoisted Subqueries in Policies: When writing
CREATE POLICY, wrap(SELECT auth.uid())in scalar subqueries so PostgreSQL evaluates the identity once per statement rather than once per row.
Technical Pointers
- Identity Contract:
src/vibrant_platform_common/auth/identity_contract.py - Security & JWT Verification:
src/vibrant_platform_common/core/security.py - Local PostgREST & Keycloak Config:
infrastructure/local/compose.yaml - SQL Contracts & Coding Standards:
docs/database/sql-contracts.md