Supabase GraphQL Direct im Vergleich zu Ihrem eigenen Apollo-Backend
Vergleichen Sie pg_graphql direkt aus Supabase mit einem separaten Apollo-Server – Authentifizierung, RLS, Abonnements sowie die Situationen, in denen ein hybrider Edge-Function-Pfad vorteilhaft ist.
GraphQL ist eine Abfragesprache, kein Laufzeitumfeld. Sie kann von mehreren Orten aus bereitgestellt werden:
- Supabases integrierter
pg_graphql– Schema, das aus PostgreSQL generiert wird, von Supabase gehostet und nahezu konfigurationsfrei ist - Ein dedizierter Backend-Server – Node, Python oder Go mit Apollo Server, Hasura, Pothos oder ähnlichen Tools, den man selbst betreibt und bereitstellt
In jedem Fall kann der Frontend-Teil weiterhin Apollo Client verwenden. Was sich ändert, ist der Ort, an dem das GraphQL-Schema gespeichert ist, und wer es betreut.
Das Vision Expert System wählt den direkten Weg über Supabase:
// src/client/supabase-grphql-apollo.client.js
const httpLink = new HttpLink({
uri: `${SUPABASE_URL}/graphql/v1`, // ← Supabase's built-in endpoint
});
Kein Express-Prozess, kein Apollo Server, keine manuell geschriebenen Resolver, keine zusätzliche Bereitstellung. Die API-Oberfläche besteht aus dem Datenbank-Schema.
Architekturskizzen
Ansatz A: Direkt über Supabase
Der Browser kommuniziert direkt mit Supabases GraphQL-Service. Die Authentifizierung erfolgt über Supabase JWTs, und die Zeilenbereichssicherheit dient als Zugangskontrolle.
Ansatz B: Getrennter Backend
Der Browser kommuniziert mit Ihrem GraphQL-Server. Die Resolver erreichen Supabase (oder andere Quellen) mithilfe eines Admin SDKs oder SQL. Sie definieren die Typen und Felder selbst.
Intuition im Vergleich
Der Direktmodus optimiert sich für die Bereitstellung von CRUD-Funktionen ohne Hosting einer API. Der getrennte Modus optimiert sich für benutzerdefinierte Felder, Verbindungen mehrerer Dienste sowie GraphQL-eigene Abonnements. Die Apollo-Client-Muster bleiben gleich; die Verantwortung für Schema und Richtlinien unterscheidet sich.
Supabase-direct in der Praxis
Wie es funktioniert
Zur Startzeit analysiert pg_graphql PostgreSQL und stellt eine vollständige GraphQL-API bereit. Tabellen werden zu Collection-Typen, deren CRUD-Funktionen für verbindungsorientierte Ansätze wie Relay konzipiert sind.
Apollo-Client-Konfiguration (dieses Projekt)
// 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
});
Form der Abfragen
Collection-Typen folgen dem Verbindungsmodell:
query LoadStaff {
staffCollection { # ← always "Collection"
edges { # ← always "edges"
node { # ← always "node"
id
first_name
role { # ← relations via FK are auto-resolved
role_name
}
}
}
}
}
Form der Mutationen
mutation AddStaff($firstName: String!, $email: String!) {
insertIntostaffCollection( # ← prefix is always "insertInto"
objects: { first_name: $firstName, email: $email }
) {
records {
id
}
}
}
Sicherheit in der Datenbank
Die RLS-Richtlinien bestimmen, wer was sehen darf. Die „Backend“-Plattform ist 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());
Selbst eine speziell konstruierte GraphQL-Anfrage kann RLS nicht umgehen; das Filtern findet innerhalb von Postgres statt.
Beschränkungen eines direkten Ansatzes
- Custom-Felder, die nicht in der DB vorhanden sind, erfordern eine Funktion oder ein View.
- Berechnete/virtuelle Felder benötigen berechnete Spalten oder Views.
- Die Erweiterung eines einzelnen Resolvers durch einen Aufruf einer Drittanbieter-REST-API ist ohne Edge Functions umständlich.
- Mehrschrittige Workflows mit Rollbacks werden in DB-Funktionen oder im Frontend umgesetzt.
- Die Benennung folgt den Konventionen von
pg_graphql– Unterstriche bleiben erhalten, wodurch Namen wiehead_officeCollectionentstehen.
Echter separater Backend-Anteil
Wie es funktioniert
Man führt Apollo Server (oder ähnliches) aus, erstellt ein Schema und schreibt Resolver, die Supabase Admin, Prisma oder SQL aufrufen.
Apollo-Client-Konfiguration
// 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
});
Custom-Schema
# 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!
}
Resolver
// 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}`,
};
Die Pfade des Admin SDK umgehen RLS, daher muss die Autorisierung diszipliniert im Resolver-Code implementiert werden.
Einsatzbeispiel: Was unterscheidet sich tatsächlich?
1. Authentifizierungsfluss
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;
});
Selbstständiger Backend:
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
Im Direktmodus ist kein Tokenaustausch erforderlich – das Supabase JWT dient sowohl als Login-Nachweis als auch als Datenbank-Zugangsdaten. Ein separates Backend fügt zwar einen zusätzlichen Schritt hinzu, ermöglicht aber die Gestaltung der Angaben sowie der Gültigkeitsdauer.
2. Verkabelung von Apollo Link
Supabase-direct:
// Two specific headers, one endpoint
const link = offlineMutationLink
.concat(authLink) // adds apikey + Bearer
.concat(httpLink); // → supabase.co/graphql/v1
Selbstständiger Backend:
// One standard auth header, your endpoint
const link = authLink // adds your token
.concat(httpLink); // → api.yourapp.com/graphql
Dieselbe Idee wie setContext + HttpLink. Die Unterschiede liegen im URL sowie in den erforderlichen Headern.
3. Abfragen und Mutationen
Supabase-direct – die generierten Namen sind festgelegt:
# Table: staff → Query: staffCollection, Mutation: insertIntostaffCollection
query {
staffCollection(filter: { is_active: { eq: true } }) {
edges { node { id first_name } }
}
}
Selbstständiger Backend – Sie geben alles selbst einen Namen:
# You chose the name "activeStaff"
query {
activeStaff {
id
fullName # computed in resolver
avatarUrl # fetched from a CDN API in resolver
}
}
Ein benutzerdefiniertes Schema kann verschachtelte Strukturen vereinfachen, Dienste verknüpfen und abgeleitete Felder in einer einzigen Antwort berechnen. Im Direktmodus werden die Inhalte der Tabellen zurückgegeben.
4. Wo sich Geschäftsregeln befinden
Supabase-direct:
Business logic → PostgreSQL functions, triggers, and RLS policies
OR handled in the React frontend (as in this project)
Beispiel – Überprüfung auf doppelte NIC/E-Mail im Frontend vor dem Einfügen:
// 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: { ... } });
Selbstständiger Backend:
// 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 }),
]);
};
Die Backend-Regeln bleiben für jeden Nutzer (Web, Mobil, Partner) zentralisiert. Regeln, die nur im Frontend gelten, sind verstreut und leicht zu übersehen.
5. Sicherheitsmodell
Im Direktmodus ist es schwieriger, Fehler zu machen, die zu versehentlichen Datenlecks führen, da Postgres den Zugriff kontrolliert. Ein separater Backend ist flexibler, erfordert aber sorgfältige Handhabung durch die Resolver.
6. Abonnements und Echtzeit
Supabase-direct verwendet Supabase Realtime-Kanäle statt GraphQL-Abonnements:
// Supabase Realtime — separate from GraphQL
const channel = supabase
.channel("staff-changes")
.on("postgres_changes", { event: "*", schema: "vision_expert", table: "staff" },
(payload) => { refetch(); }
)
.subscribe();
Dieser Weg ist vom Apollo-Modell getrennt; der GraphQL-Client erhält keine Live-Daten selbstständig.
Ein separater Backend kann echte GraphQL-Abonnements über WebSocket anbieten:
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. Offline-Unterstützung
Sowohl Seiten können Offline-Mutationen über Apollo Link in die Warteschlange geben. Vision Expert System tut dies im Direktmodus mit RxDB als lokaler Warteschlange.
Im Grunde ist das Prinzip dasselbe:
Offline mutation → ApolloLink intercepts → stores in local DB → syncs on reconnect
Die Erneuerung von Tokens bei der Wiederverbindung ist in der Regel einfacher, wenn man die Kontrolle über den Lebenszyklus des Tokens hat. Bei Supabase muss man zunächst bestätigen, dass das JWT noch gültig ist, bevor die Warteschlange geleert wird.
Wann Supabase-direct geeignet ist
Wählen Sie es, wenn:
- Die Arbeitslast hauptsächlich aus CRUD-Aufgaben besteht und tabellenbasiert ist
- Sie ohne Betrieb eines API-Hosts veröffentlichen möchten
- Das Team klein ist und die Tiefe des Backends begrenzt ist
- Die Logik in PostgreSQL-Funktionen und RLS untergebracht werden kann
- Im Grunde nur ein Frontend-Verbraucher vorhanden ist
- Sie nahezu keine Hosting-Kosten für die API-Schicht haben möchten
- Die Abfragen tabellenzentriert bleiben, ohne Verbindungen zu anderen Diensten
Das entspricht dem Vision Expert System: ein internes Mehrfachrollen-Tool, alle Daten in Supabase, RLS nach Rolle und Branch, ein Frontend.
Wann ein separates Backend sinnvoll ist
Fügen Sie eines hinzu, wenn:
- Ihre Anwendung benötigt berechnete/virtuelle Felder, die in der Datenbank nicht vorhanden sind
- Eine GraphQL-Antwort muss Supabase zusammen mit Zahlungsdiensten, CRM-Systemen oder anderen Diensten aggregieren
- Mehrschrittige Transaktionen müssen in Bezug auf Tabellen und Dienste atomar bleiben
- Externe Entwickler werden eine öffentliche API aufrufen
- Ihre Anwendung benötigt GraphQL-Subscriptions als erstklassige Push-Funktion
- Business-Regeln sind für SQL zu aufwendig umzusetzen
- GraphQL dient als Federation-Gateway in einem Microservice-Ökosystem
- Ihre Anwendung benötigt API-Ebene-Rate-Limits, Validierungen oder Audit-Logs
- Die generierten
pg_graphql-Namen sind inakzeptabel und nicht korrigierbar
Hybridansatz
Viele Produktivanwendungen kombinieren beides:
- Direktes GraphQL für gewöhnliche Lese- und Schreibvorgänge
- Supabase Edge Functions für die schwierigen 10 %: Admin-SDK (sichere Umgehung von RLS), Aufrufe Drittanbieter, mehrschrittige Abläufe
Frontend Apollo Client
│
├── /graphql/v1 → Supabase pg_graphql (standard CRUD)
│
└── /functions/complex-operation → Supabase Edge Function (business logic)
→ calls Supabase admin SDK internally
Man behält die Geschwindigkeit sowie den geringen Wartungsaufwand des Direktmodus für den größten Teil des Verkehrs bei und reserviert die an einen Backend angepasste Flexibilität für die Operationen, die sie tatsächlich benötigen – ohne dafür einen vollständig ständig verfügbaren Server für alles einzurichten.