Supabase GraphQL Direct проти власного сервера Apollo
Порівняйте pg_graphql безпосередньо з Supabase та окремим сервером Apollo — автентифікація, RLS, підписки та ситуації, коли перевагу має гібридний шлях Edge Function.
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.
Обмеження прямого підходу
- Для користувацьких полів, яких немає в БД, потрібні функція чи представлення.
- Для обчислюваних/віртуальних полів потрібні обчислювані стовпці чи представлення.
- Розширення одного резолвера за допомогою виклику REST від сторонньої системи є складним без Edge Functions.
- Багатоетапні робочі процеси з можливістю скасування виконання реалізуються через функції БД або на фронтенді.
- Найменування відповідають конвенціям
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
Ви зберігаєте швидкість та простоту обслуговування прямого режиму для більшості трафіку, а гнучкість типу бекенду залишаєте для операцій, які її дійсно потребують — без необхідності створювати повний сервер, який працює постійно для всього.