Supabase GraphQL Direct contre votre propre backend Apollo
Comparer pg_graphql directement depuis Supabase avec un serveur Apollo distinct — authentification, RLS, abonnements, et les cas où une solution hybride avec des fonctions Edge l’emporte.
GraphQL est un langage de requête, pas un environnement d’exécution. Vous pouvez l’exposer depuis plusieurs endroits :
pg_graphqlintégré de Supabase — schéma généré à partir de PostgreSQL, hébergé par Supabase, presque aucune configuration requise- Un backend dédié — Node, Python ou Go exécutant Apollo Server, Hasura, Pothos ou un outil similaire que vous possédez et déployez vous-même
Le frontend peut toujours être Apollo Client dans les deux cas. Ce qui change, c’est l’emplacement du schéma GraphQL et son propriétaire.
Le système d’expertise Vision emprunte la voie directe via Supabase :
// src/client/supabase-grphql-apollo.client.js
const httpLink = new HttpLink({
uri: `${SUPABASE_URL}/graphql/v1`, // ← Supabase's built-in endpoint
});
Aucun processus Express, aucun Apollo Server, aucune résolution écrite manuellement, pas de déploiement supplémentaire. L’interface API correspond au schéma de la base de données.
Esquisses d’architecture
Méthode A : Direct via Supabase
Le navigateur communique directement avec le GraphQL de Supabase. L’authentification s’effectue via des JWT de Supabase, et la sécurité au niveau des lignes constitue le mécanisme de contrôle.
Méthode B : Backend séparé
Le navigateur communique avec votre serveur GraphQL. Les résolveurs se connectent à Supabase (ou à d’autres sources) à l’aide d’un SDK administratif ou de SQL. Vous définissez les types et les champs.
Intuition comparative
Le mode direct est optimisé pour mettre en place des opérations CRUD sans héberger d’API. Le mode séparé est optimisé pour les champs personnalisés, les jointures entre plusieurs services et les abonnements natifs de GraphQL. Mêmes schémas de client Apollo ; propriété différente du schéma et des politiques.
Supabase-direct en pratique
Fonctionnement
Lors du démarrage, pg_graphql analyse PostgreSQL et génère une API GraphQL complète. Les tables deviennent des types Collection avec des opérations CRUD adaptées aux connexions de type Relay.
Configuration du client Apollo (ce projet)
// 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
});
Format des requêtes
Les Collections suivent le schéma de connexion :
query LoadStaff {
staffCollection { # ← always "Collection"
edges { # ← always "edges"
node { # ← always "node"
id
first_name
role { # ← relations via FK are auto-resolved
role_name
}
}
}
}
}
Format des mutations
mutation AddStaff($firstName: String!, $email: String!) {
insertIntostaffCollection( # ← prefix is always "insertInto"
objects: { first_name: $firstName, email: $email }
) {
records {
id
}
}
}
Sécurité de la base de données
Les politiques RLS déterminent qui peut voir quoi. Le « backend » est 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());
Même une requête GraphQL malveillante ne peut pas contourner RLS ; le filtrage s’effectue directement dans Postgres.
Limites de l’approche directe
- Les champs personnalisés qui ne se trouvent pas dans la base de données nécessitent une fonction ou une vue.
- Les champs calculés/virtuels exigent des colonnes calculées ou des vues.
- Il est difficile d’enrichir un seul résolveur via une appel REST de tiers sans Edge Functions.
- Les workflows à plusieurs étapes avec possibilité de rollback doivent être implémentés dans des fonctions de la base de données ou au niveau du frontend.
- La nomenclature suit les conventions
pg_graphql— les underscores sont conservés, d’où des noms tels quehead_officeCollection.
Backend séparé en pratique
Fonctionnement
Vous exécutez Apollo Server (ou un équivalent), vous créez un schéma, et vous écrivez des résolveurs qui appellent Supabase admin, Prisma ou SQL.
Configuration du client 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
});
Schéma personnalisé
# 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!
}
Résolveurs
// 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}`,
};
Les chemins du SDK admin contournent RLS, donc l’autorisation doit être gérée dans le code des résolveurs de manière rigoureuse.
Décryptage approfondi : qu’est-ce qui diffère réellement
1. Flux d’authentification
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 séparé :
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
Le mode direct ne nécessite pas d’échange de tokens — le JWT de Supabase sert à la fois de preuve de connexion et de mot de passe pour la base de données. Un backend séparé ajoute une étape supplémentaire mais vous permet de définir les droits et les durées de validité.
2> Connexion via Apollo Link
Supabase-direct :
// Two specific headers, one endpoint
const link = offlineMutationLink
.concat(authLink) // adds apikey + Bearer
.concat(httpLink); // → supabase.co/graphql/v1
Backend séparé :
// One standard auth header, your endpoint
const link = authLink // adds your token
.concat(httpLink); // → api.yourapp.com/graphql
Même principe que setContext + HttpLink. Les différences résident dans l’URL et dans les en-têtes obligatoires.
3. Requêtes et mutations
Supabase-direct — les noms générés sont fixes :
# Table: staff → Query: staffCollection, Mutation: insertIntostaffCollection
query {
staffCollection(filter: { is_active: { eq: true } }) {
edges { node { id first_name } }
}
}
Backend séparé — vous donnez un nom à tout :
# You chose the name "activeStaff"
query {
activeStaff {
id
fullName # computed in resolver
avatarUrl # fetched from a CDN API in resolver
}
}
Un schéma personnalisé peut simplifier les structures imbriquées, relier des services et calculer des champs dérivés dans une seule réponse. Le mode direct renvoie simplement le contenu des tables.
4. Où se trouvent les règles métier
Supabase-direct :
Business logic → PostgreSQL functions, triggers, and RLS policies
OR handled in the React frontend (as in this project)
Exemple — vérification des doublons de NIC/email côté frontend avant insertion :
// 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 séparé :
// 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 }),
]);
};
Les règles backend restent centralisées pour tous les utilisateurs (web, mobile, partenaires). Les règles uniquement côté frontend sont dispersées et faciles à ignorer.
5. Modèle de sécurité
Le mode direct est plus difficile à mal configurer et à mettre en cause des fuites de données accidentelles, car Postgres impose un contrôle d’accès strict. Un backend séparé est plus flexible, mais dépend davantage de résolveurs soigneusement choisis.
6. Abonnements et temps réel
Supabase-direct utilise les canaux Supabase Realtime, et non des abonnements GraphQL :
// Supabase Realtime — separate from GraphQL
const channel = supabase
.channel("staff-changes")
.on("postgres_changes", { event: "*", schema: "vision_expert", table: "staff" },
(payload) => { refetch(); }
)
.subscribe();
Cette approche est distincte d’Apollo ; le client GraphQL ne reçoit pas automatiquement des mises à jour en temps réel.
Un backend séparé peut offrir de véritables abonnements GraphQL via 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. Soutien hors ligne
Les deux parties peuvent mettre en file des mutations hors ligne grâce à Apollo Link. Vision Expert System fait de même sur la voie directe, en utilisant RxDB comme file d’attente locale.
Le principe reste identique dans l’esprit :
Offline mutation → ApolloLink intercepts → stores in local DB → syncs on reconnect
Le renouvellement des tokens lors de la réconnexion est généralement plus simple lorsque l’on contrôle le cycle de vie des tokens. Avec Supabase, vous devez confirmer que le JWT est toujours valide avant de vider la file d’attente.
Lorsque Supabase-direct convient
Préférez-le lorsque :
- Les charges de travail sont axées sur les opérations CRUD et ont une structure tabulaire
- Vous souhaitez déployer sans gérer un hébergeur d’API
- L’équipe est petite et la complexité du backend est limitée
- La logique peut être intégrée dans les fonctions PostgreSQL et le RLS
- Il n’y a essentiellement qu’un seul consommateur frontend
- Vous souhaitez des coûts d’hébergement quasi nuls pour la couche API
- Les requêtes restent centrées sur les tables sans jointures inter-services
Cela correspond au Vision Expert System : un outil interne multi-rôles, avec toutes les données dans Supabase, RLS par rôle et branche, un seul frontend.
Lorsqu’un backend distinct est nécessaire
Ajoutez-en un lorsque :
- Vous avez besoin de champs calculés/virtuels absents de la base de données
- Une réponse GraphQL doit regrouper Supabase ainsi que les services de paiement, CRM ou autres
- Les transactions en plusieurs étapes doivent rester atomiques entre les tables et les services
- Les développeurs externes appelleront une API publique
- Vous avez besoin de souscriptions GraphQL en tant que mécanisme de notification de premier ordre
- Les règles métier sont trop complexes pour SQL
- GraphQL devient un passage de connexion en fédération au sein d’un écosystème de microservices
- Vous avez besoin de limites de débit au niveau de l’API, de validations ou de journaux d’audit
- Les noms générés par
pg_graphqlsont inacceptables et irréparables
Approche hybride
De nombreuses applications en production combinent les deux approches :
- GraphQL direct pour les lectures et écritures ordinaires
- Fonctions Edge de Supabase pour les 10 % de cas complexes : SDK d’administration (dépassement sécurisé des RLS), appels à des tiers, flux en plusieurs étapes
Frontend Apollo Client
│
├── /graphql/v1 → Supabase pg_graphql (standard CRUD)
│
└── /functions/complex-operation → Supabase Edge Function (business logic)
→ calls Supabase admin SDK internally