node-postgrest-sidecar

Passthrough a PostgREST per accesso dati del tenant.
data-access
/apiV1rewrite /api/$2auth: nginx

node-postgrest-sidecar

Passthrough al runtime PostgREST: lo spec OpenAPI è generato dinamicamente da PostgREST e non viene snapshottato qui. Rewrite asimmetrico /apiV1//api/.

In sintesi

Thin wrapper Fastify davanti a PostgREST: tutte le chiamate dati del tenant transitano da qui invece di toccare PostgREST direttamente. Il servizio non possiede modello di dominio proprio — il suo lavoro è prendere l'identità autenticata da nginx (header X-User-*), tradurla in un JWT firmato con il ruolo Postgres corretto e iniettarlo come Authorization: Bearer prima di inoltrare la richiesta. PostgREST lato downstream legge il claim role e attiva la Row-Level Security del database. Lo stesso flusso di iniezione viene riusato su tre transport: HTTP proxy (catch-all /api/*), NATS request/reply e modulo @pzeta/orchestrator-node.

Funzionalità principali

  • HTTP passthrough verso PostgREST con rewrite /apiV1/<x>/api/<x> lato ingress e /api// verso l'upstream PostgREST, basato su @fastify/http-proxy (catch-all su /api/*)
  • JWT injection per RLS PostgreSQL: estrae 9 header canonici X-User-* (x-user-id, x-user-email, x-user-roles, x-user-tenant, x-user-groups, x-user-department, ecc.), determina il ruolo PostgREST e firma un JWT HS256 oppure delega la generazione a un IdP OAuth2 via client_credentials
  • Token cache TTL-bound (SecureTokenCache): chiave jwt:<userId>:<roles>:<tenant> per gli autenticati e jwt:anonymous per gli accessi pubblici; cleanup automatico ogni min(JWT_EXPIRATION/2, 5min)
  • Anti-bypass: JwtInjectionService.injectJWT() azzera sempre qualsiasi Authorization in ingresso prima di iniettare il proprio JWT, impedendo che un client possa presentare un token arbitrario a PostgREST
  • Triplo transport, una sola pipeline: lo stesso JwtInjectionService viene chiamato da HTTP, NATS e Orchestrator module — la lista canonica degli header X-User-* è centralizzata in HeaderConstants.ts per evitare drift fra transport
  • Public routes bypass: lista CSV PUBLIC_ROUTES (default /live,/ready,/metrics) servita senza autenticazione verso PUBLIC_SERVICE_URL (default = POSTGREST_URL), con match esatto + wildcard sotto-path
  • Role mapping: X-User-Roles (CSV, primo vince) → fallback X-User-Groups → fallback ANONYMOUS_ROLE; nessun ruolo gestito staticamente nel codice, tutto guidato dal token e dalla configurazione
  • NATS proxy headers-based (opzionale): un solo subject (<NATS_NAMESPACE>.postgrest.request), routing letto dagli header X-PostgREST-Method / X-PostgREST-Path, validazione mittente HMAC via NATS_INTERNAL_SECRET
  • Integrazione Orchestrator (opzionale): si registra come modulo jwt-sidecar con task postgrest-proxy su @pzeta/orchestrator-node, esponendo il proxy come step di workflow

Architettura

Stack: Fastify v5 · @fastify/http-proxy · Inversify (DI con constructor injection) · jsonwebtoken (HS256) · pg driver · NATS · @pzeta/fastify-utils (securityPlugin per CORS + rate limit) · @pzeta/orchestrator-node · @pzeta/log · Zod per validazione body PostgREST sui codici di errore. Build production via bundle esbuild (scripts/build-prod.mjs).

Layout DDD (src/):

