PrimeBrickPrimeBrick
  • Docs
  • Contact
  • MIT License
  • Documentation
  • MCP Server
  • API Catalog
  • Services
  • Libraries
PrimeBrickPrimeBrick

© 2026 PrimeBrick. MIT License. v3.11.2

github
DAL Library
SDK Library
    Developer Guide
      OverviewGetting StartedAnatomy of a MicroserviceAuthenticationClient Shield & Rotating SecretsHTTP ServerRPC RouterEntity RouterOpenAPI GenerationDocs PackConfig tables & ConfigLoaderDatabase MigrationsTranslationsCurrency UtilitiesNATS™ ClientService RegistrationSSE StandardPresenceExt-JSONRedis cache layerTelemetry & LoggingAPI Reference
Decimal Library
powered by Zudoku
Developer Guide

Authentication

The auth module provides JWT/OIDC verification, API key auth, RBAC, and session context propagation via AsyncLocalStorage. It is framework-agnostic: the same verifyAuth() works with raw Node.js® HTTP, Express, and NATS™ messages through a single HeaderProvider abstraction.

Auth modes

Two operating modes control who validates the token and how the user identity reaches the service:

ModeWho uses itToken verificationPorts needed
STANDALONEBEService validates JWT against IDP via OIDC discoveryUserResolverPort, RoleMappingPort
GATEWAYMicroservicesBE already resolved the user; microservice verifies gateway secret + deserializes headersNone

AuthConfig

Auth configuration is loaded once at startup from the service's config table and cached in memory. The config determines the mode, OIDC settings, gateway secret, and the header names used for serialization.

Code
import { initAuthConfig, loadAuthConfig, getAuthConfig } from "@primebrick/sdk"; // At startup — inject your AuthConfigPort adapter (BE provides one using its DAL) initAuthConfig(myAuthConfigPort); await loadAuthConfig(); // On the hot path — returns cached config, zero DB hits const config = getAuthConfig();

Call invalidateAuthConfig() to force a reload on the next loadAuthConfig().

STANDALONE mode (BE)

The BE validates the JWT, resolves the IDP subject to an internal UUID, and expands roles into permissions using the role_mappings table.

