1. 项目概述一个Supabase全栈应用的原型与起点如果你正在寻找一个能快速上手、理解现代全栈开发流程的实战项目那么amha/supabase-demo这个仓库很可能就是你需要的那个“脚手架”。这不是一个复杂的企业级应用而是一个精心设计的、开箱即用的演示项目它清晰地展示了如何利用 Supabase 这套开源的 Firebase 替代方案从前端到后端构建一个具备完整身份认证、实时数据同步和数据库操作能力的 Web 应用。简单来说这个项目就像一份“全栈开发食谱”。它把 Supabase 的核心功能——身份认证Auth、实时订阅Realtime、数据库Database和存储Storage——通过一个具体的应用场景串联起来。你拿到手的不只是一堆零散的代码片段而是一个可以立即运行、并在此基础上进行扩展的完整应用骨架。对于前端开发者想涉足后端逻辑或是全栈新手希望理解前后端如何协同工作这个 Demo 的价值在于它提供了一个“最佳实践”的范本避免了从零搭建时在架构设计和工具链集成上可能踩的无数个坑。我最初接触这个项目时正是想验证 Supabase 是否真如宣传那样能极大提升开发效率。实际跑通之后我的体会是它确实大幅降低了全栈应用特别是需要快速原型验证MVP项目的启动门槛。接下来我将为你深度拆解这个项目的设计思路、技术实现细节并分享在复现和扩展过程中积累的实操经验。2. 项目核心架构与设计思路拆解2.1 为什么选择 Supabase 作为技术栈核心在启动任何项目前技术选型是首要决策。amha/supabase-demo选择 Supabase 作为基石背后有一系列务实的考量。Supabase 本质上是一个基于 PostgreSQL 的开源 BaaS后端即服务平台。与手动搭建 Node.js Express PostgreSQL 的传统后端相比它提供了即时可用的、通过 REST 和 GraphQL API 访问的数据库以及内置的认证、授权、实时功能、存储和边缘函数。选择它的核心理由有三点开发速度、数据一致性和成本控制。对于个人开发者或小团队从零构建一套安全、稳定的用户认证系统、实时订阅机制和文件存储服务需要投入大量非业务逻辑的开发时间。Supabase 将这些基础设施封装成简单的 API 调用让开发者能聚焦于业务逻辑本身。其次由于所有服务数据库、认证、存储都紧密集成在同一个 PostgreSQL 数据库中数据关系和外键约束可以轻松维护避免了微服务架构中常见的数据一致性问题。最后其开源版本可以完全自托管对于学习、内部工具或早期创业项目能够有效控制云服务成本。这个 Demo 项目正是基于这些优势构建的。它没有选择将前端、后端和数据库分离成三个独立项目而是采用了 Supabase 推荐的“一体化”架构前端应用直接通过supabase/supabase-js客户端库与 Supabase 服务通信。这种架构极大地简化了部署和运维复杂度特别适合全栈单人开发或小团队协作。2.2 应用场景与功能模块设计一个 Demo 的价值在于其场景的代表性。amha/supabase-demo通常不会是一个复杂的电商或社交平台而更可能是一个“任务管理看板”或“简易社交帖子墙”。这类场景能集中展示 CRUD增删改查操作、用户权限管理和实时更新。以“任务看板”为例其功能模块设计会围绕以下几个核心展开用户认证模块实现邮箱/密码注册、登录、登出以及用户会话管理。这是所有多用户应用的基础。数据模型与数据库设计在 Supabase 中设计tasks表包含id,title,description,is_complete,user_id外键关联auth.users,created_at等字段。这里会充分利用 PostgreSQL 的行级安全策略RLS。前端数据交互模块使用 Supabase 客户端库进行数据的查询、插入、更新和删除。例如获取当前用户的所有任务创建新任务标记任务完成。实时同步模块对tasks表开启实时订阅当任何任务被创建、更新或删除时所有在线的客户端界面能自动、即时地刷新无需手动轮询。文件存储模块可选但常见允许用户为任务上传附件如图片或文档。这展示了 Supabase Storage 的集成使用。这种设计麻雀虽小五脏俱全。它迫使你去思考和实践如何设计数据库表结构如何通过 RLS 确保用户只能操作自己的数据如何在 UI 上优雅地处理异步数据加载和实时更新这些正是构建更复杂应用的基石。2.3 前端框架与工具链选型分析原项目amha/supabase-demo具体使用哪个前端框架React, Vue, Svelte取决于创建者的偏好但无论哪种其集成模式是相通的。目前社区最活跃的搭配可能是Next.js (React) 或 SvelteKit因为它们都提供了优秀的服务端渲染SSR和静态生成支持能与 Supabase 的认证状态管理很好地结合。以 Next.js (App Router) 为例选型理由如下身份认证集成简便Next.js 的中间件Middleware可以轻松实现基于 Supabase 会话的页面保护路由在服务端验证用户状态提升安全性。服务端数据获取可以在 React 服务器组件中直接使用 Supabase 的服务端客户端安全地获取数据避免将敏感密钥暴露给前端同时提升首屏加载速度。完善的工具链Next.js 内置路由、打包、优化等减少配置负担。配合 TypeScript能为 Supabase 生成的数据库类型提供完美的类型安全支持。项目通常会使用supabase/ssr包来配置适用于 Next.js App Router 的客户端和服务端工具。工具链还包括Tailwind CSS用于快速构建 UI这是目前与 Supabase 演示项目高度流行的搭配因其实用性强、开发效率高。Supabase CLI本地开发神器。用于链接项目、在本地运行 Supabase 服务包括 PostgreSQL 数据库、认证模拟器等、管理数据库迁移Migration。TypeScript强烈推荐。Supabase 可以根据你的数据库表结构自动生成完整的 TypeScript 类型定义实现从数据库到前端的端到端类型安全极大减少运行时错误。这样的选型组合确保了开发体验的流畅性和项目代码的可维护性。3. 环境准备与项目初始化实操3.1 本地开发环境搭建在开始编码前你需要一个准备好的本地环境。首先确保你的机器上安装了Node.js建议 LTS 版本和npm或yarn、pnpm等包管理器。这是运行前端构建工具的基础。接下来最关键的一步是安装Supabase CLI。这是你管理本地和远程 Supabase 项目的瑞士军刀。通过 npm 全局安装npm install -g supabase安装完成后在终端运行supabase --version验证是否成功。之后你需要登录到你的 Supabase 账户如果你打算将项目部署到云上supabase login这个命令会打开浏览器让你完成认证。对于纯本地开发登录不是必须的但为了后续的平滑部署建议先登录。注意Supabase CLI 在首次初始化项目时可能会自动下载并启动一个 Docker 容器用于运行本地的 PostgreSQL 数据库、Kong 网关、Auth 等服务。因此请确保你的系统已安装并运行了Docker Desktop。对于 Windows 用户需要启用 WSL 2 后端以获得最佳体验。3.2 创建并链接 Supabase 项目你有两个起点一是从云端 Supabase 项目开始二是从本地开始。对于学习而言我推荐从本地开始这样你对整个数据结构的控制力更强。初始化本地 Supabase在你的项目根目录下运行supabase init这个命令会创建一个supabase文件夹里面包含config.toml配置文件、migrations文件夹用于数据库版本控制和seed.sql文件用于初始数据。启动本地开发服务supabase start首次运行会花费一些时间下载 Docker 镜像。成功后CLI 会输出一组本地服务的 URL包括本地数据库 URLpostgresql://postgres:postgreslocalhost:54322/postgresAPI URLhttp://localhost:54321Anon Key和Service Role Key用于前端客户端连接。请妥善保存这些信息尤其是anon key公开和service role key绝不可暴露给前端。在 Supabase 云端创建项目可选但推荐前往 Supabase 官网创建一个新项目。创建完成后在项目设置 - API 页面找到项目的 URL 和anon key。链接本地与远程为了将本地的数据库架构同步到云端或者将云端数据拉取到本地你需要建立链接supabase link --project-ref your-project-ref这里的your-project-ref是云端项目 ID可以在云端项目的 API 设置页面找到。执行此命令后你的本地config.toml会更新远程链接信息。3.3 前端项目脚手架搭建假设我们选择 Next.js (App Router) 和 TypeScript。使用官方创建工具快速搭建npx create-next-applatest supabase-demo --typescript --tailwind --app cd supabase-demo然后安装 Supabase 客户端库和 Next.js 辅助库npm install supabase/supabase-js supabase/ssr接下来配置环境变量。创建.env.local文件确保它在.gitignore中填入从本地或云端 Supabase 获取的密钥# 如果你连接本地服务 NEXT_PUBLIC_SUPABASE_URLhttp://localhost:54321 NEXT_PUBLIC_SUPABASE_ANON_KEYyour-local-anon-key # 如果你直接连接云端项目 # NEXT_PUBLIC_SUPABASE_URLhttps://your-project-ref.supabase.co # NEXT_PUBLIC_SUPABASE_ANON_KEYyour-cloud-anon-key实操心得在开发初期我强烈建议先使用本地 Supabase 服务。这样做的好处是完全离线、速度极快、对数据库的“破坏性”操作无成本大不了supabase stop再supabase start重置。等核心数据模型和认证逻辑在本地跑通后再链接到云端项目进行同步和最终部署。这能有效避免在云端项目里留下混乱的数据和表结构。4. 核心功能实现与代码深度解析4.1 身份认证系统的集成与封装身份认证是应用的第一道门。Supabase Auth 支持多种方式这里我们实现最基础的邮箱/密码注册登录。首先我们需要创建一套工具函数来初始化 Supabase 客户端。在lib/supabase目录下分别创建服务端和客户端工具文件。服务端客户端 (用于 Next.js Server Components/Actions)lib/supabase/server.tsimport { createServerClient } from supabase/ssr import { cookies } from next/headers export function createClient() { const cookieStore cookies() return createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: { getAll() { return cookieStore.getAll() }, setAll(cookiesToSet) { try { cookiesToSet.forEach(({ name, value, options }) cookieStore.set(name, value, options) ) } catch (error) { // 在 Server Action 或中间件中处理 } }, }, } ) }客户端组件用客户端 (用于 Client Components)lib/supabase/client.tsimport { createBrowserClient } from supabase/ssr export function createBrowserClient() { return createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ) }接下来实现登录页面 (app/login/page.tsx)。关键动作是使用supabase.auth.signInWithPassword或supabase.auth.signUp。use client import { useState } from react import { createBrowserClient } from /lib/supabase/client export default function LoginPage() { const [email, setEmail] useState() const [password, setPassword] useState() const [loading, setLoading] useState(false) const [message, setMessage] useState() const supabase createBrowserClient() const handleSignIn async (e: React.FormEvent) { e.preventDefault() setLoading(true) setMessage() const { error } await supabase.auth.signInWithPassword({ email, password, }) if (error) { setMessage(error.message) } else { // 登录成功Next.js 通常配合中间件重定向 window.location.href /dashboard } setLoading(false) } // 类似地实现 handleSignUp // ... return ( form onSubmit{handleSignIn} {/* 输入框和按钮 */} /form ) }关键点与避坑指南密码重置与邮箱确认在生产环境中务必在 Supabase 仪表盘的 Auth - Settings 中配置“Site URL”和“重定向URL”并启用“确认邮箱”和“密码重置”功能。否则用户注册和重置密码的邮件链接将是错误的。会话管理supabase/ssr包帮我们处理了在 Next.js 中安全传递会话的复杂性。确保中间件 (middleware.ts) 正确配置用于保护路由。// middleware.ts import { createServerClient } from supabase/ssr import { NextResponse, type NextRequest } from next/server export async function middleware(request: NextRequest) { const supabase createServerClient(...) const { data: { session } } await supabase.auth.getSession() // 如果用户未登录且不在登录页重定向到 /login if (!session request.nextUrl.pathname ! /login) { return NextResponse.redirect(new URL(/login, request.url)) } // 如果用户已登录且访问登录/注册页重定向到首页 if (session (request.nextUrl.pathname /login || request.nextUrl.pathname /signup)) { return NextResponse.redirect(new URL(/, request.url)) } return NextResponse.next() }类型安全运行supabase gen types typescript --project-id your-project-ref lib/database.types.ts生成数据库类型定义。然后在创建客户端时传入类型import { Database } from /lib/database.types const supabase createBrowserClientDatabase()此后所有数据库操作都将获得完美的代码提示和类型检查。4.2 数据库设计与行级安全策略配置这是 Supabase 最强大的特性之一。我们设计一个tasks表来演示。首先通过 Supabase 本地仪表盘运行supabase start后会给出链接或 SQL 来创建表。我更喜欢使用迁移文件这是可重复、版本控制的推荐方式。在supabase/migrations目录下创建一个新文件例如20240320000001_create_tasks_table.sql-- 创建 tasks 表 CREATE TABLE IF NOT EXISTS public.tasks ( id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, title TEXT NOT NULL, description TEXT, is_complete BOOLEAN DEFAULT FALSE, created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE(utc::text, NOW()) NOT NULL, updated_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE(utc::text, NOW()) NOT NULL ); -- 为 user_id 和 created_at 创建索引以提升查询性能 CREATE INDEX IF NOT EXISTS idx_tasks_user_id ON public.tasks(user_id); CREATE INDEX IF NOT EXISTS idx_tasks_created_at ON public.tasks(created_at DESC); -- 启用行级安全 (RLS) ALTER TABLE public.tasks ENABLE ROW LEVEL SECURITY; -- 创建策略用户只能插入属于自己的任务 CREATE POLICY Users can insert their own tasks ON public.tasks FOR INSERT WITH CHECK (auth.uid() user_id); -- 创建策略用户只能查看属于自己的任务 CREATE POLICY Users can view their own tasks ON public.tasks FOR SELECT USING (auth.uid() user_id); -- 创建策略用户只能更新属于自己的任务 CREATE POLICY Users can update their own tasks ON public.tasks FOR UPDATE USING (auth.uid() user_id); -- 创建策略用户只能删除属于自己的任务 CREATE POLICY Users can delete their own tasks ON public.tasks FOR DELETE USING (auth.uid() user_id);然后运行迁移命令将更改应用到本地数据库supabase db pushRLS 策略详解auth.uid()是一个 Supabase 提供的函数在查询执行时返回当前请求用户的 UUID。它通过 JWT 令牌自动识别。WITH CHECK用于INSERT和UPDATE确保新增或修改后的数据满足策略条件。USING用于SELECT,UPDATE,DELETE作为过滤行的条件。这四条策略共同确保了数据的绝对隔离每个用户只能操作user_id与自己auth.uid()匹配的行。这是多租户应用数据安全的核心。注意事项在开发中一个常见的错误是忘记启用 RLS 或者策略写错导致前端查询不到数据SELECT策略过严或插入失败INSERT策略缺失。当你遇到“权限被拒绝”的错误时首先检查表是否启用了 RLS以及相关操作的策略是否已创建。可以通过本地仪表盘的“表编辑器”界面直观地查看和管理 RLS 策略。4.3 数据查询、变更与实时订阅的实现有了安全的表结构前端就可以与之交互了。我们创建一个任务列表页面。服务端获取数据 (Server Component)app/dashboard/page.tsximport { createClient } from /lib/supabase/server import { redirect } from next/navigation export default async function DashboardPage() { const supabase createClient() const { data: { session } } await supabase.auth.getSession() if (!session) { redirect(/login) } // 使用服务端客户端安全查询无需暴露 anon key 给浏览器 const { data: tasks, error } await supabase .from(tasks) .select(*) .eq(user_id, session.user.id) .order(created_at, { ascending: false }) if (error) { console.error(Error fetching tasks:, error) } return ( div h1Your Tasks/h1 TaskList initialTasks{tasks || []} session{session} / /div ) }客户端交互与实时订阅 (Client Component)components/TaskList.tsxuse client import { useEffect, useState } from react import { createBrowserClient } from /lib/supabase/client import { Database } from /lib/database.types import { Session } from supabase/supabase-js type Task Database[public][Tables][tasks][Row] export default function TaskList({ initialTasks, session }: { initialTasks: Task[], session: Session }) { const [tasks, setTasks] useStateTask[](initialTasks) const supabase createBrowserClientDatabase() // 1. 设置实时订阅 useEffect(() { // 订阅 tasks 表的所有更改且只接收属于当前用户的行 const channel supabase .channel(tasks-channel) .on( postgres_changes, { event: *, // INSERT, UPDATE, DELETE schema: public, table: tasks, filter: user_ideq.${session.user.id}, }, (payload) { // 根据事件类型更新本地状态 if (payload.eventType INSERT) { setTasks((current) [payload.new as Task, ...current]) } else if (payload.eventType UPDATE) { setTasks((current) current.map((task) task.id payload.new.id ? { ...task, ...payload.new } : task ) ) } else if (payload.eventType DELETE) { setTasks((current) current.filter((task) task.id ! payload.old.id)) } } ) .subscribe() // 清理订阅 return () { supabase.removeChannel(channel) } }, [supabase, session.user.id]) // 2. 处理任务状态切换 const toggleTaskComplete async (taskId: number, isComplete: boolean) { const { error } await supabase .from(tasks) .update({ is_complete: !isComplete, updated_at: new Date().toISOString() }) .eq(id, taskId) if (error) alert(更新失败) // 状态更新将由上面的实时订阅自动处理 } // 3. 处理新增任务 const handleAddTask async (title: string) { const { error } await supabase .from(tasks) .insert([{ title, user_id: session.user.id }]) if (error) alert(创建失败) } return ( div {/* 渲染任务列表和添加表单 */} {tasks.map(task ( div key{task.id} input typecheckbox checked{task.is_complete} onChange{() toggleTaskComplete(task.id, task.is_complete)} / span{task.title}/span /div ))} /div ) }实时订阅的要点filter参数至关重要。它确保客户端只接收与自己相关的数据变更避免收到全表广播既安全又高效。一定要在组件卸载时调用supabase.removeChannel(channel)或channel.unsubscribe()来清理订阅防止内存泄漏和重复订阅。实时连接是基于 WebSocket 的。Supabase 客户端会自动管理连接的重连和状态。4.4 文件存储与管理的集成示例如果 Demo 包含文件上传功能我们会用到 Supabase Storage。首先在 Supabase 仪表盘本地或云端的 Storage 部分创建一个名为task-attachments的存储桶Bucket。创建时记得配置公共权限或精细的 RLS 策略。假设我们允许用户上传任务相关的图片。前端实现如下use client import { createBrowserClient } from /lib/supabase/client import { useState } from react export default function FileUpload({ taskId }: { taskId: number }) { const [uploading, setUploading] useState(false) const supabase createBrowserClient() const handleUpload async (event: React.ChangeEventHTMLInputElement) { try { setUploading(true) const file event.target.files?.[0] if (!file) return // 生成唯一文件名避免冲突 const fileExt file.name.split(.).pop() const fileName ${taskId}/${Math.random().toString(36).slice(2)}.${fileExt} const filePath ${fileName} // 上传文件到指定存储桶 const { error: uploadError } await supabase.storage .from(task-attachments) // 存储桶名称 .upload(filePath, file) if (uploadError) throw uploadError // 获取文件的公开访问 URL如果存储桶是公共的 const { data: { publicUrl } } supabase.storage .from(task-attachments) .getPublicUrl(filePath) // 或者如果你需要带有时效性的签名 URL用于私有存储桶 // const { data: { signedUrl } } await supabase.storage // .from(task-attachments) // .createSignedUrl(filePath, 60) // 60秒有效期 // 将文件路径或URL保存到 tasks 表或一个单独的 attachments 表 const { error: dbError } await supabase .from(task_attachments) .insert([{ task_id: taskId, file_path: filePath, url: publicUrl }]) if (dbError) throw dbError alert(上传成功) } catch (error) { alert(上传失败: ${error.message}) } finally { setUploading(false) } } return ( div input typefile onChange{handleUpload} disabled{uploading} / {uploading span上传中.../span} /div ) }存储安全最佳实践RLS for Storage像数据库一样为存储桶启用 RLS 并创建策略。例如INSERT策略可以检查用户是否登录SELECT策略可以检查文件是否属于用户相关的任务。避免直接暴露公共 URL对于敏感文件永远不要使用公共 URL。始终使用createSignedUrl生成有时效性的临时链接。文件大小和类型限制在前端和后端通过存储桶策略或 Edge Functions都实施限制防止恶意上传。组织文件结构使用类似{user_id}/{task_id}/{filename}的路径结构便于管理和清理。5. 部署上线与生产环境配置5.1 将本地变更同步至云端当本地开发完成后你需要将数据库架构表、策略、函数等推送到云端项目。Supabase CLI 使得这个过程非常简单。生成迁移文件如果你在本地通过仪表盘直接修改了数据库可以使用 diff 命令生成迁移文件。supabase db diff -f add_user_profile_table这会将本地数据库与链接的远程数据库进行比较并生成一个包含差异的 SQL 文件到supabase/migrations目录。推送迁移将累积的所有迁移文件应用到云端。supabase db push这个命令会按时间顺序执行migrations文件夹中尚未应用到远程数据库的所有 SQL 文件。可选拉取远程数据如果你需要将云端的生产数据拉取到本地进行测试注意数据安全可以使用supabase db pull这会在本地创建一个新的迁移文件其内容反映了远程数据库的当前状态。5.2 前端应用部署Next.js 应用可以部署到 Vercel、Netlify 或任何支持 Node.js 的托管平台。以 Vercel 为例过程非常顺畅将你的代码推送到 GitHub、GitLab 或 Bitbucket。在 Vercel 中导入该项目。在项目设置的环境变量部分添加NEXT_PUBLIC_SUPABASE_URL和NEXT_PUBLIC_SUPABASE_ANON_KEY值为你的云端 Supabase 项目的对应值。绝对不要使用本地环境的密钥。部署。Vercel 会自动识别 Next.js 项目并完成构建部署。关键部署检查清单[ ] 确保所有环境变量已在部署平台正确设置。[ ] 在 Supabase 云端项目仪表盘的Auth - URL Configuration中将“Site URL”和“Redirect URLs”更新为你的生产环境域名如https://your-app.vercel.app。这对于第三方 OAuth 登录和密码重置邮件至关重要。[ ] 检查 Storage 存储桶的 RLS 策略在生产环境是否按预期工作。[ ] 禁用或清理本地开发中可能创建的测试数据。5.3 性能优化与监控建议项目上线后有几个方面需要关注数据库索引对于频繁查询的字段如user_id,created_at,is_complete务必创建索引。我们的迁移文件中已经为user_id和created_at创建了索引。查询优化使用select()时只获取需要的字段避免select(*)。使用分页range()限制返回的数据量。实时订阅管理确保客户端在不需要时如页面不可见、组件卸载及时取消订阅减少不必要的服务器连接。利用 Supabase 仪表盘Supabase 项目仪表盘提供了丰富的监控工具包括 API 请求量、数据库 CPU/内存使用情况、实时连接数、错误日志等。定期查看可以帮助你发现性能瓶颈和异常。设置数据库备份在 Supabase 云端项目设置中可以配置定期的自动备份。对于重要数据这是必须的。6. 常见问题排查与进阶技巧6.1 身份认证与 RLS 策略故障排查问题1用户登录成功但查询数据返回空数组或“权限被拒绝”。原因最可能的原因是目标表没有启用 RLS或者针对SELECT操作的 RLS 策略缺失/条件错误。排查进入 Supabase 仪表盘 - Table Editor - 选择你的表 - 点击“Policies”选项卡。确认 RLS 已启用绿色开关并且存在针对SELECT的策略。检查策略的 SQL 条件。确保使用了auth.uid()并与表中的user_id字段正确关联。一个快速测试方法是在仪表盘的 SQL 编辑器中以服务角色密钥Service Role Key拥有绕过 RLS 的权限运行查询如果能查到数据但用前端 anon key 查不到就是 RLS 策略问题。确保前端查询时用户已成功登录且会话有效。可以在前端console.log(session)来验证。问题2用户注册后在public.users或自定义的profiles表中找不到记录。原因Supabase Auth 将用户核心信息存储在内部的auth.users表对前端不可直接访问。通常我们需要创建一个profiles表并通过数据库触发器或 Edge Function 在用户注册时自动创建对应记录。解决方案创建一个profiles表并设置一个after signup触发器。-- 创建 profiles 表 CREATE TABLE public.profiles ( id UUID REFERENCES auth.users(id) ON DELETE CASCADE PRIMARY KEY, username TEXT UNIQUE, avatar_url TEXT, updated_at TIMESTAMP WITH TIME ZONE ); -- 启用 RLS ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY; CREATE POLICY Public profiles are viewable by everyone. ON public.profiles FOR SELECT USING (true); CREATE POLICY Users can update their own profile. ON public.profiles FOR UPDATE USING (auth.uid() id); -- 创建函数在 auth.users 插入时触发 CREATE OR REPLACE FUNCTION public.handle_new_user() RETURNS TRIGGER AS $$ BEGIN INSERT INTO public.profiles (id, username) VALUES (new.id, new.email); RETURN new; END; $$ LANGUAGE plpgsql SECURITY DEFINER; -- 创建触发器 CREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();6.2 实时订阅连接不稳定或数据重复问题页面收到重复的更新事件或者连接频繁断开重连。原因组件重复渲染导致useEffect多次执行创建了多个订阅通道。或者网络不稳定。解决使用useRef或状态管理确保订阅只建立一次。可以用一个useRef标记是否已订阅。const subscribed useRef(false) useEffect(() { if (subscribed.current) return subscribed.current true const channel supabase.channel(...).subscribe() return () { supabase.removeChannel(channel) } }, [])检查过滤器确保filter条件准确避免收到不相关的广播消息。监听连接状态Supabase 客户端提供了连接状态监听可以用于 UI 提示。supabase.getChannels().forEach(channel { channel.on(system, { event: disconnect }, () console.log(Disconnected)) channel.on(system, { event: reconnect }, () console.log(Reconnected)) })6.3 类型安全与开发体验优化技巧自动生成并同步类型定义手动编写数据库类型定义非常繁琐且易错。利用 Supabase CLI 可以自动化这个过程。我通常在package.json中配置一个脚本{ scripts: { gen:types: supabase gen types typescript --project-id your-project-ref lib/database.types.ts } }每当数据库结构发生变化比如新增了表或字段就运行npm run gen:types来更新类型。为了更自动化可以将其与数据库迁移挂钩或者使用supabase start的--gen参数在本地开发时自动生成。技巧使用 React Query 或 SWR 进行状态管理对于复杂的数据获取、缓存、轮询和乐观更新可以考虑集成tanstack/react-query或swr。它们能与 Supabase 查询完美结合提供更强大的数据同步和状态管理能力减少手动状态管理的复杂度。例如使用 React Query 获取任务列表import { useQuery, useMutation, useQueryClient } from tanstack/react-query const fetchTasks async () { const supabase createBrowserClient() const { data: { session } } await supabase.auth.getSession() const { data } await supabase.from(tasks).select(*).eq(user_id, session.user.id) return data } function TaskList() { const { data: tasks, isLoading } useQuery({ queryKey: [tasks], queryFn: fetchTasks }) // ... 渲染逻辑 }6.4 安全加固要点回顾永远不要将service_role密钥暴露给前端这个密钥可以绕过所有 RLS 策略拥有最高权限。仅用于服务器端脚本或可信的后台服务。善用 RLS这是 Supabase 安全的第一道防线。为每张表、每个操作SELECT, INSERT, UPDATE, DELETE都定义明确的策略。遵循最小权限原则。验证用户输入虽然 Supabase 有参数化查询防止 SQL 注入但业务逻辑验证如字段长度、格式、枚举值仍需在前端和后端通过数据库约束或 Edge Functions进行。限制 API 速率在 Supabase 项目设置中可以配置 API 的速率限制防止滥用。定期审计日志查看 Supabase 仪表盘中的网络日志和数据库日志监控异常访问模式。从amha/supabase-demo这样一个起点出发你实际上获得了一套构建现代全栈应用的完整方法论和工具链。它教会你的不仅仅是 Supabase 的 API 调用更是如何思考数据安全、实时交互和前后端分离架构。当你成功复现并理解了这个 Demo 的每一行代码和每一个配置后你就已经具备了用这套技术栈独立开发一个真实应用的能力。剩下的就是发挥你的创意去构建更复杂、更有价值的产品了。