Supabase GraphQL Direct versus tu propio backend Apollo
Compare pg_graphql directamente desde Supabase con un servidor Apollo separado: autenticación, RLS, suscripciones, y en qué casos prevalece una ruta de función Edge híbrida.
GraphQL es un lenguaje de consultas, no un entorno de ejecución. Puedes exponerlo desde más de un lugar:
pg_graphqlintegrado de Supabase: esquema generado a partir de PostgreSQL, alojado por Supabase, con casi ninguna configuración- Un backend dedicado: Node, Python o Go ejecutando Apollo Server, Hasura, Pothos u otros similares que tú mismo posees e implementas
En cualquier caso, el frontend puede seguir siendo Apollo Client. Lo que cambia es dónde se encuentra el esquema de GraphQL y quién lo gestiona.
El Sistema Experto Vision sigue el enfoque directo con Supabase:
// src/client/supabase-grphql-apollo.client.js
const httpLink = new HttpLink({
uri: `${SUPABASE_URL}/graphql/v1`, // ← Supabase's built-in endpoint
});
Sin proceso Express, sin Apollo Server, sin resolvers escritos a mano y sin necesidad de una segunda implementación. La interfaz de la API es el esquema de la base de datos.
Bocetos de arquitectura
Enfoque A: Directo con Supabase
El navegador se comunica directamente con el GraphQL de Supabase. La autenticación se realiza mediante JWT de Supabase, y la seguridad a nivel de fila sirve como mecanismo de control.
Enfoque B: Backend separado
El navegador se comunica con tu servidor GraphQL. Los resolvers acceden a Supabase (u otras fuentes) mediante un SDK administrativo o SQL. Tú defines los tipos y campos.
Intuición comparativa
El modo directo se optimiza para implementar operaciones CRUD sin necesidad de alojar una API. El modo separado se optimiza para campos personalizados, uniones entre múltiples servicios y suscripciones nativas de GraphQL. Los patrones del cliente Apollo son los mismos; solo difiere la propiedad del esquema y las políticas.
Supabase-direct en la práctica
Cómo funciona
Al iniciar, pg_graphql examina PostgreSQL y genera una API GraphQL completa. Las tablas se convierten en tipos de colección con operaciones CRUD diseñadas para conexiones al estilo Relay.
Configuración del cliente Apollo (este proyecto)
// Two required headers — no custom backend token scheme
const authLink = setContext((_, { headers }) => ({
headers: {
...headers,
apikey: SUPABASE_ANON_KEY, // identifies the Supabase project
Authorization: `Bearer ${accessToken}`, // Supabase JWT for RLS
"Content-Type": "application/json",
},
}));
const httpLink = new HttpLink({
uri: `${SUPABASE_URL}/graphql/v1`, // Supabase endpoint — no server to host
});
Estructura de las consultas
Las colecciones siguen el patrón de conexión:
query LoadStaff {
staffCollection { # ← always "Collection"
edges { # ← always "edges"
node { # ← always "node"
id
first_name
role { # ← relations via FK are auto-resolved
role_name
}
}
}
}
}
Estructura de las mutaciones
mutation AddStaff($firstName: String!, $email: String!) {
insertIntostaffCollection( # ← prefix is always "insertInto"
objects: { first_name: $firstName, email: $email }
) {
records {
id
}
}
}
Seguridad en la base de datos
Las políticas RLS deciden quién ve qué. El “backend” es PostgreSQL:
-- Only active staff can be read
CREATE POLICY "select active staff"
ON vision_expert.staff
FOR SELECT
USING (is_active = true AND auth_user_id = auth.uid());
Incluso una consulta GraphQL elaborada no puede eludir a RLS; el filtrado se realiza dentro de Postgres.
Límites de trabajar directamente
- Los campos personalizados que no están en la base de datos requieren una función o vista.
- Los campos computados/virtuales necesitan columnas computadas o vistas.
- Enriquecer un único resolvedor con una llamada REST de terceros resulta complicado sin Edge Functions.
- Los flujos de trabajo de múltiples pasos con posibilidad de reversión se gestionan en funciones de la base de datos o en el frontend.
- La nomenclatura sigue las convenciones de
pg_graphql: se mantienen los guiones bajos, por lo que aparecen nombres comohead_officeCollection.
Backend separado en la práctica
Cómo funciona
Ejecutas Apollo Server (o similar), creas un esquema y escribes resolvers que llaman a Supabase admin, Prisma o SQL.
Configuración del cliente Apollo
// Separate backend — your own auth token, your own endpoint
const authLink = setContext((_, { headers }) => ({
headers: {
...headers,
Authorization: `Bearer ${yourCustomJwt}`,
"Content-Type": "application/json",
},
}));
const httpLink = new HttpLink({
uri: "https://api.yourapp.com/graphql", // your server
});
Esquema personalizado
# You define every type by hand
type Staff {
id: ID!
firstName: String!
lastName: String!
fullName: String! # computed field — not in DB
role: Role!
branch: Branch!
subordinates: [Staff!]! # custom logic
}
type Query {
staff(id: ID!): Staff
staffByBranch(branchId: ID!): [Staff!]!
}type Mutation {
addStaff(input: AddStaffInput!): StaffPayload!
deactivateStaff(id: ID!): Boolean!
}
Resolvers
// server/resolvers/staffResolvers.js
const Query = {
staffByBranch: async (_, { branchId }, { supabaseAdmin, currentUser }) => {
// Business logic: only managers can list their own branch
if (currentUser.role !== "manager" && currentUser.branchId !== branchId) {
throw new ForbiddenError("Access denied");
}
// Fetch from Supabase using admin SDK (bypasses RLS)
const { data, error } = await supabaseAdmin
.from("staff")
.select("*, branch(*), role(*)")
.eq("branch_id", branchId); return data;
},
};const Staff = {
// Computed field not in DB
fullName: (parent) => `${parent.first_name} ${parent.last_name}`,
};
Las rutas del SDK de administración evitan RLS, por lo que la autorización debe estar incluida en el código de los resolvers de manera organizada.
Análisis en profundidad: qué difiere realmente
1. Flujo de autenticación
Supabase-direct:
User logs in → supabase.auth.signInWithPassword()
→ Supabase returns JWT
→ JWT stored in session
→ Apollo authLink reads JWT from supabase.auth.getSession()
→ JWT sent in Authorization header with every GraphQL request
→ Supabase validates JWT and enforces RLS
// Token is read dynamically from the Supabase session
let accessToken = null;
supabase.auth.getSession().then(({ data: { session } }) => {
accessToken = session?.access_token ?? null;
});
supabase.auth.onAuthStateChange((_event, session) => {
accessToken = session?.access_token ?? null;
});
Backend separado:
User logs in → POST /auth/login (your endpoint) or Supabase Auth
→ Your server returns a custom JWT
→ Frontend stores JWT
→ Apollo authLink reads JWT from localStorage / context
→ JWT sent to your server
→ Your server validates JWT in middleware
→ Resolver runs with the validated user context
El modo directo no requiere intercambio de tokens: el JWT de Supabase sirve tanto como prueba de inicio de sesión como credencial de la base de datos. Un backend separado añade un paso adicional, pero permite definir los claims y sus períodos de validez.
2. Conexión de Apollo Link
Supabase-direct:
// Two specific headers, one endpoint
const link = offlineMutationLink
.concat(authLink) // adds apikey + Bearer
.concat(httpLink); // → supabase.co/graphql/v1
Backend separado:
// One standard auth header, your endpoint
const link = authLink // adds your token
.concat(httpLink); // → api.yourapp.com/graphql
Misma idea de setContext + HttpLink. Las diferencias están en la URL y en qué encabezados son obligatorios.
3. Consultas y mutaciones
Supabase-direct — los nombres generados son fijos:
# Table: staff → Query: staffCollection, Mutation: insertIntostaffCollection
query {
staffCollection(filter: { is_active: { eq: true } }) {
edges { node { id first_name } }
}
}
Backend separado — tú nombras todo:
# You chose the name "activeStaff"
query {
activeStaff {
id
fullName # computed in resolver
avatarUrl # fetched from a CDN API in resolver
}
}
Un esquema personalizado puede simplificar estructuras anidadas, combinar servicios y calcular campos derivados en una sola respuesta. El modo directo devuelve lo que contienen las tablas.
4. Dónde se encuentran las reglas de negocio
Supabase-direct:
Business logic → PostgreSQL functions, triggers, and RLS policies
OR handled in the React frontend (as in this project)
Ejemplo: verificación de duplicados de NIC/email en el frontend antes de la inserción:
// Frontend handles the business rule: check for dup NIC/email before inserting
const { data: dupData } = await checkDuplicate({ variables: { nic, email } });
const nicExists = dupData?.nicCheck?.edges?.length > 0;
if (nicExists) {
alert("A staff member with this NIC already exists.");
return;
}
await addStaff({ variables: { ... } });
Backend separado:
// Backend resolver handles the business rule
const addStaff = async (_, { input }, { db }) => {
const existing = await db.staff.findFirst({
where: { OR: [{ nic: input.nic }, { email: input.email }] }
});
if (existing) throw new UserInputError("NIC or email already exists");
// Atomic transaction: create auth user + insert staff profile
return await db.$transaction([
supabaseAdmin.auth.admin.createUser({ email: input.email }),
db.staff.create({ data: input }),
]);
};
Las reglas del backend permanecen centralizadas para todos los consumidores (web, móvil, socios). Las reglas solo en el frontend están dispersas y son fáciles de pasar por alto.
5. Modelo de seguridad
El modo directo es más difícil de configurar incorrectamente y causar fugas accidentales de datos, ya que Postgres controla el acceso. Un backend separado es más flexible pero depende más de resolvers cuidadosos.
6. Suscripciones y tiempo real
Supabase-direct utiliza canales de Supabase Realtime, no suscripciones GraphQL:
// Supabase Realtime — separate from GraphQL
const channel = supabase
.channel("staff-changes")
.on("postgres_changes", { event: "*", schema: "vision_expert", table: "staff" },
(payload) => { refetch(); }
)
.subscribe();
Ese camino es independiente de Apollo; el cliente GraphQL no recibe notificaciones en tiempo real por sí solo.
Backend separado puede ofrecer verdaderas suscripciones GraphQL a través de WebSocket:
subscription {
staffAdded {
id
first_name
role { role_name }
}
}
// Apollo Client with WebSocket link
import { GraphQLWsLink } from "@apollo/client/link/subscriptions";
import { createClient } from "graphql-ws";
const wsLink = new GraphQLWsLink(
createClient({ url: "wss://api.yourapp.com/graphql" })
);
7. Soporte sin conexión
Ambos lados pueden poner en cola mutaciones sin conexión mediante Apollo Link. Vision Expert System hace esto en el camino directo usando RxDB como cola local.
El patrón es similar en esencia:
Offline mutation → ApolloLink intercepts → stores in local DB → syncs on reconnect
La actualización de tokens al reconectar suele ser más sencilla cuando se controla el ciclo de vida del token. Con Supabase es necesario confirmar que el JWT sigue siendo válido antes de vaciar la cola.
Cuándo es adecuado Supabase-direct
Preferirlo cuando:
- Las cargas de trabajo son intensivas en operaciones CRUD y tienen estructura tabular
- Deseas lanzar la aplicación sin necesidad de ejecutar un servidor API
- El equipo es pequeño y la complejidad del backend es limitada
- La lógica se adapta a las funciones de PostgreSQL y al RLS
- Básicamente hay un único consumidor frontend
- Deseas costos de hosting cercanos a cero para la capa API
- Las consultas se centran en las tablas sin uniones entre servicios distintos
Eso coincide con Vision Expert System: una herramienta interna multirol, todos los datos en Supabase, RLS por rol y ramificación, un único frontend.
Cuándo merece la pena usar un backend separado
Agregar uno cuando:
- Necesita campos calculados/virtuales que no estén en la base de datos
- Una respuesta GraphQL debe agregar información de Supabase además de pagos, CRM u otros servicios
- Las transacciones de múltiples pasos deben mantenerse atómicas entre tablas y servicios
- Los desarrolladores externos llamarán a una API pública
- Necesita suscripciones GraphQL como mecanismo de notificación de primera clase
- Las reglas de negocio son demasiado complejas para SQL
- GraphQL funciona como puerta de enlace de federación en un entorno de microservicios
- Necesita límites de velocidad a nivel de API, validación o registros de auditoría
- Los nombres generados por
pg_graphqlson inaceptables e irreparables
Ruta híbrida
Muchas aplicaciones en producción combinan ambos enfoques:
- GraphQL directo para lecturas y escrituras habituales
- Supabase Edge Functions para el 10% de casos complejos: SDK administrativo (evita RLS), llamadas a terceros, flujos de múltiples pasos
Frontend Apollo Client
│
├── /graphql/v1 → Supabase pg_graphql (standard CRUD)
│
└── /functions/complex-operation → Supabase Edge Function (business logic)
→ calls Supabase admin SDK internally
Mantiene la velocidad y el bajo mantenimiento del modo directo para la mayor parte del tráfico, y reserva la flexibilidad propia del backend para las operaciones que realmente la necesitan, sin tener que configurar un servidor siempre activo para todo.
Lecturas relacionadas
- La consulta GraphQL que agotó el pool de base de datos — Un documento GraphQL anidado agotó una base de datos en producción. Por qué fallaron los límites de tasa, los tiempos de espera HTTP y DataLoader — y los cuatro límites que finalmente lo contuvieron.