Strona główna / Artykuły / Supabase GraphQL Direct kontra własny backend Apollo

Supabase GraphQL Direct kontra własny backend Apollo

Porównaj pg_graphql bezpośrednio z Supabase z oddzielnym serwerem Apollo – autoryzację, RLS, subskrypcje oraz sytuacje, gdy korzystniejsza okazuje się ścieżka hybrydowych funkcji Edge.

1853 słów

GraphQL to język zapytań, a nie środowisko wykonawcze. Można go udostępnić z więcej niż jednego miejsca:

  • wbudowany w Supabase pg_graphql — schemat generowany z PostgreSQL, hostowany przez Supabase, praktycznie bez konfiguracji
  • dedykowany backend — Node, Python lub Go z serwerem Apollo, Hasura, Pothos lub podobnym, który należy do Ciebie i jest przez Ciebie wdrażany

We frontendzie i tak może być użyty Apollo Client. Zmienia się jedynie miejsce przechowywania schematu GraphQL oraz to, kto nim zarządza.

System ekspertów Vision wybiera drogę bezpośrednią przez Supabase:

// src/client/supabase-grphql-apollo.client.js
const httpLink = new HttpLink({
  uri: `${SUPABASE_URL}/graphql/v1`,  // ← Supabase's built-in endpoint
});

Brak procesu Express, brak serwera Apollo, brak ręcznie pisanych rozwiązywarek, brak dodatkowego wdrażania. Interfejs API stanowi schemat bazy danych.

Szkice architektury

Podejście A: Bezpośrednio przez Supabase

Brauzer komunikuje się bezpośrednio z GraphQL w Supabase. Autoryzacja odbywa się za pomocą tokenów JWT od Supabase, a bezpieczeństwo na poziomie wierszy to mechanizm kontrolny.

Sposób B: Odrębny backend

Brauzer komunikuje się z twoim serwerem GraphQL. Rozwiązujące zadania funkcje łączą się z Supabase (lub innymi źródłami) za pomocą SDK administracyjnego lub SQL. Ty określasz typy i pola.

Intuicja porównawcza

Tryb bezpośredni optymalizuje się pod kątem realizacji operacji CRUD bez konieczności hostowania API. Tryb oddzielny optymalizuje się pod kątem dostosowanych pól, łączeń między wieloma usługami oraz subskrypcji typowych dla GraphQL. Te same wzorce klienta Apollo; różna forma zarządzania schematem i zasadami.

Supabase-direct w praktyce

Jak to działa

Podczas uruchamiania pg_graphql analizuje bazę danych PostgreSQL i generuje pełne API GraphQL. Tabele stają się typami Collection, a operacje CRUD są dostosowane do połączeń w stylu Relay.

Konfiguracja klienta Apollo (ten 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
});

Struktura zapytania

Kolekcje stosują wzorzec połączeń:

query LoadStaff {
  staffCollection {        # ← always "Collection"
    edges {                # ← always "edges"
      node {               # ← always "node"
        id
        first_name
        role {             # ← relations via FK are auto-resolved
          role_name
        }
      }
    }
  }
}

Struktura mutacji

mutation AddStaff($firstName: String!, $email: String!) {
  insertIntostaffCollection(         # ← prefix is always "insertInto"
    objects: { first_name: $firstName, email: $email }
  ) {
    records {
      id
    }
  }
}

Bезpieczeństwo w bazie danych

Zasady RLS decydują, kto może zobaczyć co. „Backend” to 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());

Nawet specjalnie skonstruowana zapytanie GraphQL nie może obejść RLS; filtrowanie odbywa się wewnątrz Postgresa.

