使用 Supabase、Svelte 与 Vite 构建带行级安全的多用户 Todo 应用(完整实战指南)
2026/9/7 7:07:37 网站建设 项目流程

使用 Supabase、Svelte 与 Vite 构建带行级安全的多用户 Todo 应用(完整实战指南)

【免费下载链接】supabaseThe Postgres development platform. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications.项目地址: https://gitcode.com/GitHub_Trending/supa/supabase

本指南以仓库中的 examples/todo-list/sveltejs-todo-list 官方示例为核心,讲解如何用 Svelte + TypeScript + Vite 作为前端,配合 Supabase 托管的 Postgres 数据库与@supabase/supabase-js客户端,从零搭建一个支持邮箱/密码注册登录、GitHub/Google 第三方登录、以及数据实时增删改查的 Todo 应用。读完本文你将掌握:Supabase 项目的初始化流程、Todo 快速启动 SQL 的执行方法、anonsecret两类密钥的区别,以及 Postgres Row Level Security(RLS)如何做到"每个用户只能看到并操作自己的待办事项"。

技术栈与目录结构总览

该示例遵循官方推荐的三层架构:浏览器端 Svelte 组件只负责渲染与交互,所有数据读写都通过 Supabase.js 走到托管 Postgres 后端的 RESTful API(PostgREST),而数据库端则由 Postgres 的 RLS 策略充当最终的安全守门员。

  • 前端:Svelte、TypeScript、Vite,配合 Tailwind CSS 负责样式;
  • 后端:在 Supabase 云平台创建的托管 Postgres 数据库,自动暴露 RESTful API 供 Supabase.js 使用;
  • 关键依赖:@supabase/supabase-js(示例使用^2大版本),完整依赖见 package.json。

示例的源码目录结构如下,组件划分清晰、非常适合作为学习范本:

  • src/lib/db.ts:创建全局唯一 Supabase 客户端;
  • src/lib/schema.ts:手写的数据库类型声明(Database接口);
  • src/lib/Auth.svelte:登录/注册 UI 与逻辑;
  • src/lib/Home.svelte:登出后的主界面壳层;
  • src/lib/TodoList.svelte:Todo 列表的查询、新增、删除;
  • src/lib/Todo.svelte:单个 Todo 项的完成状态切换;
  • src/App.svelte:会话状态管理与会话监听。

第一步:在 Supabase 创建新项目

在开始写任何代码之前,需要先在云端准备数据库。前往 Supabase Dashboard 注册账号并创建一个新项目,随后等待数据库完成初始化(通常会持续一两分钟)。

创建成功后你会拿到一组与项目绑定的配置信息,其中最关键的是Project URL(API 地址)API Key,它们将在第三步配置到前端环境中。数据库本身无需手工建表,下一步的快速启动 SQL 会一次性完成建表与授权策略的创建。

第二步:运行 "Todo List" 快速启动 SQL

数据库启动后,进入 Dashboard 项目的SQL Editor标签页,向下滚动找到名为TODO LIST: Build a basic todo list with Row Level Security的模板,直接点击运行即可。它会替你在数据库中完成建表、开启行级安全、创建四条访问策略的全部工作。

如果希望跳过 Dashboard 手动执行,也可以走更贴近工程化的迁移方式:仓库中配套的 examples/todo-list/nextjs-todo-list/supabase/migrations/20230712094349_init.sql 保存了与本示例完全一致的初始化 SQL,可用 Supabase CLI 的supabase db push等迁移命令导入到你的项目,效果等价。

在阅读下一节 SQL 之前,先记住一个核心心智模型:todo 表上所有的"数据行属于哪个用户"由user_id字段承载,而"谁能访问哪些行"由 RLS 策略在数据库端强制执行。前端代码无论怎么写,都绕不过这一层数据库权限校验。

第三步:获取 Project URL 与 anon 密钥

进入项目设置(齿轮图标),打开API标签页,找到如下两样东西并记录下来,下一步将用到:

  • Project URL:形如https://<project-ref>.supabase.co的 API 端点;
  • anon公钥:用于客户端侧的 API 密钥。

