Startseite / Artikel / Supabase GraphQL Direct im Vergleich zu Ihrem eigenen Apollo-Backend

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.

1853 Wörter

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

  1. Custom-Felder, die nicht in der DB vorhanden sind, erfordern eine Funktion oder ein View.
  2. Berechnete/virtuelle Felder benötigen berechnete Spalten oder Views.
  3. Die Erweiterung eines einzelnen Resolvers durch einen Aufruf einer Drittanbieter-REST-API ist ohne Edge Functions umständlich.
  4. Mehrschrittige Workflows mit Rollbacks werden in DB-Funktionen oder im Frontend umgesetzt.
  5. Die Benennung folgt den Konventionen von pg_graphql – Unterstriche bleiben erhalten, wodurch Namen wie head_officeCollection entstehen.

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.