Skip to content

0018. Worker daemon su processes: polling mediato da noeva-server-api, non più Supabase diretto

  • Stato: Accettata
  • Data: 2026-07-15
  • Decisori: Team BI/ETL

Contesto

ADR-0017 aveva scelto di far parlare etl worker direttamente con Supabase tramite @my-ginkgo/noeva-shared (ProcessManager.claimProcess per il claim CAS, updateProcess per lo stato/ progress/report per nodo), bypassando la RPC condivisa claim_next_process per poter filtrare anche per workspace_id.

Questo però comporta che ogni istanza di etl worker porti con sé credenziali Supabase dirette (SUPABASE_URL, SUPABASE_SERVICE_KEY — una service role key, che bypassa RLS). A differenza di noeva-server-api, che è un backend condiviso unico per tutti i workspace, etl worker è deployato come istanza indipendente per workspace, potenzialmente su infrastruttura controllata dal cliente. Qualunque credenziale data al worker è quindi una credenziale esposta sull'ambiente del cliente — per una service role key Supabase questo è un rischio di sicurezza inaccettabile: sparpaglia le chiavi più privilegiate del nostro Supabase fuori dal nostro perimetro.

Decisione

Il polling/claim/report di etl worker verso processes passa esclusivamente per noeva-server-api, via HTTP autenticato (stesso pattern header Authorization: Bearer <NOEVA_API_KEY> + x-noeva-workspace già usato dal connettore workspace files):

  • noeva-server-api espone POST /api/processes/etl/claim (claim CAS + filtro worker_type/workspace_id, fatto server-side, dove le credenziali Supabase vivono legittimamente) e PATCH /api/processes/etl/:processId/progress (scrittura di stato/progress/output/error).
  • etl worker (src/worker/processes-api-client.ts, ProcessesApiClient) non ha più alcuna dipendenza né da @my-ginkgo/noeva-shared né da @supabase/supabase-js — entrambe rimosse da package.json. Il worker non detiene più nessuna credenziale Supabase, solo la sua API key NOEVA_API_KEY (già scoped al workspace).
  • src/worker/claim.ts (il wrapper di claim lato worker) è stato rimosso anziché riscritto: con CAS e filtro spostati server-side, un passthrough di una riga attorno a ProcessesApiClient.claimNextEtlProcess sarebbe un'astrazione senza scopo.
  • Osservabilità: il claim ora porta anche un workerId (claimed_by, default hostname:pid), assente nel design precedente.

Alternative considerate

  • Mantenere @my-ginkgo/noeva-shared ma con una service-role key ristretta/ scoped — scartata: Supabase non offre service role key con permessi ristretti a livello di riga per questo caso d'uso senza reimplementare RLS custom, e comunque non risolverebbe il problema di fondo (chiavi Supabase su infrastruttura del cliente).
  • Un livello di proxy separato solo per Supabase (non noeva-server-api) — scartata: noeva-server-api già espone endpoint autenticati per workspace, aggiungere un secondo servizio solo per questo caso d'uso sarebbe una duplicazione di infrastruttura senza benefici.

Conseguenze

  • etl worker non ha più @my-ginkgo/noeva-shared/@supabase/supabase-js tra le dipendenze, né alcuna variabile SUPABASE_* in configurazione — solo NOEVA_API_BASE_URL/NOEVA_API_KEY/NOEVA_WORKSPACE_ID.
  • Il claim CAS e il filtro per worker_type/workspace_id sono ora responsabilità di noeva-server-api (etl-worker.controller.ts), coperti dai suoi test di integrazione — il worker si limita a consumare il contratto HTTP.
  • Il test di integrazione del worker (tests/integration/etl-worker.integration.test.ts) non mocka più Supabase: simula il contratto HTTP di noeva-server-api con un fetchImpl in-memory, verificando l'uso lato worker del client, non la logica di claim/isolamento (già coperta lato server).
  • Deploy su infrastruttura cliente: l'unico segreto da distribuire è NOEVA_API_KEY, una API key applicativa scoped al workspace — non più una service role key Supabase.

Noeva è un marchio registrato di 4D S.R.L.