Головна / Статті / Supabase GraphQL Direct проти власного сервера Apollo

Supabase GraphQL Direct проти власного сервера Apollo

Порівняйте pg_graphql безпосередньо з Supabase та окремим сервером Apollo — автентифікація, RLS, підписки та ситуації, коли перевагу має гібридний шлях Edge Function.

1853 слів

GraphQL — це мова запитів, а не середовище виконання. Його можна розгорнути з кількох місць:

  • Вбудований у Supabase модуль pg_graphql — схема, створена на основі PostgreSQL, хостована Supabase, майже без налаштувань
  • Спеціалізований бекенд — Node, Python чи Go із сервером Apollo, Hasura, Pothos або подібними інструментами, які належать вам та розгорнуті вами

У будь-якому разі фронтенд може залишатися Apollo Client. Змінюється лише місце зберігання схеми GraphQL та той, хто нею керує.

Система експертів Vision обирає шлях безпосереднього використання Supabase:

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

Жодного процесу Express, жодного сервера Apollo, жодних власноруч написаних резолверів, жодного додаткового розгортання. Інтерфейс API — це схема бази даних.

Ескізи архітектури

Підхід А: безпосереднє використання Supabase

Браузер взаємодіє безпосередньо з GraphQL Supabase. Аутентифікація здійснюється за допомогою JWT від Supabase. Безпека на рівні рядків є основним механізмом контролю.

Підхід B: Окремий бекенд

Браузер взаємодіє з вашим сервером GraphQL. Резолвери підключаються до Supabase (або інших джерел) за допомогою admin SDK чи SQL. Ви самі визначаєте типи та поля.

Інтуїтивне розуміння порівняно

Прямий режим оптимізований для реалізації функцій CRUD без необхідності хостингу API. Окремий режим оптимізований для власних полів, об’єднання кількох сервісів та підписок у стилі GraphQL. Ті самі шаблони клієнта Apollo; різна власність на схему та правила.

Supabase-direct на практиці

Як це працює

Під час запуску pg_graphql аналізує PostgreSQL та створює повний GraphQL API. Таблиці перетворюються на типи Collection із функціями CRUD, придатними для з’єднань у стилі Relay.

Конфігурація клієнта Apollo (цей проект)

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

Формат запиту

Колекції дотримуються шаблону з’єднання:

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

Формат мутації

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

Безпека бази даних

Політики RLS визначають, хто бачить що. «Бекенд» — це 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());

Nавіть спеціально створений запит GraphQL не може обійти RLS; фільтрація відбувається всередині Postgres.

Обмеження прямого підходу

  1. Для користувацьких полів, яких немає в БД, потрібні функція чи представлення.
  2. Для обчислюваних/віртуальних полів потрібні обчислювані стовпці чи представлення.
  3. Розширення одного резолвера за допомогою виклику REST від сторонньої системи є складним без Edge Functions.
  4. Багатоетапні робочі процеси з можливістю скасування виконання реалізуються через функції БД або на фронтенді.
  5. Найменування відповідають конвенціям pg_graphql — підкреслення зберігаються, тому використовуються назви на кшталт head_officeCollection.

Роздільний бекенд на практиці

Як це працює

Ви запускаєте Apollo Server (або подібний), створюєте схему та пишете резолвери, які викликають Supabase admin, Prisma чи SQL.

Конфігурація клієнта 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
});

Персоналізована схема

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

Резолвери

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

Шляхи до SDK адміністрування оминають RLS, тому авторизація мусить бути реалізована у коді резолверів з суворим дотриманням правил.

Детальний аналіз: у чому саме різниця

1. Процес автентифікації

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

Окремий бекенд:

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

У прямому режимі не потрібна заміна токенів — JWT від Supabase є одночасно доказом входу та обліковими даними бази даних. Окремий бекенд додає додатковий етап, але дозволяє формувати вимоги та терміни дії.

2. Підключення Apollo Link

Supabase-direct:

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

Окремий бекенд:

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

Та сама ідея setContext + HttpLink. Різниця полягає у URL та в тому, які заголовки є обов’язковими.