理解这两类密钥的作用是配置安全的关键:

  • anon密钥:面向客户端的公开密钥,允许对数据库做"匿名访问",直到用户真正登录。用户登录成功后,客户端发出的请求会自动携带用户自己的 JWT(登录令牌),此时数据库端会依据 JWT 中的角色与auth.uid()切换数据可见范围,从而为数据启用行级安全。本示例刻意在 src/lib/db.ts 与 .env.example 中使用VITE_SUPABASE_PUBLISHABLE_KEY命名该密钥——在较新版本的 Supabase 中,anon密钥已被逐步更名为publishable key(可发布密钥),二者指向同一把可安全暴露在浏览器端的密钥,读者在控制台看到的名称以实际为准;
  • secret密钥:拥有数据的完整访问权限,会绕过所有安全策略,因此必须严格保密,只能在服务端环境中使用,绝不能放进客户端或浏览器代码(凡是VITE_前缀的变量都会被打包进前端产物,一旦放置 secret 密钥等于公开泄漏)。

配置前端环境变量

拿到上述两样配置后,将本示例根目录下的 .env.example 复制为.env并填入真实值:

# 在 examples/todo-list/sveltejs-todo-list 目录下 cp .env.example .env

对应内容如下:

# 从 Supabase 项目设置 > API 中获取 VITE_SUPABASE_URL=https://your-project.supabase.co VITE_SUPABASE_PUBLISHABLE_KEY=your-publishable-key

VITE_前缀是 Vite 暴露环境变量给客户端代码的约定:只有以此前缀开头的变量才会被import.meta.env读取。随后启动开发服务器:

npm install npm run dev

从 package.json 可以看到,dev脚本用concurrently同时运行 Tailwind CSS 监听编译与 Vite 开发服务器,二者缺一不可;npm run build则会先压缩编译 CSS 再执行vite build产出静态文件。仓库还提供了svelte-check脚本用于对 Svelte 组件做类型检查。

客户端初始化与类型安全

前端唯一需要初始化的全局单例就是 Supabase 客户端,src/lib/db.ts 的实现如下:

import { createClient } from '@supabase/supabase-js' import type { Database } from './schema' export const supabase = createClient<Database>( import.meta.env.VITE_SUPABASE_URL, import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY )

这里值得注意两个工程细节:

  1. createClient<Database>是泛型调用,Database类型来自 src/lib/schema.ts。该文件把todos表的Row(查询返回形态)、Insert(插入形态)、Update(更新形态)逐字段声明清楚——例如idRow中必填、在Insert中可选(由数据库自增生成),inserted_at同样允许缺省。有了这套类型,所有.from('todos').insert(...)调用都会获得编译期校验与自动补全,这是本示例强调 TypeScript 的根本原因;
  2. 直接用import.meta.env.VITE_SUPABASE_URL读取环境变量,若.env未正确配置,客户端会在运行时收到空值错误,因此务必先完成第三步的配置。

用户认证流程:从会话监听谈起

应用根组件 src/App.svelte 通过一个user变量在"登录页"与"Todo 主界面"之间切换。会话恢复与实时同步的代码如下:

onMount(() => { supabase.auth.getSession().then(({ data: { session } }) => { user = session?.user ?? null; }); const { data: { subscription: authListener } } = supabase.auth.onAuthStateChange( (_, session) => { const currentUser = session?.user; user = currentUser ?? null; } ); return () => { authListener?.unsubscribe(); }; });

这段逻辑承担三个职责:

  1. 会话恢复:页面刷新后通过getSession()立即拉取本地缓存的会话,避免已登录用户被强制跳回登录页;
  2. 登录状态订阅onAuthStateChange监听登录、登出、令牌刷新等全部认证事件,任何状态变化都会实时驱动 UI 切换;
  3. 资源清理:组件卸载时调用authListener.unsubscribe()解除订阅,这是 SvelteonMount返回清理函数的典型用法,可避免内存泄漏。

App.svelte根据user是否存在,条件渲染<Home>(已登录)或<Auth>(未登录)。登录成功的用户对象随后被逐层下传给业务组件,作为写入数据时user_id的来源。

邮箱密码登录与第三方 OAuth

