Supabase 是什么
Supabase 是一个开源的 Backend-as-a-Service(BaaS)平台,定位是 Firebase 的开源替代品。它以 PostgreSQL 为核心,在数据库之上提供了一套完整的后端服务:
| 服务 | 能力 |
|---|---|
| Database | 托管的 PostgreSQL,通过 PostgREST 自动暴露 REST API |
| Auth | 用户认证与 JWT 管理,支持多种登录方式 |
| Storage | 兼容 S3 协议的对象存储,用于图片、文件上传 |
| Realtime | 基于 WebSocket 的实时数据推送,监听数据库变更 |
| Edge Functions | 基于 Deno 的 Serverless 函数,处理自定义业务逻辑 |
和 Firebase 的核心差异在于数据库。 Firebase 用的是 NoSQL(Firestore / Realtime Database),Supabase 用的是关系型数据库 PostgreSQL。对于有复杂查询、多表关联、事务需求的应用,PostgreSQL 的表达能力远超 NoSQL。
为什么选择 Supabase
不需要自己维护后端服务器。 传统模式下,一个小程序需要一套 Node.js/Java 后端来处理 CRUD、认证、文件上传,需要服务器运维、数据库运维、接口开发。Supabase 把这些全部托管——数据模型设计好,配上 Row Level Security 策略,客户端可以直接查询数据库,不需要为每个接口写 controller。
Row Level Security 让数据库本身承担鉴权。 传统后端的鉴权逻辑分散在每个接口里,容易遗漏。Supabase 的 RLS 把权限规则定义在数据库表上,任何路径的访问都受同一套规则约束,安全边界更清晰。
开源可自托管。 如果以后需要迁出 Supabase 云服务(合规要求、成本控制),可以用 Supabase 的 Docker 镜像在自己的服务器上运行完全相同的服务栈,不存在厂商锁定问题。
免费计划对初期项目够用。 免费计划包含 500MB 数据库、1GB 文件存储、50,000 月活用户,足以支撑一个社区类小程序从零到初期规模。
小程序中 Supabase 承担的职责
小程序使用 Supabase 作为唯一的后端:
- Database + PostgREST:所有业务数据的增删查改(物品发布、用户资料、收藏、地址等)
- Auth:用户身份管理,配合 Edge Function 实现微信 openid 登录
- Storage:用户头像、物品图片、师傅作品图片的上传与访问
- Edge Functions:微信登录验证(
wechat-login)、账号注销(delete-account)
项目没有自建任何后端服务器,所有需要服务端执行的逻辑(需要保护密钥的操作)都通过 Edge Function 处理。
为什么官方 SDK 无法运行
@supabase/supabase-js 是为浏览器和 Node.js 设计的,它依赖的每一个底层 API 在微信小程序的逻辑层里都不存在。
微信小程序运行在一个双线程沙箱中:渲染层(Webview)负责 UI,逻辑层(JSCore/V8)负责 JavaScript 执行。两者通过消息机制通信,逻辑层被刻意隔离在一个没有 DOM、没有标准浏览器 API 的环境里。这个设计不是疏忽,是微信的主动选择——统一网络请求审计、防止 DOM 操作、保证沙箱安全。
SDK 的四个断点分别是:
-
fetch不存在。postgrest-js和auth-js的所有 HTTP 调用都通过globalThis.fetch发出。小程序没有fetch,运行即抛ReferenceError。你可以写一个基于wx.request的 polyfill,但这只是第一层问题,后面还有三个。 -
localStorage不存在。auth-js用localStorage持久化 session。小程序的持久化 API 是wx.getStorageSync/wx.setStorageSync,两套接口完全不同。auth-js提供了storage自定义配置项,理论上可以注入适配器解决,但这需要深入 SDK 内部,维护成本高。 -
WebSocket构造函数不兼容。realtime-js用标准new WebSocket(url)建连接,小程序的入口是wx.connectSocket(),返回SocketTask对象。SocketTask的接口与标准WebSocket完全不同——事件绑定是函数调用(task.onMessage(fn))而不是属性赋值(ws.onmessage = fn)。 -
FormData/Blob残缺。storage-js用FormData上传文件,小程序文件上传必须走wx.uploadFile,这是两套完全不同的上传机制。
四个断点加在一起,polyfill 的复杂度已经远超自己封装一个适配层。
supabase-wechat-stable-v2:已封装好的适配层
理解了四个断点之后,自己实现适配层是可行的,但有一个社区维护的现成方案:supabase-wechat-stable-v2。
这个包的本质是将 @supabase/supabase-js 的四个底层依赖全部替换为微信等价实现:
| 官方 SDK 依赖 | 替换为 |
|---|---|
fetch | wx.request |
localStorage | wx.getStorageSync / wx.setStorageSync |
WebSocket 构造函数 | wx.connectSocket |
FormData 上传 | wx.uploadFile |
替换完成后,对外暴露与官方 @supabase/supabase-js v2 完全相同的 API——createClient、from().select().eq()、auth.signIn()、storage.upload() 等写法不变,只是底层网络层换成了微信专有 API。
安装与构建 npm
npm install supabase-wechat-stable-v2
安装后,必须在微信开发者工具中执行「工具 → 构建 npm」,将 node_modules 中的包编译到 miniprogram_npm 目录。微信小程序无法直接引用 node_modules,必须经过这一步构建。
phoenix 的兼容性问题与修复
supabase-wechat-stable-v2 依赖链中有一个隐患:
supabase-wechat-stable-v2
└── @supabase/realtime-js
└── phoenix(Phoenix Channel 的 JS 实现)
什么是 Phoenix?
Phoenix 最初是一个基于 Elixir 语言的著名 Web 框架,以其处理超高并发的 WebSocket 实时通信能力(Phoenix Channels)而闻名。Supabase 后端的 Realtime(实时推送)服务就是基于 Elixir 和 Phoenix 构建的。因此,Supabase 的前端 JS SDK 必须引入 phoenix 这个 npm 包(它的 JS 客户端),才能使用约定的协议与后端建立 WebSocket 通信。
在小程序中会用到吗?
- 绝大多数普通应用用不到。 如果你的小程序只做常规的增删查改(Database)和登录注册(Auth),并不会用到 Realtime,自然也就不需要跑
phoenix的代码。 - 仅在特定强实时场景需要。 只有当你要做“聊天室”、“实时数据大屏推送”、“多人在线协同协作”这类需要“数据库一有变化立马主动推给前端”的功能时,才会触发 Supabase Realtime 和
phoenix。
phoenix v1.x 的 CJS 文件包含微信 JS 解析器不支持的语法,导致「构建 npm」时报错。
如果在小程序中不使用 Supabase Realtime,可以通过 scripts/patch-deps.js 在 postinstall 钩子中自动将 phoenix 入口替换为一个空 stub——保留 Socket / Channel / Presence 的类结构但所有方法为空实现,让 @supabase/realtime-js 能正常引入而不报错。npm install 后这个 patch 会自动执行,不需要手动干预。
如果你的项目需要使用 Realtime,则不能使用这个 patch,需要寻找兼容微信 JS 引擎的 phoenix 版本。
三种接入路径的本质权衡
路径 A:REST 裸调。 直接用 wx.request 调用 PostgREST REST API,不引入任何 SDK。零依赖,完全可控,但你需要手动处理所有认证逻辑、错误格式和查询参数的序列化。适合接口调用极少的轻量项目,一旦项目规模增长,维护成本急剧上升。
路径 B:使用 supabase-wechat-stable-v2。 安装这个包后,调用方式与官方 SDK 完全一致,无需自己实现适配层。可以在此基础上再包一层业务封装,处理认证拦截和 token 刷新逻辑。
路径 C:云函数代理。 所有 Supabase 请求经微信云函数或自建 Node.js 服务转发,服务端使用官方 SDK。优点是可以用 SDK 的完整功能,缺点是每次请求多一跳延迟,实时功能(Realtime)很难通过代理透传,架构复杂度明显上升。
路径 B 是大多数项目的最优解,小程序的所有实现均可基于这条路径。
PostgREST 协议:理解它才能封装好
Supabase 的数据库 API 不是 Supabase 自己设计的,而是 PostgREST——一个将 PostgreSQL schema 自动映射为 RESTful HTTP API 的开源项目。理解 PostgREST 的协议设计,是封装好适配层的前提。
PostgREST 的核心思路是:HTTP 动词对应 SQL 操作,URL 参数对应查询条件,HTTP Header 携带行为控制指令。
具体映射关系:
| HTTP | SQL | 说明 |
|---|---|---|
GET /table | SELECT | 查询,条件通过 URL 参数传递 |
POST /table | INSERT | 插入,数据在 request body |
PATCH /table?filters | UPDATE ... WHERE | 更新指定行 |
DELETE /table?filters | DELETE ... WHERE | 删除指定行 |
POST /rpc/function_name | SELECT function_name(...) | 调用数据库函数(RPC) |
查询参数遵循 列名=操作符.值 的格式。例如 ?status=eq.published 对应 WHERE status = 'published',?created_at=gte.2024-01-01 对应 WHERE created_at >= '2024-01-01'。这个设计让 URL 具备了表达完整 SQL WHERE 子句的能力,同时保持 RESTful 风格。
Prefer 这个 Header 是 PostgREST 的行为控制开关,最重要的两个值:
Prefer: return=representation:让写操作(INSERT/UPDATE/DELETE)返回操作后的完整行数据,而不是默认的 HTTP 204 空响应Prefer: resolution=merge-duplicates:配合POST,在主键冲突时执行 UPDATE(实现 upsert 语义)
理解了这一层,封装的逻辑就清晰了:适配层需要做的事情是把链式 API 调用翻译成符合 PostgREST 协议的 HTTP 请求。
小程序的封装架构:utils/supabase.js
为什么还需要自己封装?
supabase-wechat-stable-v2 已经完美解决了底层网络适配(将 fetch 换成 wx.request 等),让 SDK 能够在小程序环境中正常运行。但仅仅能跑起来是不够的,如果直接暴露底层的 createClient 给各个页面组件使用,会导致代码极其冗余。
因此,我们需要在它之上再加一层业务架构封装(即 utils/supabase.js),主要用来处理以下三件事:
1. 创建客户端单例
// utils/supabase.js(伪代码)
import { createClient } from 'supabase-wechat-stable-v2'
import config from '~/config'
const _client = createClient(config.supabaseUrl, config.supabaseAnonKey)
createClient 与官方 SDK 接口完全相同,传入项目 URL 和 anon key 即可。_client 是私有的,不直接对外暴露,所有业务访问都经过下面的封装层。
2. 认证拦截:_unauthorizedBuilder
当用户未登录时,_client.from() 仍然可以发出查询(以 anon 角色),但大多数数据接口需要登录。与其让请求打到服务端被 RLS 拒绝,不如在客户端就拦截,立刻返回统一的 UNAUTHENTICATED 错误,减少无效网络请求。
_unauthorizedBuilder() 返回一个假的查询构建器:它支持所有链式方法(.select()、.eq()、.order() 等),每个方法都返回自身以支持继续链式调用,但实际上不发出任何网络请求。当链式调用被 await 时(通过 then / catch / finally 实现 thenable),直接返回 { data: null, error: { message: '未登录', code: 'UNAUTHENTICATED' } }。
这个模式的价值在于:业务代码的写法完全一致,const { data, error } = await supabase.from('table').select('*'),不需要在每次调用前手动检查登录状态。所有拦截逻辑集中在一处。
3. skipAuthCheck 逃生口
某些场景需要在未登录状态下读取公开数据,例如从分享链接进入的物品详情页。对这类场景,supabase.from() 支持传入 { skipAuthCheck: true } 跳过认证拦截,直接走 _client.from():
// 伪代码:公开只读查询,不需要登录
const { data } = await supabase.from('table', { skipAuthCheck: true })
.select('id, title, price')
.eq('id', itemId)
.single()
supabase.auth 和 supabase.storage 始终透传 _client 的对应属性,不做认证拦截——登录操作本身就是在未登录状态下发起的,拦截 auth 会造成死锁。
supabaseCall:token 过期时的自动续期
utils/supabase-call.js 解决另一个运行时问题:token 在请求执行中途过期。
_unauthorizedBuilder 只能拦截"本地没有 token"的情况。如果本地有 token 但已过期,请求会发出去,服务端(PostgREST)返回 PGRST301(JWT expired)错误。这时需要自动刷新 token 并重试原请求。
supabaseCall 是一个高阶函数,接受一个返回 Supabase 查询的函数(而不是 Promise),执行后检查 error 是否为认证类错误。如果是,触发静默重登(silentRelogin),成功后用新 token 重新执行同一个函数。整个过程用户无感知:
// 伪代码:业务层用法
const { data, error } = await supabaseCall(() =>
supabase.from('table').select('*').eq('user_id', userId)
)
注意 fn 必须是函数而不是已执行的 Promise。这是因为 Promise 一旦创建就已经在执行,无法"重试"。把查询包在函数里,才能在 token 刷新后再次调用它,发出第二次请求。
认证错误的判断覆盖了所有可能的表现形式:HTTP 401、PostgREST 错误码 PGRST301、以及错误消息中包含 jwt expired / not authenticated 等关键词。因为不同场景(from()、auth.getUser()、storage.upload())的错误格式不完全一致,需要多路匹配。
关键实现细节
认证 Header 的动态注入。 supabase-wechat-stable-v2 内部通过 supabase.auth.setSession() 管理 session 状态,每次请求时自动从内存中读取当前 session 的 access_token 注入 Authorization Header。这意味着调用 setSession() 是同步 SDK 状态的关键动作——登录成功后、token 刷新后,都需要调用 setSession() 告知 SDK 当前的 token,否则 SDK 的内部状态会与 wx.storage 中的 token 不一致。
合法域名配置。 小程序调用外部 API 前,必须在微信公众平台的「开发设置 → 服务器域名」中添加 Supabase 项目的域名。request 合法域名、socket 合法域名(Realtime)、uploadFile 合法域名(Storage 上传)、downloadFile 合法域名各需单独配置。本地开发阶段可在开发者工具中勾选「不校验合法域名」临时跳过,但上线前必须完整配置。
与官方 SDK 的行为差异
supabase-wechat-stable-v2 虽然 API 对齐官方 SDK,但由于底层实现的限制,少数功能行为不同:
- 无流式响应。
wx.request不支持流式读取,只能等待完整响应返回。 wx.uploadFile限制。 Storage 的文件上传底层走wx.uploadFile,该 API 只支持multipart/form-data,不支持直接上传二进制流(ArrayBuffer)。如果需要上传非文件类型的二进制数据,需要先写入本地临时文件再上传。- Realtime 受限。 因 phoenix 兼容性问题可能需要使用 stub,导致 Realtime 功能不可用。如需使用,参见前文的 phoenix 问题说明。