3. Запити та зміни

Supabase-direct — імена, які генеруються, є фіксованими:

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

Окремий бекенд — ви самі називаєте все:

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

Власна схема може спростити структуру даних, об’єднати сервіси та обчислити похідні поля в одній відповіді. Режим Direct повертає те, що містять таблиці.

4. Де знаходяться бізнес-правила

Supabase-direct:

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

Приклад — перевірка на наявність дублікатів NIC/електронної пошти на фронтенді перед додаванням:

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

Правила бекенду залишаються централізованими для кожного користувача (веб, мобільні пристрої, партнери). Правила лише для фронтенду розкидані та легко ігноруються.

5. Модель безпеки

У прямому режимі складніше виникнути помилці конфігурації, яка призведе до випадкової втрати даних, оскільки Postgres контролює доступ. Окремий бекенд є більш гнучким, але більше залежить від ретельної налаштуваності механізмів обробки запитів.

6. Підписки та реальний час

Supabase-direct використовує канали Supabase Realtime, а не підписки GraphQL:

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

Цей шлях є окремим від Apollo; клієнт GraphQL сам по собі не отримує живих повідомлень.

Окремий бекенд може забезпечити справжні підписки GraphQL через 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. Підтримка офлайн-режиму

Обидві сторони можуть чергувати операції офлайн через Apollo Link. Vision Expert System робить це у прямому режимі за допомогою RxDB як локальної черги.

Принцип є однаковим у суті:

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

Оновлення токена під час повторного підключення зазвичай простіше, якщо ви контролюєте цикл життя токена. З Supabase вам потрібно підтвердити, що JWT все ще дійсний, перш ніж очистити чергу.

Коли підходить Supabase-direct

Використовуйте його, коли:

  • Робота полягає переважно у операціях CRUD та структурована за таблицями
  • Ви хочете запустити проект без необхідності керувати хостингом API
  • Команда невелика, а глибина роботи з бекендом обмежена
  • Логіка підходить для функцій PostgreSQL та механізму RLS
  • Існує фактично один користувач фронтенду
  • Ви хочете майже нульові витрати на хостинг API-шару
  • Запити залишаються орієнтованими на таблиці без з’єднань між сервісами

Це відповідає Vision Expert System: внутрішньому інструменту з кількома ролями, усі дані знаходяться в Supabase, захист через RLS за ролями та гілками, один фронтенд.

Коли потрібен окремий бекенд

Додайте його, коли:

  • Вам потрібні обчислювані/віртуальні поля, відсутні у БД
  • Одна відповідь GraphQL має об’єднувати дані Supabase та інформацію про оплати, CRM чи інші сервіси
  • Багатокрокові транзакції мають залишатися атомарними між таблицями та сервісами
  • Зовнішні розробники будуть використовувати публічний API
  • Вам потрібні підписки GraphQL як засіб для негайних повідомлень
  • Бізнес-правила є занадто складними для реалізації за допомогою SQL
  • GraphQL виступає шлюзом федерації у екосистемі мікросервісів
  • Вам потрібні обмеження швидкості, перевірка даних чи журнали аудиту на рівні API
  • Генеровані імена типу pg_graphql є неприйнятними та не підлягають виправленню

Гібридний підхід

Багато продакшн-застосунків поєднують обидва підходи:

  • Прямий використання GraphQL для звичайних операцій читання та запису
  • Supabase Edge Functions для складних випадків – адміністративне SDK (безпечний обхід RLS), виклики сторонніх сервісів, багатокрокові процеси
Frontend Apollo Client
    │
    ├── /graphql/v1  →  Supabase pg_graphql  (standard CRUD)
    │
    └── /functions/complex-operation  →  Supabase Edge Function (business logic)
                                          → calls Supabase admin SDK internally

Ви зберігаєте швидкість та простоту обслуговування прямого режиму для більшості трафіку, а гнучкість типу бекенду залишаєте для операцій, які її дійсно потребують — без необхідності створювати повний сервер, який працює постійно для всього.