src/lib/Auth.svelte 封装了两种登录形态。其一是邮箱/密码方式,用一个参数区分登录与注册:

const handleLogin = async (type) => { const { data: { user }, error, } = type === "LOGIN" ? await supabase.auth.signInWithPassword({ email, password }) : await supabase.auth.signUp({ email, password }); if (error) { helperText = { error: true, text: error.message }; } else if (!user && !error) { helperText = { error: false, text: "An email has been sent to you for verification!", }; } };

这段代码隐含了一个 Supabase Auth 的默认行为:当项目开启了邮件确认时,signUp成功后不会直接返回已登录用户,而是先发送验证邮件——因此代码用!user && !error分支提示用户查收邮件完成验证。

其二是 GitHub / Google 等第三方 OAuth 登录:

const handleOAuthLogin = async (provider: Provider) => { let { error } = await supabase.auth.signInWithOAuth({ provider }); if (error) console.log("Error: ", error.message); };

按钮分别以handleOAuthLogin("github")handleOAuthLogin("google")调用。需要提醒的是:使用第三方登录前,必须先到 Dashboard 的Authentication > Settings(或对应 Provider 配置页)中启用相应的第三方认证并填入应用的 OAuth 回调地址,否则会登录失败——这也解释了为什么示例代码的注释强调"你需要在 Authentication > Settings 中启用你想使用的第三方认证"。

登出逻辑放在 src/lib/Home.svelte,调用supabase.auth.signOut()即可,成功后onAuthStateChange会将user置空、UI 自动切回登录页。

Todo 数据层:查询、新增、完成、删除

查询列表

src/lib/TodoList.svelte 在组件挂载时拉取当前用户可见的全部待办:

const fetchTodos = async () => { let { data, error } = await supabase .from("todos") .select("*") .order("id", { ascending: true }); if (error) { console.log("error", error); } else { todos = data; } };

注意这里没有写任何WHERE user_id = ...条件——能这样做的原因是 RLS 的select策略已经在数据库端把结果过滤为"仅当前登录用户自己的行",客户端无需(也无法可靠地)自行过滤。这也是理解本示例安全模型最重要的一点。

新增 Todo

const addTodo = async (taskText: string) => { let task = taskText.trim(); if (task.length) { let { data: todo, error } = await supabase .from("todos") .insert({ task, user_id: user.id }) .select() .single(); if (error) { errorText = error.message; } else { todos = [...todos, todo]; newTaskText = ""; } } };

插入时必须显式携带user_id: user.id,也就是把当前登录用户的 UUID 写入行数据,与 RLS 的insert with check (auth.uid() = user_id)形成呼应。链式.select().single()让插入后直接返回新行并更新本地数组,UI 无需二次刷新;若 RLS 校验不通过(例如恶意传入他人的user_id),error会携带数据库返回的权限错误信息并展示在页面顶部的 Alert 中。

切换完成状态

src/lib/Todo.svelte 处理单行完成状态切换:

const toggle = async () => { try { const { data, error } = await supabase .from("todos") .update({ is_complete: !isCompleted }) .eq("id", todo.id) .select("is_complete") .single(); if (error) throw error; isCompleted = data.is_complete; } catch (error) { console.log("error", error); } };

.eq("id", todo.id)精确锁定目标行,.update(...)只更新is_complete字段。由于 RLS 的update策略限定"只能更新自己的行",即便客户端拿到的是别人数据的id,更新请求也会在数据库端被拒绝。

删除 Todo

const deleteTodo = async (id: number) => { try { await supabase.from("todos").delete().eq("id", id); todos = todos.filter((x) => x.id != id); } catch (error) { console.log("error", error); } };

删除同样只依赖行id,可见性控制完全交给 RLS 的delete策略。以上增删改查四条链路共同说明一个结论:所有客户端过滤逻辑都只是用户体验优化,真正的数据边界始终由数据库端 RLS 兜底

深入 Supabase 细节:Postgres 行级安全(RLS)

RLS 的工作机制