Ograniczenia pracy bezpośredniej

  1. Domyślne pola, których nie ma w bazie danych, wymagają funkcji lub widoku.
  2. Pola obliczane/wirtualne wymagają kolumn obliczanych lub widoków.
  3. wzbogacanie pojedynczego rozwiązującego zapytania za pomocą połączenia REST od strony trzeciej jest utrudnione bez Edge Functions.
  4. Procesy wieloetapowe z możliwością cofnięcia działania realizowane są w funkcjach bazy danych lub w frontendzie.
  5. Nazewnictwo follows zasadami pg_graphql — podkreślniki pozostają, więc pojawiają się nazwy takie jak head_officeCollection.

Rzeczywiste zastosowanie oddzielnego backendu

Jak to działa

Zapусkasz Apollo Server (lub podobne narzędzie), tworzysz schemat oraz piszesz rozwiązania, które wywołują funkcje administracyjne Supabase, Prismę lub SQL.

Konfiguracja klienta 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
});

Schemat dostosowany

# 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!
}

Rozwiązania

// 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}`,
};

Drogi do SDK administracyjnego omijają mechanizm RLS, dlatego autoryzacja musi być realizowana w kodzie rozwiązań w sposób uporządkowany.

Głębsze spojrzenie: co tak naprawdę się różni

1. Przepływ autoryzacji

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;
});

Odrębny 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

W trybie bezpośrednim nie ma konieczności wymiany tokenów — JWT od Supabase służy zarówno jako dowód logowania, jak i uprawnienia do bazy danych. Odrębny backend dodaje jeden etap, ale umożliwia dostosowanie informacji o użytkowniku oraz okresu ważności uprawnień.

2> Połączenie w Apollo Link

Supabase-direct:

// Two specific headers, one endpoint
const link = offlineMutationLink
  .concat(authLink)   // adds apikey + Bearer
  .concat(httpLink);  // → supabase.co/graphql/v1

Odrębny backend:

// One standard auth header, your endpoint
const link = authLink    // adds your token
  .concat(httpLink);     // → api.yourapp.com/graphql

To ta sama koncepcja setContext + HttpLink. Różnice dotyczą adresu URL oraz tego, które nagłówki są obowiązkowe.

3. Zapytania i modyfikacje

Supabase-direct — nazwy generowane są stałe:

# Table: staff → Query: staffCollection, Mutation: insertIntostaffCollection
query {
  staffCollection(filter: { is_active: { eq: true } }) {
    edges { node { id first_name } }
  }
}

Odrębny backend — sam wybierasz nazwy wszystkich elementów:

# You chose the name "activeStaff"
query {
  activeStaff {
    id
    fullName          # computed in resolver
    avatarUrl         # fetched from a CDN API in resolver
  }
}

Własny schemat może spłaszczać struktury zagnieżdżone, łączyć usługi oraz obliczać pola pochodne w jednej odpowiedzi. Tryb bezpośredni zwraca dane zawarte w tabelach.

4. Gdzie znajdują się reguły biznesowe

Supabase-direct:

Business logic →  PostgreSQL functions, triggers, and RLS policies
                  OR handled in the React frontend (as in this project)

Przykład — sprawdzenie duplikatów numeru karty sieciowej/adresu e-mail w interfejsie użytkownika przed dodaniem danych:

// 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: { ... } });

Odrębny 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 }),
  ]);
};

Reguły backendu pozostają scentralizowane dla wszystkich użytkowników (strona internetowa, aplikacje mobilne, partnerzy). Reguły dostępne tylko w interfejsie użytkownika są rozproszone i łatwo je pominąć.

5. Model bezpieczeństwa

Tryb bezpośredni jest trudniejszy do błędnego skonfigurowania, co mogłoby doprowadzić do wycieku danych, ponieważ Postgres kontroluje dostęp. Odrębny backend jest bardziej elastyczny, ale wymaga dokładniejszego doboru narzędzi do rozwiązywania problemów.

6. Subskrypcje i transmisja w czasie rzeczywistym

Supabase-direct wykorzystuje kanały Supabase Realtime, a nie subskrypcje GraphQL:

