Inicio / Artículos / Supabase GraphQL Direct versus tu propio backend Apollo

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.

1853 palabras

GraphQL es un lenguaje de consultas, no un entorno de ejecución. Puedes exponerlo desde más de un lugar:

  • pg_graphql integrado 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

  1. Los campos personalizados que no están en la base de datos requieren una función o vista.
  2. Los campos computados/virtuales necesitan columnas computadas o vistas.
  3. Enriquecer un único resolvedor con una llamada REST de terceros resulta complicado sin Edge Functions.
  4. 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.
  5. La nomenclatura sigue las convenciones de pg_graphql: se mantienen los guiones bajos, por lo que aparecen nombres como head_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_graphql son 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

  • Cache Stampedes en Node.js: Por qué un caché de Redis puede sobrecargar su base de datos — Aprenda por qué una configuración ingenua de caché separado con Redis sincroniza la carga en su base de datos cuando una clave importante vence, y cómo los bloqueos y las actualizaciones en segundo plano evitan esto.