用 Refine 与 Supabase 在 5 分钟内搭建企业级 Admin Panel
导读
本篇技术指南围绕 documentation/blog/2025-11-12-admin-panel-in-5-minutes.md 的核心思路展开:以 Supabase 作为开箱即用的 PostgreSQL 后端(认证、自动生成的 REST API、实时订阅、文件存储),以 Refine 作为前端框架,把"连接数据库 → AI 分析 schema → 生成生产级 CRUD 页面 → 接通认证与实时更新"这一整套流程压缩到几分钟内。读完本文,你将掌握:如何用 Refine 官方启动向导连接 Supabase、如何让 AI 分析数据库表结构并生成可定制的列表/创建/编辑页面,以及 Refine 内置 Supabase data provider、live provider 与 auth provider 的底层实现原理与关键配置。
为什么是 Supabase?
Supabase 是一个构建在 PostgreSQL 之上的后端即服务(Backend-as-a-Service)平台。对你来说,它直接提供了一整套 admin panel 必需的基础设施:
- 真实的 PostgreSQL 数据库,而不是某种私有数据格式,schema、外键、约束全部可用 SQL 管理;
- 自动生成的 REST API(基于 PostgREST),表一建好就有对应的 CRUD 端点;
- 内置认证(Auth),支持邮箱密码、OAuth 社交登录、会话持久化;
- 实时订阅(Realtime),数据库变化可以实时推送到前端;
- 文件存储(Storage),可直接对接图片、附件上传。
对 admin panel 场景而言,Supabase 最省心的地方在于"什么都不缺"——你不需要花一天时间搭建认证、不需要手动设计 API 端点,这些都已经就绪。配合 Refine,你只需要把前端指向你的 Supabase 数据库,剩下的 CRUD 页面、表单、路由都由 Refine 生成。
Refine 如何把"项目"变成"几分钟"
Refine 是一个用于构建内部工具、Admin Panel、Dashboard 与 B2B 应用的 React 框架。而本博客描述的工作流,是 Refine 的 AI 辅助生成能力:你把数据库连上去,它分析你的 schema,然后你像和一名精通 Refine Core 的开发者对话一样,用聊天或建议选项直接生成页面。生成结果不是一次性原型代码,而是生产级、TypeScript、遵循最佳实践、可继续定制的 React 代码。
这种能力本质上建立在 Refine 成熟的包体系之上。以 Supabase 集成为例,仓库中的 packages/supabase 包集中提供了三样东西:
| 模块 | 文件 | 作用 |
|---|---|---|
| data provider | packages/supabase/src/dataProvider/index.ts | 把 Refine 的 CRUD 数据钩子映射到 Supabase/PostgREST API |
| live provider | packages/supabase/src/liveProvider/index.ts | 基于 Supabase Realtime 的实时订阅 |
createClient |
packages/supabase/src/index.ts | 再导出 @supabase/supabase-js 客户端工厂 |
也就是说,AI 生成的页面背后,是这些经过反复测试的 provider 在负责"接线",这也是它能做到又快又稳的原因。
Step 1:创建项目并连接 Supabase
在 Refine 的启动页面(refine.dev/start)点击 Connect to Supabase,然后授权并选择你要用作后端数据库的 Supabase 项目即可。整个过程不需要写任何初始化代码——连接建立后,Refine 会拿到你的数据库访问凭证。
传统手工方式的等价物,是仓库示例 examples/data-provider-supabase 中由 CLI 自动生成的 supabaseClient.ts:
// examples/data-provider-supabase/src/utility/supabaseClient.ts
import { createClient } from "@refinedev/supabase";
const SUPABASE_URL = "https://iwdfzvfqbtokqetmbmbp.supabase.co";
const SUPABASE_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYzMDU2NzAxMCwiZXhwIjoxOTQ2MTQzMDEwfQ._gr6kXGkQBi9BM9dx5vKaNKYj_DJN1xlkarprGpM_fU";
export const supabaseClient = createClient(SUPABASE_URL, SUPABASE_KEY, {
db: {
schema: "public", // 可通过 data hooks 的 meta.schema 覆盖
},
auth: {
persistSession: true,
},
});
值得注意的配置点:
db.schema:默认使用publicschema,可以在单个 data hook 上通过meta.schema覆盖(见下文"meta 参数"一节)。auth.persistSession:会话持久化开关,决定刷新页面后用户是否保持登录。- 凭证安全:建议把 URL 和 Key 放进环境变量,而不是硬编码在源码里。
随后在 App.tsx 中把客户端注册为 Refine 的 data provider:
import { Refine } from "@refinedev/core";
import { dataProvider } from "@refinedev/supabase";
import { supabaseClient } from "./utility/supabaseClient";
function App() {
return (
<Refine
dataProvider={dataProvider(supabaseClient)}
// ...
>
{/* ... */}
</Refine>
);
}
export default App;
底层:data provider 是如何工作的
从源码看,dataProvider(supabaseClient) 返回一个完整的、实现全部数据方法(getList、getMany、create、createMany、update、updateMany、getOne、deleteOne、deleteMany)的对象,见 packages/supabase/src/dataProvider/index.ts。
几个关键实现细节:
- schema 支持:每个方法都会检查
meta?.schema,若存在则调用supabaseClient.schema(meta.schema)切换到指定 schema(dataProvider/index.ts#L16-L18)。 - 分页:
getList默认currentPage = 1, pageSize = 10,在mode === "server"时通过.range()做服务端分页(dataProvider/index.ts#L24-L26)。 - 排序:支持对关联表的字段排序,例如
categories.title会被拆分为外键表与字段,并调用.order(field, { foreignTable })(dataProvider/index.ts#L28-L43)。 - 计数:
getList默认使用count: "exact",可通过meta.count改为"estimated"以提升大表性能。 - 主键列名:默认按
id列查询,可通过meta.idColumnName指定自定义主键。 getApiUrl与custom:这两个方法在 Supabase data provider 中不实现,会直接抛错(dataProvider/index.ts#L256-L262),因为 Supabase 通过 PostgREST 已覆盖全部所需数据操作。
底层:过滤器是如何翻译的
Refine 的通用 CrudFilter 会在 packages/supabase/src/utils/generateFilter.ts 中被翻译成 Supabase 查询:
| Refine 操作符 | Supabase 方法 | 语义 |
|---|---|---|
eq / ne |
query.eq / query.neq |
等于 / 不等于 |
in |
query.in |
属于集合 |
gt / gte / lt / lte |
query.gt 等 |
大小比较 |
between |
query.gte(...).lte(...) |
区间(要求值长度为 2,否则抛错) |
contains / containss |
query.ilike / query.like |
模糊匹配(大小写不敏感 / 敏感) |
null |
query.is(field, null) |
为空 |
startswith / endswith |
query.ilike |
前缀 / 后缀匹配 |
or |
query.or |
组合 OR 条件 |
and |
抛错 | 不支持 |
Step 2:让 AI 分析你的数据库
连接 Supabase 项目后,点击 Continue,Refine 的 AI 会分析你的 schema,并把分析结果展示给你确认。这一步你会看到:
- 所有表及其结构;
- 每列的类型(text、数字、日期、UUID 等);
- 表与表之间的外键关系;
- 约束与默认值。
在最终点击 Continue 之前,值得最后过一遍这些信息——因为后续生成的页面、表单字段、下拉关联(如 posts -> categories)全部由这份分析驱动。schema 里的外键关系越清晰,生成的关联字段就越准确。
Step 3:生成生产级代码
分析完成后,你会进入项目起始页,AI 会给出几个建议动作(例如"创建带数据表格的 Employee 列表页")。你可以直接用聊天框下达自己的指令,也可以直接采纳建议。
以本博客的演示为例:先选择创建 Employee 列表页。不到一分钟,AI 就生成一个完全可浏览、可交互的列表资源页——表格、排序、筛选、操作列一应俱全。不满意布局或数据展示方式?直接告诉 AI 调整,Refine 负责接线。如果一路跟随建议,一个完整的 admin panel 在一小时内就能成型。
手工等价物:一个典型的 List 页面
如果不用 AI,手工创建列表页的典型形态(来自 documentation/docs/data/packages/supabase/index.md 与 examples/data-provider-supabase/src/pages/posts/list.tsx):
import { List, useTable, EditButton, ShowButton } from "@refinedev/antd";
import { Table, Space } from "antd";
export const PostList: React.FC = () => {
const { tableProps, sorters } = useTable<IPost>({
sorters: { initial: [{ field: "id", order: "asc" }] },
meta: {
select: "*, categories(title)", // 关联 categories 表并取 title 字段
},
});
return (
<List>
<Table {...tableProps} rowKey="id">
<Table.Column key="id" dataIndex="id" title="ID" sorter />
<Table.Column key="title" dataIndex="title" title="Title" sorter />
<Table.Column
key="categoryId"
dataIndex={["categories", "title"]}
title="Category"
/>
<Table.Column<IPost>
title="Actions"
dataIndex="actions"
render={(_, record) => (
<Space>
<EditButton hideText size="small" recordItemId={record.id} />
<ShowButton hideText size="small" recordItemId={record.id} />
</Space>
)}
/>
</Table>
</List>
);
};
AI 生成页面时,本质上就是按同样的模式(useTable + meta.select + Refine 的 UI 组件)为你铺好初始代码,然后把布局微调交给对话式交互完成。
认证:authProvider 与 Supabase Auth
AI 生成的工程同样包含完整的认证闭环。Refine 的 auth provider 概念允许接入任意认证服务;对 Supabase,CLI 会生成一个 authProvider.ts,其内部直接调用 Supabase Auth API:
| auth provider 方法 | 底层 Supabase API |
|---|---|
login |
auth.signInWithOAuth(provider)(社交登录)或 auth.signInWithPassword({ email, password }) |
register |
auth.signUp({ email, password }) |
forgotPassword |
auth.resetPasswordForEmail(email, { redirectTo }) |
updatePassword |
auth.updateUser({ password }) |
logout |
auth.signOut() |
check |
auth.getSession() 判断是否存在有效会话 |
getPermissions |
auth.getUser() 返回用户角色 |
getUserIdentity |
auth.getUser() 返回用户信息(以 email 作为 name) |
完整的参考实现在 documentation/docs/data/packages/supabase/index.md 的 "Understanding the Auth Provider" 一节,示例工程见 examples/data-provider-supabase/src。
注册 authProvider 并接入 AuthPage
import { Refine, Authenticated } from "@refinedev/core";
import { AuthPage } from "@refinedev/antd";
import routerProvider, { CatchAllNavigate } from "@refinedev/react-router";
import authProvider from "./authProvider";
function App() {
return (
<Refine routerProvider={routerProvider} authProvider={authProvider}>
<Routes>
<Route
element={
<Authenticated fallback={<CatchAllNavigate to="/login" />}>
<ThemedLayout>
<Outlet />
</ThemedLayout>
</Authenticated>
}
>
<Route path="/posts" element={<PostList />} />
</Route>
<Route path="/login" element={<AuthPage type="login" />} />
<Route path="/register" element={<AuthPage type="register" />} />
{/* forgot-password / update-password 同理 */}
</Routes>
</Refine>
);
}
<AuthPage> 是开箱即用的登录/注册/忘记密码/更新密码页面,它的按钮会自动绑定到 authProvider 对应方法上。
添加 Google 社交登录
只需在 <AuthPage> 上配置 providers:
<AuthPage
type="login"
providers={[
{
name: "google",
label: "Sign in with Google",
icon: <GoogleOutlined style={{ fontSize: 18, lineHeight: 0 }} />,
},
]}
/>
同时在 Supabase 控制台(Authentication → Settings → Auth Providers)启用 Google Auth 并填入你的 Google OAuth 凭证。登录成功后,Refine 会把用户重定向回应用。
实时更新:Supabase Realtime + Refine Live Provider
Supabase 内置 Realtime 能力:当记录被创建、更新或删除时,变更可以实时推送到前端。Refine 的 Supabase live provider 在 packages/supabase/src/liveProvider/index.ts 中封装了订阅逻辑:
- 事件类型映射:
INSERT → created、UPDATE → updated、DELETE → deleted(见 packages/supabase/src/types/index.ts); - 订阅通过
channel("resources/{resource}")建立,内部注册postgres_changes监听; - 支持通过
params.ids过滤(只对已存在的 id 触发回调),以及通过params.filters生成 Realtime 过滤串(liveProvider/index.ts#L52-L80); - 需要注意:Supabase Realtime 目前只支持单个
filter字符串,多个过滤器时仅使用第一个并打印警告。
启用方式——在 <Refine> 上注册 live provider,并把 liveMode 设为 auto:
import { Refine } from "@refinedev/core";
import { liveProvider } from "@refinedev/supabase";
import { supabaseClient } from "./utility/supabaseClient";
function App() {
return (
<Refine
liveProvider={liveProvider(supabaseClient)}
options={{ liveMode: "auto" }}
// ...
/>
);
}
liveMode: "auto" 意味着列表、详情等页面在数据变化时自动刷新;如果只想在特定页面手动控制,也可以用 "manual" 配合 onLiveEvent 自行处理(编辑页的"数据已过期,请刷新"提示就是这种模式)。
通过 meta 深度控制数据查询
Supabase data provider 的灵活性很大程度上来自 meta 参数。以下用法在 documentation/docs/data/packages/supabase/index.md 中有完整说明,也都能在 packages/supabase/src/dataProvider/index.ts 中找到对应实现:
select —— 指定返回字段
默认查询方法用 *(全部字段),通过 meta.select 可以裁剪:
useList({
resource: "posts",
meta: { select: "title, content" },
});
select —— 一对多关联
posts -> categories 关系下,直接取关联表的字段:
useTable<IPost>({
resource: "posts",
meta: { select: "*, categories(title)" },
});
select —— 多对多关联
movies <-> categories_movies <-> categories 场景下用 !inner 强制内连接:
useTable({
resource: "movies",
meta: { select: "*, categories!inner(name)" },
});
idColumnName —— 自定义主键列
表主键不叫 id 时(例如 post_id):
useMany({
resource: "posts",
ids: [1, 2],
meta: { idColumnName: "post_id" },
});
schema —— 自定义 schema
默认使用 supabaseClient 里配置的 schema,可在单个钩子上覆盖:
useTable({
resource: "posts",
meta: { schema: "foo" },
});
深层过滤
按关联表字段过滤时,meta.select 必须用 !inner 才能让过滤生效:
useTable({
resource: "posts",
filters: {
initial: [{ field: "categories.title", operator: "eq", value: "Beginning" }],
},
meta: { select: "*, categories!inner(title)" },
});
count —— 提升 getList 性能
默认 exact 精确计数可能拖慢大表请求,可改用估算:
useList({
resource: "posts",
meta: { count: "estimated" },
});
一个已知限制
Supabase 客户端本身不支持对关联表数据做 Realtime 订阅。如果列表用了 select: "*, categories(title)",关联表变化不会自动触发刷新,需要手动订阅并 refetch:
import { useTable, useSubscription } from "@refinedev/core";
export const PostList = () => {
const table = useTable({
meta: { select: "*, categories(title)" },
});
useSubscription({
channel: "categories",
types: ["*"],
onLiveEvent: () => {
table.tableQuery.refetch();
},
});
return <>{/* ... */}</>;
};
为什么这套工作流值得采用
传统的 admin panel 开发流程是:初始化项目 → 安装依赖 → 配置 data provider → 为每个资源分别编写 list / create / edit / show 页面 → 接路由……对每个资源重复一遍。这不难,但耗时且重复。
Refine + Supabase 的 AI 工作流改变了这个过程:
- 真正快速交付:原本要数天的内部工具,一个下午就能完成;客户 Dashboard 半天内上线;需要快速迭代的 MVP 不再被脚手架拖累。
- 把时间花在关键处:CRUD、表格、表单是"已解决的问题"。AI 负责脚手架,你把精力放在业务逻辑、自定义工作流、集成等真正差异化部分。
- 提升生产力而非替代人:AI 是处理重复部分的结对程序员。架构决策、自定义逻辑仍然由你掌控,只是不必再第 N 次重写数据表格。
从 packages/supabase 的源码可以看到,这套"分钟级"体验背后是扎实的工程基础:data provider 完整覆盖 CRUD 与过滤语义,live provider 封装 Realtime 事件映射,auth provider 对接完整认证流程。你可以在生成代码后继续深入定制——例如参考 examples/data-provider-supabase 的完整示例工程,查看带文件上传(supabaseClient.storage)、关联下拉、实时刷新提示的真实实现。
如果你想复现整个流程,只需要:一个 Supabase 免费项目 + Refine 启动向导 → 连接数据库 → AI 分析 schema → 生成页面 → 按需接入认证与实时订阅。从连接数据库到拥有可用的 admin panel,整个过程可以压缩在几分钟之内——这就是本博客标题"5 分钟"的含义。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00