返回博客列表
技术文章Supabase微信小程序架构设计

微信小程序接入 Supabase 全解析

2026年04月🇨🇳 中文

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 的四个断点分别是:

  1. fetch 不存在。 postgrest-jsauth-js 的所有 HTTP 调用都通过 globalThis.fetch 发出。小程序没有 fetch,运行即抛 ReferenceError。你可以写一个基于 wx.request 的 polyfill,但这只是第一层问题,后面还有三个。

  2. localStorage 不存在。 auth-jslocalStorage 持久化 session。小程序的持久化 API 是 wx.getStorageSync / wx.setStorageSync,两套接口完全不同。auth-js 提供了 storage 自定义配置项,理论上可以注入适配器解决,但这需要深入 SDK 内部,维护成本高。

  3. WebSocket 构造函数不兼容。 realtime-js 用标准 new WebSocket(url) 建连接,小程序的入口是 wx.connectSocket(),返回 SocketTask 对象。SocketTask 的接口与标准 WebSocket 完全不同——事件绑定是函数调用(task.onMessage(fn))而不是属性赋值(ws.onmessage = fn)。

  4. FormData / Blob 残缺。 storage-jsFormData 上传文件,小程序文件上传必须走 wx.uploadFile,这是两套完全不同的上传机制。

四个断点加在一起,polyfill 的复杂度已经远超自己封装一个适配层。


supabase-wechat-stable-v2:已封装好的适配层

理解了四个断点之后,自己实现适配层是可行的,但有一个社区维护的现成方案:supabase-wechat-stable-v2

这个包的本质是将 @supabase/supabase-js 的四个底层依赖全部替换为微信等价实现:

官方 SDK 依赖替换为
fetchwx.request
localStoragewx.getStorageSync / wx.setStorageSync
WebSocket 构造函数wx.connectSocket
FormData 上传wx.uploadFile

替换完成后,对外暴露与官方 @supabase/supabase-js v2 完全相同的 API——createClientfrom().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.jspostinstall 钩子中自动将 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 携带行为控制指令

具体映射关系:

HTTPSQL说明
GET /tableSELECT查询,条件通过 URL 参数传递
POST /tableINSERT插入,数据在 request body
PATCH /table?filtersUPDATE ... WHERE更新指定行
DELETE /table?filtersDELETE ... WHERE删除指定行
POST /rpc/function_nameSELECT 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.authsupabase.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 问题说明。