Главная / Статьи / 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 Server, Hasura, Pothos или аналогичными решениями, который находится в вашем владении и развернут вами

В любом случае фронтенд может использовать Apollo Client. Изменяется лишь местоположение схемы GraphQL и тот, кто ею управляет.

Система Vision Expert System выбирает путь через Supabase:

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

Нет процесса Express, нет Apollo Server, нет ручно написанных резолверов, нет дополнительного развертывания. Интерфейс API представляет собой схему базы данных.

Эскизы архитектуры

Подход А: прямое использование Supabase

Браузер взаимодействует непосредственно с GraphQL-сервером Supabase. Аутентификация осуществляется с помощью JWT от Supabase, а защита на уровне строк — основным механизмом контроля доступа.

Подход B: Отдельный бэкенд

Браузер взаимодействует с вашим сервером GraphQL. Резолверы обращаются к Supabase (или другим источникам) с помощью 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());

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

Ограничения прямого подхода

  1. Для пользовательских полей, отсутствующих в базе данных, требуются функции или представления.
  2. Для вычисляемых/виртуальных полей необходимы вычисляемые столбцы или представления.
  3. Дополнение одного резолвера вызовом REST-сервиса сторонней компании затруднено без использования Edge Functions.
  4. Многоэтапные рабочие процессы с возможностью отката реализуются либо в функциях базы данных, либо на фронтенде.
  5. Нумерация соответствует конвенциям pg_graphql — подчеркивания сохраняются, поэтому используются такие названия, как head_officeCollection.

Практическое применение отдельного бэкенда

Как это работает

Вы запускаете Apollo Server (или аналогичный сервис), создаете схему и пишете резолверы, которые вызывают функции администрирования Supabase, 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, а не подписки 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 для решения сложных случаев в 10% сценариев: административный 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

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