// Supabase Realtime — separate from GraphQL
const channel = supabase
  .channel("staff-changes")
  .on("postgres_changes", { event: "*", schema: "vision_expert", table: "staff" },
    (payload) => { refetch(); }
  )
  .subscribe();

Ta ścieżka jest oddzielona od Apollo; klient GraphQL sam w sobie nie otrzymuje aktualizacji w czasie rzeczywistym.

Odrębny backend może zapewnić prawdziwe subskrypcje GraphQL przez 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. Obsługa pracy offline

Oba strony mogą umieszczać operacje w kolejce offline za pośrednictwem Apollo Link. Vision Expert System robi to w trybie bezpośrednim, używając RxDB jako lokalnej kolejki.

Koncepcja jest w zasadzie taka sama:

Offline mutation → ApolloLink intercepts → stores in local DB → syncs on reconnect

Odnawianie tokena po ponownym połączeniu jest zazwyczaj prostsze, gdy kontrolujesz cały cykl życia tokena. W przypadku Supabase musisz potwierdzić, że JWT nadal jest ważne, zanim wyczyścisz kolejkę.

Kiedy nadaje się rozwiązanie Supabase-direct

Należy je preferować wtedy, gdy:

  • Zadania są skoncentrowane na operacjach CRUD i mają strukturę tabeli
  • Chcesz uruchomić aplikację bez konieczności prowadzenia hosta API
  • Zespół jest mały, a złożoność backendu ograniczona
  • Logika mieści się w funkcjach PostgreSQL i mechanizmie RLS
  • Istnieje zasadniczo jeden konsument interfejsu użytkownika
  • Chcesz niemal zerowych kosztów hostingu dla warstwy API
  • Zapytania są skoncentrowane wokół tabeli, bez połączeń między usługami

To odpowiada systemowi Vision Expert System: narzędziu wewnętrznemu z wieloma rolami, wszystkim danym przechowywanym w Supabase, mechanizmem RLS dostosowanym do ról i gałęzi oraz jednym interfejsem użytkownika.

Kiedy warto użyć oddzielnego backendu

Należy go dodać wtedy, gdy:

  • Potrzebne są pola obliczane/wirtualne, których nie ma w bazie danych
  • Jedna odpowiedź GraphQL musi agregować dane z Supabase oraz informacje o płatnościach, CRM lub innych usługach
  • Transakcje wieloetapowe muszą zachować atrybut atomowości między tabelami i usługami
  • Zewnętrzni programiści będą korzystać z publicznego API
  • Potrzebne są subskrypcje GraphQL jako rozwiązanie typu push pierwszej klasy
  • Zasady biznesowe są zbyt skomplikowane dla SQL
  • GraphQL pełni rolę bramy federacyjnej w architekturze mikrousług
  • Potrzebne są ograniczenia szybkości, walidacja lub logi audytowe na poziomie API
  • Generowane nazwy typu pg_graphql są nie do przyjęcia i niemożliwe do naprawy

Szlak hybrydowy

wiele aplikacji produkcyjnych łączy oba podejścia:

  • Bezpośredni GraphQL do zwykłych operacji odczytu i zapisu
  • Supabase Edge Functions do trudnych przypadków stanowiących 10% zadań: SDK administracyjne (bezpieczne obejście RLS), wywołania stron trzecich, procesy wieloetapowe
Frontend Apollo Client
    │
    ├── /graphql/v1  →  Supabase pg_graphql  (standard CRUD)
    │
    └── /functions/complex-operation  →  Supabase Edge Function (business logic)
                                          → calls Supabase admin SDK internally

Zachowujesz szybkość i niskie wymagania konserkcyjne trybu bezpośredniego dla większości ruchu, a elastyczność typową dla backendu przeznaczasz na operacje, które faktycznie jej potrzebują — bez konieczności uruchamiania pełnego serwera dostępnego cały czas dla wszystkiego.