Code
import { verifyHttpRequest, type AuthPorts } from "@primebrick/sdk"; const ports: AuthPorts = { resolveInternalUuid: async (input) => { // look up user_profiles by idp_code / email return userRepo.resolveByAuthProvider(input); }, getRoleMapping: async (role) => { // look up role_mappings table return roleMappingRepo.findByRole(role); }, }; // In your Express middleware or route handler: const user = await verifyHttpRequest(req, config, ports);

GATEWAY-RESOLVED mode (microservices)

Microservices do not validate JWTs or touch the database. The BE serializes the fully resolved AuthUser into headers (HTTP proxy or NATS™), and the microservice verifies the gateway secret and deserializes the user.

Code
import { verifyHttpRequest } from "@primebrick/sdk"; // No ports — GATEWAY-RESOLVED mode const user = await verifyHttpRequest(req, config);

For NATS™ subscribers:

Code
import { verifyNatsMessage } from "@primebrick/sdk"; NatsClient.subscribe("emailsender.send", async (data, msg) => { const user = await verifyNatsMessage(msg, config); // user.permissions, user.isAdmin, user.isSystem are all available });

The BE publisher side uses buildNatsAuthHeaders() to serialize the user into NATS™ headers with the gateway secret:

Code
import { buildNatsAuthHeaders } from "@primebrick/sdk"; const authHeaders = buildNatsAuthHeaders(user, config); await NatsClient.publish("emailsender.send", requestBody, authHeaders);

AuthUser

The AuthUser type is the result of all auth verification — regardless of mode:

FieldTypeDescription
idstringInternal Primebrick UUID (or "system" for system API keys)
idp_codestringOriginal IDP subject (JWT sub) — traceability only
emailstring | nullUser email
namestring | nullDisplay name
rolesstring[]Normalized role names from the IDP
permissionsSet<string>Flattened permissions derived from roles
isAdminbooleanBypasses all permission checks (admin role)
isSystembooleanSystem API key — bypasses RBAC, actor = "system"
idp_orgstring | nullIDP organization
idp_usernamestring | nullIDP username
raw_access_tokenstring | undefinedRaw token (STANDALONE only, for proxy forwarding)

API keys

API keys are machine-to-machine credentials stored in the api_keys table with a SHA-256 hash. The SDK provides verifyApiKey(), hashApiKey(), and generateApiKey().

Code
import { verifyApiKey, generateApiKey, hashApiKey } from "@primebrick/sdk"; // Generate a new key (store the hash, return the plaintext once) const { key, prefix } = generateApiKey(); // key = "pbk_<32 hex chars>" // Verify on incoming request const user = await verifyApiKey(headers, apiKeyPort); // user.isSystem → true if the key has is_system=true

API keys accept two header formats:

  • Authorization: ApiKey <key>
  • Authorization: Bearer <key> (when the key starts with pbk_)

Resolution — who owns api_keys

The api_keys table lives in the BE-owned public schema. Exactly one implementation may read it directly:

PortWhereResolves via
DomainApiKeyPortBE onlypublic.api_keys (direct DB read)
NatsApiKeyPortmicroservicesgoverned auth.apikey.byHash req/reply → BE
CachingApiKeyPorteverywherecache → inner resolver (one of the two above)

CachingApiKeyPort is the single cached implementation: pass a CachePort (or a lazy () => getSdkCachePort()) for the shared Redis key be:api_keys:hash:<sha> (5 min TTL, positive results only), or omit it for the bounded in-memory mode (60s positive / 10s negative / max 1000 entries — the webhook DMZ mode). A microservice must never read public.api_keys directly — sovereignty is enforced by the fact that only DomainApiKeyPort exists in the BE repo.

RBAC

RBAC evaluates whether an authenticated user has the permissions required by an endpoint. The Permission constant defines all known permissions. Three sentinels are handled specially:

  • Permission.PUBLIC — endpoint reachable without authentication
  • Permission.AUTHENTICATED_USER — any authenticated caller passes
  • Permission.AUTHENTICATED_ADMIN — only callers with isAdmin === true pass (admin-only operations, e.g. admin change-password)
Code
import { enforceHttpRbac, Permission } from "@primebrick/sdk"; // In your route handler: enforceHttpRbac(user, [Permission.CUSTOMER_READ_ALL, Permission.CUSTOMER_READ_SINGLE]); // OR semantics by default — user needs ANY of the listed permissions enforceHttpRbac(user, [Permission.CUSTOMER_CREATE_SINGLE], "all"); // AND semantics — user needs ALL listed permissions

Admin users (isAdmin=true) and system API keys (isSystem=true) bypass all permission checks. Wildcard patterns in role mappings (e.g. customer.read.*) are supported via matchesWildcard().

For NATS™ subscribers, use enforceNatsRbac() — same logic, semantic separation.

The non-throwing variant checkRbac() returns { allowed, missing? } instead of throwing.

Access axis — isAccessGranted()

Navigation visibility uses a separate vocabulary inside the same permission set: access.{app}.{module}.{page} grants gate discovery metadata (modules, nav links, routes), not entity APIs. isAccessGranted(user, required) evaluates them:

Code
import { isAccessGranted } from "@primebrick/sdk"; isAccessGranted(user, "access.system.settings"); // page/module gate isAccessGranted(user, undefined); // no declaration → open

Semantics differ from entity RBAC:

  • Hierarchical — a bare grant covers its subtree: access.system.job ⇒ access.system.job.rules; wildcards work at any level (access.system.*, access.*). A page-level grant never climbs to its module ancestor.
  • Deny-first — an invalid required key fails closed.
  • isAdmin implies access.system only — the platform plane. Business roots (access.crm.*, …) require explicit grants even for admins, unlike the blanket RBAC bypass.

Grants live in role_mappings.permissions alongside entity permissions; the two axes never collide because access.* is never an entity scope.

Session context

Session context uses AsyncLocalStorage to propagate the authenticated actor through the async chain without passing it through every method signature. The auth middleware sets the session; DAL code reads it via requireActor().

Code
import { requireActor, runAsSystem, SYSTEM_ACTOR } from "@primebrick/sdk"; // In DAL / repository code: await repo.update(CustomerEntity, uuid, body, requireActor()); // requireActor() returns the UUID or "system" — throws if no session in scope // In seeds, migrations, background jobs: await runAsSystem(() => dal.seedIfEmpty()); // audit columns will record "system"

getSession() returns the full Session object or undefined if no session is in scope. runWithSession() is the low-level primitive — prefer runAsSystem() or the auth middleware unless you are writing infrastructure code.

AuthError

All auth failures throw AuthError with an internal_code field. The HTTP server's error handler reads internal_code and status from the error to produce RFC 7807 responses. Common codes:

CodeMeaning
AUTH_TOKEN_MISSINGNo Bearer token in Authorization header
AUTH_TOKEN_INVALIDJWT verification failed (expired, bad signature, etc.)
AUTH_GATEWAY_SECRET_INVALIDGateway secret header mismatch
AUTH_GATEWAY_HEADERS_MISSINGRequired identity header not present
AUTH_API_KEY_MISSINGNo API key in Authorization header
AUTH_API_KEY_INVALIDAPI key hash not found
AUTH_API_KEY_INACTIVEKey marked inactive
AUTH_API_KEY_EXPIREDKey past expires_at

MFA step-up

auth/mfa-step-up.ts is the consumer-side half of the step-up flow: when a route handler needs an elevated check before a sensitive action, it calls assertMfaStepUp(ctx, spec) — which consumes a single-use action authorization minted by the BE over the auth.mfa.consume NATS subject and throws MfaStepUpRequiredError when none is present (the client then shows the step-up dialog, collects the TOTP, and retries with the x-mfa-action-authorization header). MfaConsumeUnavailableError covers the "BE unreachable" path. In an entity router, wire it as a per-action guards hook — see entity-router.

Next steps

  • NATS Client — publish/subscribe with auth headers
  • Service Registration — lifecycle events over NATS™
  • HTTP Server — how auth errors become RFC 7807 responses
  • API Reference — full auth API listing
Last modified on October 28, 2026
Anatomy of a MicroserviceClient Shield & Rotating Secrets
On this page
  • Auth modes
  • AuthConfig
  • STANDALONE mode (BE)
  • GATEWAY-RESOLVED mode (microservices)
  • AuthUser
  • API keys
    • Resolution — who owns api_keys
  • RBAC
    • Access axis — isAccessGranted()
  • Session context
  • AuthError
  • MFA step-up
  • Next steps
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript