substrat.cat
Integracions

Supabase i n8n: un backend en una tarda i un bus per a tot el stack

Postgres amb API automàtica d'una banda, orquestració de l'altra. Com muntar-ho, com ordenar-hi les dades i com convertir n8n en l'ESB de la teva empresa.

·12 min de lectura

Hi ha una combinació que resol una franja enorme de problemes de sistemes petits i mitjans, i que costa poques hores muntar: Supabase per a l’estat, n8n per al moviment.

Supabase és un Postgres gestionat amb una API REST generada automàticament a sobre de l’esquema, autenticació, emmagatzematge de fitxers i funcions. n8n és un orquestrador de fluxos que connecta serveis, transforma dades i s’executa per horari o per esdeveniment.

Per separat, cadascun és útil. Junts, cobreixen el que abans requeria un backend, una cua, un planificador i un ETL.

Per què encaixen

La divisió és neta i val la pena tenir-la clara des del principi:

Supabase n8n
Guarda l’estat: què és veritat ara res permanent
Fa consultes, restriccions, permisos moviment, transformació, decisions
S’executa quan algú pregunta per horari o per esdeveniment
Si peta perds el servei perds una execució, que es reintenta

La regla que se’n deriva: la veritat viu a Postgres, la lògica de moviment viu a n8n, i no es barregen. Quan es barregen —lògica de negoci dins d’un flux, o orquestració dins d’un trigger— tot es torna impossible de depurar.

Un backend en una tarda

Aquesta és la part que sorprèn qui no ho ha provat. Crees una taula:

create table public.encarrecs (
  id           uuid primary key default gen_random_uuid(),
  client_id    uuid not null references public.clients(id),
  referencia   text not null unique,
  estat        text not null default 'nou'
               check (estat in ('nou','en_curs','fet','cancellat')),
  import       numeric(12,2) not null check (import >= 0),
  creat_el     timestamptz not null default now(),
  actualitzat_el timestamptz not null default now()
);

create index on public.encarrecs (client_id, estat);
create index on public.encarrecs (creat_el desc);

I ja tens API. Sense escriure ni una línia de servidor:

# Llistar amb filtre, ordenació i límit
curl "$URL/rest/v1/encarrecs?estat=eq.nou&order=creat_el.desc&limit=50" \
  -H "apikey: $ANON" -H "Authorization: Bearer $TOKEN"

# Amb dades de la taula relacionada, en una sola crida
curl "$URL/rest/v1/encarrecs?select=referencia,import,clients(nom,nif)&estat=eq.nou" \
  -H "apikey: $ANON" -H "Authorization: Bearer $TOKEN"

Els filtres són eq, neq, gt, gte, lt, lte, like, ilike, in, is i combinacions amb or=(...). El select amb parèntesis segueix les claus foranes i t’estalvia el JOIN a mà.

Fixa’t en on ha anat a parar la validació: el check de l’estat i el check (import >= 0) són a la base de dades, no a l’aplicació. Això vol dir que no hi ha cap camí —ni l’API, ni n8n, ni algú amb un client de SQL— per on hi entri una dada dolenta. És la diferència entre validar i garantir.

La part que la gent es descuida: RLS

Per defecte, una taula nova amb l’API pública oberta la pot llegir qualsevol amb la clau anon, que viu al navegador i per tant és pública. La seguretat no la dona amagar la clau; la dona el Row Level Security.

alter table public.encarrecs enable row level security;

-- Cada usuari només veu els encàrrecs dels seus clients
create policy "llegir els propis"
  on public.encarrecs for select
  using (
    exists (
      select 1 from public.membres m
      where m.client_id = encarrecs.client_id
        and m.user_id = auth.uid()
    )
  );

-- Escriure, només a través del servei
create policy "res d'escriptura pública"
  on public.encarrecs for insert with check (false);

Dues claus, i s’han de distingir bé:

  • anon (i el token d’un usuari identificat): passa per RLS. És la que va al navegador.
  • service_role: se salta RLS del tot. Va només a n8n, a les funcions del servidor i a res més. Si aquesta clau acaba en un client, la base de dades és oberta.

Una taula sense RLS activat és una taula pública. Ho comprova sempre abans de posar res real a dins.

n8n com a bus: el patró ESB

Aquí és on la cosa deixa de ser «una base de dades i uns scripts» i es converteix en arquitectura.

Un Enterprise Service Bus és la idea que els sistemes no es parlen entre ells directament, sinó que parlen tots amb un bus que tradueix, encamina i registra. Amb sis sistemes, les connexions directes possibles són quinze; amb un bus, són sis. Aquesta és tota la matemàtica del patró.

n8n fa de bus si el muntes amb tres capes:

   CRM ─┐                                    ┌─→ Facturació
Botiga ─┤   [adaptador d'entrada]            │
Correu ─┤          ↓                         ├─→ Comptabilitat
  Excel─┘   model canònic a Supabase  ──────→│
                   ↓                          └─→ Avisos
            [encaminament]

1. Adaptadors d’entrada. Un flux per font. La seva única feina és llegir del sistema d’origen i escriure al model canònic. No decideix res, no avisa ningú, no calcula. Si demà canvies de CRM, canvies un adaptador i res més.

2. El model canònic. Les taules de Supabase amb els noms i els tipus que fa servir la teva empresa, no els que fa servir cap proveïdor. Aquesta traducció és el valor real del bus: Acme_CustomerRef i id_cliente es converteixen tots dos en client_id.

3. Adaptadors de sortida i encaminament. Fluxos que llegeixen del model canònic i actuen. A n8n, el node Execute Workflow és el que fa que això sigui mantenible: cada adaptador és un subflux amb una entrada i una sortida definides, i el flux principal només encamina.

Sense subfluxos acabes amb el «llenç espagueti» de 60 nodes que ningú vol tocar. Amb subfluxos tens dotze fluxos de vuit nodes, cadascun provable per separat.

Fer el bus fiable

Un bus que perd missatges és pitjor que no tenir-ne. Tres peces:

La taula de moviments, que és el registre del bus:

create table public.bus_events (
  id            bigserial primary key,
  origen        text not null,        -- 'crm', 'botiga', 'excel'
  tipus         text not null,        -- 'encarrec.nou'
  clau_externa  text not null,        -- l'id al sistema d'origen
  carrega       jsonb not null,
  estat         text not null default 'pendent',
  intents       int  not null default 0,
  darrer_error  text,
  creat_el      timestamptz not null default now(),
  unique (origen, tipus, clau_externa)   -- ← la idempotència
);

Aquell unique és la peça central. Si el mateix esdeveniment arriba dues vegades —i arribarà—, la segona inserció falla i el flux continua sense duplicar res. La idempotència és una restricció de base de dades, no una comprovació dins del flux. Les comprovacions dins del flux tenen finestres de carrera; una restricció no.

Els reintents. Cada node d’n8n té Retry On Fail amb nombre d’intents i espera. Per al que no s’ha de perdre, activa’l. I al flux sencer, assigna-li un Error Workflow a la configuració: quan un flux peta, se n’executa un altre que apunta l’error a bus_events i t’avisa. Sense això, un flux que falla de matinada falla en silenci.

La reconciliació. Un flux nocturn que compti els dos costats i et digui les diferències. És la peça que gairebé mai es fa i l’única que garanteix que els sistemes acaben quadrant.

Ordenar dades: el patró de dues capes

Aquest és el cas d’ús on la parella brilla més, i el patró és sempre el mateix.

Capa d’aterratge. Una taula on hi cap tot tal com arriba, sense validar:

create table public.landing_clients (
  id         bigserial primary key,
  origen     text not null,
  rebut_el   timestamptz not null default now(),
  dades      jsonb not null
);

n8n hi bolca el CSV, l’Excel o la resposta de l’API sense tocar res. Si demà descobreixes que la transformació estava malament, tornes a processar des d’aquí en comptes de demanar els fitxers una altra vegada.

Capa canònica. Les taules netes, amb tipus i restriccions. La transformació es fa amb SQL, que és molt més fàcil de revisar que una cadena de nodes:

insert into public.clients (nif, nom, email, provincia)
select
  upper(regexp_replace(dades->>'nif', '[^0-9A-Za-z]', '', 'g')),
  trim(regexp_replace(dades->>'nom', '\s+', ' ', 'g')),
  lower(nullif(trim(dades->>'email'), '')),
  initcap(trim(dades->>'provincia'))
from public.landing_clients
where origen = 'crm' and rebut_el > now() - interval '1 day'
on conflict (nif) do update
  set nom = excluded.nom,
      email = coalesce(excluded.email, clients.email),
      actualitzat_el = now();

L’on conflict ... do update és l’upsert: insereix si és nou, actualitza si ja hi era. Amb això, tornar a processar el mateix fitxer dues vegades no fa cap mal, que és la propietat que vols de tot procés que s’executa sol.

Des de l’API de Supabase el mateix es fa amb la capçalera Prefer: resolution=merge-duplicates i ?on_conflict=nif.

I les files que no passen les restriccions no es perden: van a una taula d’errors amb el motiu, i aquesta taula és el que envies a qui genera les dades.

Supabase avisant n8n

Fins aquí n8n va a buscar. També pot esperar: Supabase té webhooks de base de dades, que són disparadors que fan una petició HTTP quan una fila canvia.

Es configuren al tauler, o a mà:

create trigger encarrec_nou
after insert on public.encarrecs
for each row execute function supabase_functions.http_request(
  'https://n8n.exemple.com/webhook/encarrec-nou',
  'POST',
  '{"Content-Type":"application/json"}',
  '{}',
  '5000'
);

A partir d’aquí, inserir una fila engega un flux. Això converteix la base de dades en la font d’esdeveniments del bus, i deixa d’haver-hi fluxos que consulten cada cinc minuts per si de cas.

Un avís que estalvia un ensurt: el disparador s’executa a cada fila. Una càrrega massiva de 20.000 files dispararà 20.000 peticions. Per a les càrregues grans, condiciona el disparador (when (new.origen <> 'carrega_massiva')) o deixa que n8n reculli per lots.

Dashboards sense muntar un backend d’informes

Tres nivells, per ordre de cost:

1. Vistes. Una vista és una consulta amb nom, i a Supabase també té API automàtica. És la capa semàntica: defineixes «encàrrecs oberts per client» un cop, i tothom fa servir la mateixa definició.

create view public.v_encarrecs_oberts as
select c.nom, count(*) as oberts, sum(e.import) as import_total
from public.encarrecs e
join public.clients c on c.id = e.client_id
where e.estat in ('nou','en_curs')
group by c.nom;

2. Vistes materialitzades, quan el càlcul és car i no cal que sigui d’aquest segon:

create materialized view public.mv_resum_mensual as
select date_trunc('month', creat_el) as mes,
       count(*) as encarrecs, sum(import) as facturat
from public.encarrecs group by 1;

create unique index on public.mv_resum_mensual (mes);   -- cal per al refresc concurrent

I un flux d’n8n cada nit fa refresh materialized view concurrently public.mv_resum_mensual;. El concurrently és el que fa que el panell no es quedi en blanc mentre es refresca.

3. L’eina de visualització. Metabase, Grafana o el que ja facis servir, connectat directament a Postgres. Com que la lògica és a les vistes, l’eina és intercanviable i no hi ha regles de negoci amagades dins d’un gràfic.

Casos d’ús que veig sovint

  • Consolidar fonts. Tres exportacions diferents cada mes que ningú aconsegueix creuar. Aterratge, canònic, i un panell que abans no existia.
  • Un portal per a clients en dues setmanes. Taules amb RLS, autenticació de Supabase i un front senzill. Sense servidor propi.
  • Formularis que van a algun lloc. El formulari escriu a Supabase; un webhook engega un flux que valida, avisa i crea la fitxa al CRM.
  • Substituir la carpeta compartida. Els fitxers a Storage amb enllaços signats i caducitat, i la metadada a una taula.
  • Sincronitzar dos sistemes que no es parlen. El cas clàssic del bus, amb la taula de moviments com a àrbitre de qui té raó.
  • Informes que s’envien sols. Vista materialitzada, refresc nocturn, i un flux que genera el fitxer i el reparteix.

Els límits, que també són reals

Cap dels dos és màgic. n8n executa fluxos en memòria: per a 500.000 files no vols un flux, vols una consulta SQL. La regla és fer la feina pesada dins de Postgres i que n8n només l’orquestri.

n8n s’ha de vigilar. Amb volum, cal el mode de cua amb Redis, i les execucions antigues s’han de purgar o la seva pròpia base de dades creix sense aturador.

Les credencials són el punt sensible. n8n amb la clau service_role té accés total a les dades. Es tracta com el que és: un sistema de producció, amb accés restringit i còpia de seguretat.

Els esquemes s’han de versionar. Fer canvis clicant al tauler funciona el primer mes i és insostenible el tercer. Migracions amb el CLI de Supabase, al repositori, com el codi.

I les dues es poden allotjar tu mateix. Tant Supabase com n8n tenen versió autoallotjada. Val la pena saber-ho abans de començar, encara que decideixis pagar el servei gestionat: vol dir que la sortida existeix.


Si havies de triar dues eines per cobrir el màxim de terreny amb el mínim de manteniment, avui són aquestes dues. Postgres per garantir el que és veritat, un orquestrador per moure-ho, i tota la resta —API, permisos, panells— sortint de l’esquema en comptes d’escriure’s a mà.

El següent pas

Tens un procés
que odies fer?

Explica-m'ho i et diré si es pot automatitzar — i si no es pot, també t'ho diré. La primera conversa no es cobra — però el cafè el poses tu.

hola@substrat.cat