Supabase GraphQL Direct与自建Apollo后端对比
将 Supabase 直接提供的 pg_graphql 与独立的 Apollo 服务器进行比较——包括身份验证、RLS、订阅功能,以及何种混合式 Edge Function 路径更具优势。
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内部完成。
直接处理的局限性
- 数据库中不存在的自定义字段需要通过函数或视图来实现。
- 计算型/虚拟字段则需要借助计算列或视图来生成。
- 若没有Edge Functions,便难以通过第三方REST调用为单个解析器添加功能。
- 包含回滚机制的多步骤工作流需要实现于数据库函数或前端代码中。
- 命名需遵循
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
对于大多数流量,你可以保持直连模式的高速度与低维护成本,同时为真正需要灵活性的操作保留后端式的灵活性——无需为所有场景都搭建全时运行的服务器。