首页 / 文章 / Supabase GraphQL Direct与自建Apollo后端对比

Supabase GraphQL Direct与自建Apollo后端对比

将 Supabase 直接提供的 pg_graphql 与独立的 Apollo 服务器进行比较——包括身份验证、RLS、订阅功能,以及何种混合式 Edge Function 路径更具优势。

1853 词

GraphQL是一种查询语言,而非运行时环境。你可以通过多个位置来提供它:

  • Supabase内置的pg_graphql——由PostgreSQL生成架构,由Supabase托管,几乎无需配置
  • 专用的后端——你自己拥有并部署的运行Apollo Server、Hasura、Pothos或类似服务的Node、Python或Go应用

无论哪种方式,前端都可以使用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 Server,无需手动编写解析器,也无需二次部署。API接口就是数据库架构本身。

架构示意图

方案A:直接使用Supabase

浏览器直接与Supabase GraphQL通信。认证通过Supabase JWT实现,行级安全则作为访问控制机制。

方案B:独立后端

浏览器与您的GraphQL服务器通信。解析器通过管理SDK或SQL连接到Supabase(或其他数据源),由您自行定义类型和字段。

直观对比

直接模式旨在无需托管API即可实现CRUD操作;独立模式则侧重于自定义字段、多服务关联以及GraphQL原生的订阅功能。两者使用相同的Apollo客户端模式,但在架构设计和策略管理上有所不同。

Supabase直接模式的实际应用

工作原理

在启动时,pg_graphql会分析PostgreSQL数据库并生成完整的GraphQL API。表会被转换为集合类型,其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. 若没有Edge Functions,便难以通过第三方REST调用为单个解析器添加功能。
  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}`,
};

Admin 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

直接模式无需进行令牌交换——Supabase JWT 同时充当登录凭证和数据库访问权限。而独立后端虽然会增加一层处理步骤,但能让你更灵活地控制权限声明和有效期。

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

自定义模式可以在一个响应中简化嵌套结构、连接不同服务并计算派生字段,而直接模式则仅返回表中的数据。

4. 业务规则的位置

Supabase-direct:

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

示例:在插入数据前于前端检查网络接口卡/邮箱是否重复:

// 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 客户端本身无法接收实时推送。

独立后端 可以通过 WebSocket 提供真正的 GraphQL 订阅功能:

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
  • 对于那些处理起来比较棘手的10%场景,使用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

对于大多数流量,你可以保持直连模式的高速度与低维护成本,同时为真正需要灵活性的操作保留后端式的灵活性——无需为所有场景都搭建全时运行的服务器。