LayerContenuto
application/services/AuthService (extract / determineRole / OAuth2 flow / JWT sign / cache), JwtInjectionService (punto unico di iniezione condiviso dai tre transport), SecureTokenCache
application/constants/HeaderConstants.ts — lista canonica USER_HEADER_NAMES (9 header, lowercase + variante PascalCase per NATS)
application/dtos/ · application/ports/Interfacce UserInfo, JwtClaims, OAuth2TokenResponse, ProxyConfig, contratto IConfig
infrastructure/Server.ts (wiring Fastify), config/Config.ts (lettura env), di/Container.ts (composition root Inversify, binding condizionale NATSPostgRESTProxy se natsProxy.enabled), http/PostgRESTHttpClient.ts, nats/* (NatsPostgrestProxy, HeadersParser, NATSIdentityValidator HMAC, NATSMessageHandler, NATSMetricsCollector), orchestrator/OrchestratorAdapter.ts
presentation/http/plugins/ProxyPlugin.ts (preHandler JWT + catch-all /api/*), routes/PublicRoutes.ts, routes/DebugRoutes.ts

Pattern adottati: Sidecar (un solo container davanti a PostgREST, deploy 1:1 con il backend dati), thin wrapper, Dependency Injection (Inversify), single-source-of-truth per gli header utente, anti-bypass su Authorization.

Ordine di registrazione dei plugin in Server.initialize() — l'ordine è significativo perché @fastify/http-proxy registra un catch-all su /api/*:

  1. securityPlugin di @pzeta/fastify-utils (CORS opzionale + rate limit)
  2. Handler inline /live e /ready (probe interne sempre locali, evitano self-loop quando PUBLIC_SERVICE_URL punta al sidecar)
  3. DebugRoutes (solo se ENABLE_DEBUG_ROUTES=true e NODE_ENV !== production — il flag è forzato a false in produzione)
  4. registerProxyPlugindeve essere ultimo: registra le PUBLIC_ROUTES esplicitamente e poi il catch-all /api/* verso PostgREST

Override rispetto allo stack standard: questo servizio non usa authPlugin di @pzeta/fastify-utils perché è lui stesso il layer di autenticazione (genera/inietta JWT a monte di PostgREST). Per lint e format usa Biome invece di ESLint+Prettier.

Casi d'uso

  • Frontend Vue → dati del tenant: il frontend chiama https://ditta.pzeta.it/apiV1/<tabella>?<filtri-postgrest> invece di POSTGREST_URL/<tabella> perché (a) PostgREST non è esposto pubblicamente, (b) richiede un JWT firmato che il browser non possiede, (c) il sidecar traduce l'identità autenticata da nginx in quel JWT senza che il client veda mai un secret
  • Microservizio interno → query PostgREST autenticata: node-orchestrator, node-print, node-renderer e node-notification chiamano il sidecar via DNS interno (node-postgrest-sidecar-ditta.ditta.svc.cluster.local) propagando gli stessi X-User-* ricevuti dal proprio chiamante a monte. Il sidecar emette il JWT con il ruolo dell'utente originale e PostgREST applica la RLS coerente con quella sessione
  • Step di workflow orchestrato: node-orchestrator richiama il task postgrest-proxy esposto dal modulo @pzeta/orchestrator-node registrato dal sidecar quando ORCHESTRATOR_URL è valorizzato — utile per pipeline data-driven che alternano query PostgREST a step custom
  • Trasporto NATS per servizi event-driven: quando natsProxy.enabled = true, un microservizio interno pubblica su <NATS_NAMESPACE>.postgrest.request con header X-PostgREST-Method / X-PostgREST-Path e riceve in reply lo stesso payload che otterrebbe via HTTP; utile per esecuzioni batch con back-pressure NATS invece di connection pool HTTP
  • Accesso anonimo controllato: richieste senza X-User-* ricevono il ruolo ANONYMOUS_ROLE configurato (in modalità autonomous via JWT firmato, in modalità OAuth2 senza JWT — PostgREST applica db-anon-role direttamente). Permette di esporre viste pubbliche del DB senza un secondo deploy del sidecar
  • Health esposti pubblicamente: /live, /ready e /metrics (e qualsiasi entry in PUBLIC_ROUTES) bypassano l'iniezione JWT — utili per probe Kubernetes e scraping Prometheus

Identità & esposizione

CampoValore
Categoriadata-access
Versione cluster1.0.22
Imagegitea.pzetatouch.it/pzeta_touch/node-postgrest-sidecar:1.0.22
URL pubblicohttps://ditta.pzeta.it/apiV1
Path regex ingress`/apiV1(/
Rewrite a backend/api/$2
DNS internonode-postgrest-sidecar-ditta.ditta.svc.cluster.local:3000
Auth nginxauth_requestnode-user-auth
Repositorynode-postgrest-sidecar

Endpoint operazionali

Endpoint convenzionali esposti da tutti i microservizi PZeta basati su @pzeta/fastify-utils:

Path pubblicoScopo
https://ditta.pzeta.it/apiV1/healthliveness probe
https://ditta.pzeta.it/apiV1/readyreadiness probe
https://ditta.pzeta.it/apiV1/metricsmetriche Prometheus

Note specifiche del sidecar: /live e /ready sono serviti direttamente dal processo Fastify (non proxati) per evitare self-loop quando PUBLIC_SERVICE_URL coincide con POSTGREST_URL puntando al sidecar stesso. Lo spec OpenAPI completo dell'API dati è renderizzato dinamicamente da PostgREST a GET /apiV1/ — non è snapshottato in questa pagina perché varia in funzione dello schema database del tenant.

Configurazione

Variabili d'ambiente che un integratore deve conoscere (per la lista completa vedi .env.example del repo):

VariabileRuolo
POSTGREST_URLURL upstream PostgREST (obbligatorio) — target del proxy /api/*
PUBLIC_SERVICE_URLTarget delle PUBLIC_ROUTES; default = POSTGREST_URL. Distinguilo se le rotte pubbliche vivono altrove
PUBLIC_ROUTESCSV di path che bypassano l'iniezione JWT (default /live,/ready,/metrics). Match esatto + wildcard sotto-path
JWT_GENERATION_MODEautonomous (firma locale HS256, richiede JWT_SECRET ≥ 32 char) oppure centralized (delega a AUTH_SERVICE_URL)
JWT_SECRET / JWT_EXPIRATION / JWT_ISSUER / JWT_AUDIENCEParametri di firma in modalità autonomous; TTL del token e dell'entry di cache
OAUTH2_ENABLEDSe true, sostituisce la firma locale con client_credentials verso OAUTH2_TOKEN_ENDPOINT (passando X-User-* come user_headers). In OAuth2 gli anonimi non ricevono JWT — PostgREST applica db-anon-role
OAUTH2_TOKEN_ENDPOINT / OAUTH2_CLIENT_ID / OAUTH2_CLIENT_SECRET / OAUTH2_SCOPEConfigurazione IdP esterno
ANONYMOUS_ROLERuolo PostgREST per richieste senza identità (deve esistere come ruolo Postgres nel DB del tenant)
TOKEN_CACHE_CAPACITYNumero massimo di entry nella cache JWT (1 entry = userId+roles+tenant)
CORS_ALLOWED_ORIGINSCSV di origini abilitate; stringa vuota = CORS disabilitato (non aperto). Imposta esplicitamente per accesso browser cross-origin
RATE_LIMIT_MAX / RATE_LIMIT_WINDOWThrottle in entrata via securityPlugin
NATS_ENABLED / NATS_URL / NATS_NAMESPACEAbilita il proxy NATS headers-based; il subject finale è <NATS_NAMESPACE>.postgrest.request (namespace vuoto = nessun prefisso)
NATS_INTERNAL_SECRETHMAC sender validation per il proxy NATS (≥ 32 char). Obbligatorio quando NATS_ENABLED=true
ORCHESTRATOR_URLSe valorizzato, registra il sidecar come modulo @pzeta/orchestrator-node (moduleId: jwt-sidecar, task postgrest-proxy)
ENABLE_DEBUG_ROUTESAttiva endpoint diagnostici (/debug/user-info); forzato a false in NODE_ENV=production anche se settato a true

Quality gate consigliato in CI: npm run validate (typecheck + lint:check + format:check + build + test). Build production via npm run build:prod (bundle esbuild, non tsc).

Note eventing NATS

Il servizio non pubblica eventi di dominio: PostgREST è un layer di accesso dati sincrono e il sidecar è un thin wrapper che si limita a brokerare richieste/risposte. Non emette events.* né si sottoscrive a subject di altri domini.

L'unica integrazione NATS è il proxy headers-based (NATSPostgRESTProxy), opzionale e attivato da NATS_ENABLED=true. È un pattern request/reply puro:

  • Un solo subject configurabile (default postgrest.request, prefissato da NATS_NAMESPACE se presente → <ns>.postgrest.request)
  • Routing letto dagli header NATS X-PostgREST-Method (GET | POST | PUT | PATCH | DELETE) e X-PostgREST-Path
  • Validazione mittente via HMAC SHA-256 con NATS_INTERNAL_SECRET (NATSIdentityValidator) — previene proxy non autorizzati sul bus interno
  • Stesso JwtInjectionService usato dal proxy HTTP: gli X-User-* passati negli header NATS vengono trasformati in Authorization: Bearer verso PostgREST
  • Queue group per load-balancing fra repliche del sidecar
  • Risposta sincrona via reply subject NATS con body PostgREST originale + metriche (NATSMetricsCollector)

Per questo motivo la tabella subject auto-generata è (e resta) vuota o limitata al solo subject di proxy: non c'è un dominio eventato dietro questo servizio.

Dipendenze e dipendenti

Dipende da (servizi che questo servizio chiama):

Consumato da (chi chiama questo servizio):

Infrastruttura (PostgreSQL, NATS, Redis, MinIO) non è elencata qui — vedi sezione Architettura del singolo servizio.

Loading OpenAPI…
Loading NATS contracts…