本示例采用 Postgres 自带的高级授权能力 —— Row Level Security。其工作原理可概括为:

  1. 在 Supabase 创建 Postgres 数据库时,平台会自动预置一个authschema 以及若干辅助函数(如auth.uid());
  2. 用户登录后,GoTrue(Supabase 的认证服务)为其签发携带角色authenticated与其 UUID 的 JWT;
  3. PostgREST 收到带 JWT 的请求后,会注入对应的数据库角色与auth.uid()上下文;
  4. 表上的每一条策略都基于这些上下文做逐行判断,从而实现"精细到行"的访问控制。

建表与策略 SQL 逐行解读

下方是示例中精简后的完整 schema,与 examples/todo-list/nextjs-todo-list/supabase/migrations/20230712094349_init.sql 内容一致(差异仅在于示例用了(select auth.uid())显式子查询写法,语义相同):

create table todos ( id bigint generated by default as identity primary key, user_id uuid references auth.users not null, task text check (char_length(task) > 3), is_complete boolean default false, inserted_at timestamp with time zone default timezone('utc'::text, now()) not null ); alter table todos enable row level security; create policy "Individuals can create todos." on todos for insert with check ((select auth.uid()) = user_id); create policy "Individuals can view their own todos. " on todos for select using ((select auth.uid()) = user_id); create policy "Individuals can update their own todos." on todos for update using ((select auth.uid()) = user_id); create policy "Individuals can delete their own todos." on todos for delete using ((select auth.uid()) = user_id);

逐段拆解:

  • 建表id使用bigint generated by default as identity自增主键(现代 Postgres 推荐的 identity 列而非旧式 serial);user_iduuid类型且通过外键references auth.users关联认证用户表,并声明not null,从根源上杜绝"无主数据";task通过check (char_length(task) > 3)约束任务文本必须超过 3 个字符——这也是前端addTodo里做task.trim()与长度判断的数据库端镜像约束;is_complete默认falseinserted_at默认取 UTC 当前时间(timezone('utc'::text, now()));
  • 开启 RLSalter table todos enable row level security是承上启下的一行。注意:如果不显式开启 RLS 且未赋予表级权限,表默认对所有人开放;而一旦开启 RLS 却没有创建任何策略,则默认拒绝一切访问。Supabase 的建议是始终开启 RLS,再由策略显式放行;
  • 四条策略分别覆盖 insert/select/update/delete,且全部围绕auth.uid()(当前登录用户 UUID)与行的user_id做等值判断:
    • insert 使用with check:校验的是即将写入的新行是否满足user_id = auth.uid(),防止用户伪造他人user_id插入不属于自己的记录;
    • select / update / delete 使用using:针对的是已存在的行,判断该行是否属于当前用户。用using还是with check(或二者兼用)是编写 RLS 时最需要区分的概念;
  • update策略只写了using而未写with check,意味着用户只能"定位到自己拥有的行",但配合前端update({ is_complete: ... })只改非关联字段的用法已足够;若需要允许更新user_id等敏感字段,则应补上with check

正是这条 SQL 让上一节中的所有前端调用"天然安全":任何用户查询到的行都必然是自己创建的,插入、删除、更新都只在自己的数据范围内生效。

本地运行完整流程回顾

  1. 复制环境变量:cp .env.example .env,填入从 Dashboard API 设置中获取的 Project URL 与 anon/publishable key;
  2. 安装依赖:npm install(仓库为 pnpm workspace 结构,单跑本示例亦可直接npm install);
  3. 启动:npm run dev,浏览器打开 Vite 输出的本地地址;
  4. 在 SQL Editor 执行 Todo List 快速启动 SQL(或由 CLI 迁移导入 init.sql);
  5. 注册一个账号完成登录,即可体验"添加任务 → 勾选完成 → 删除任务 → 登出"的完整闭环;再注册第二个账号对比,会发现两个账号的数据彼此完全隔离,这正是 RLS 的实际效果验证。

如需进一步掌握认证与实时同步机制,可在仓库中继续研读相关文档与应用实现;本示例侧重展示"最小可运行的 RLS 多用户应用",也正因为其精简,非常适合作为理解 Supabase 鉴权与 Postgres 安全模型的第一份源码范例。

【免费下载链接】supabaseThe Postgres development platform. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications.项目地址: https://gitcode.com/GitHub_Trending/supa/supabase

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询