Supabase + Svelte 用户管理示例全解:从零搭建 Svelte 应用与 Postgres 行级安全鉴权
本指南以 Supabase 官方示例项目 svelte-user-management 为蓝本,完整拆解如何在一个基于 Svelte 5 + Vite 的前端应用中集成 Supabase Auth、实时会话监听、Profiles 数据表 CRUD 与 Storage 头像上传。读完本文你将掌握从「创建项目、执行 Quickstart SQL、配置环境变量」到「Magic Link 登录、按用户写入个人资料、受 RLS 保护的头像存取」的端到端实战方案,并能在仓库源码层面理解每一行关键实现。
示例项目概览
examples/user-management/svelte-user-management 是 Supabase 仓库中 user-management 系列示例的 Svelte 实现。它演示了一条完整的用户管理链路:用户通过邮箱 Magic Link 登录 → 应用根据登录会话展示「登录」或「账户资料编辑」两种界面 → 用户可维护 username、website、avatar_url,并上传头像到 Storage。
项目的技术栈(以 package.json 为准):
| 依赖 | 版本区间 | 用途 |
|---|---|---|
svelte |
^5.37.3 |
UI 框架(使用 Svelte 5 的 runes 语法) |
@supabase/supabase-js |
^2 |
官方 JS 客户端,负责 Auth、数据库与 Storage |
vite |
^7.0.6 |
开发服务器与构建工具 |
@sveltejs/vite-plugin-svelte |
^6.1.0 |
Svelte 的 Vite 插件 |
typescript / svelte-check |
~5.9.2 / ^4.3.1 |
类型检查 |
整个示例没有引入 SvelteKit,而是以纯 Vite + Svelte 的 SPA 形式组织,更聚焦于 Supabase 客户端 API 本身,便于单独抽出复用。
安装依赖与可用脚本
进入示例目录后先安装依赖:
npm install
安装完成后,项目目录下可用的脚本(对应 package.json):
| 命令 | 作用 |
|---|---|
npm run dev |
以开发模式启动应用,浏览器打开 http://localhost:5173 即可访问;修改源码后页面会热更新重载 |
npm run build |
构建生产版本,产物输出到 dist 文件夹。Vite 会在生产模式下正确打包 Svelte 并优化构建产物,文件名带内容哈希 |
npm run preview |
本地预览生产构建产物 |
npm run check |
执行 svelte-check 与 tsc 做类型与编译期检查 |
Vite 的开发服务器默认监听 5173 端口,vite.config.ts 仅注册了 svelte() 插件,保持最小化配置。
从零搭建:五步快速上手
1. 创建 Supabase 项目
前往 Supabase Dashboard 注册并创建新项目,等待数据库启动完成。每个 Supabase 项目都是一个完整的 Postgres 数据库实例。
2. 运行 "User Management" Quickstart SQL
数据库启动后,进入项目的 SQL Editor,运行 User Management Starter 快速启动脚本(在 SQL editor 页面滚动找到 “User Management Starter: Sets up a public Profiles table which you can access with your API”),点击 RUN 执行。执行完毕后前往 Table Editor,即可看到新建的 profiles 表。
该脚本的关键内容包括三部分:
- 建表
profiles并开启 RLS——每条用户记录与auth.users通过外键id关联; - 为
supabase_realtime发布订阅添加profiles表——让该表支持 Realtime 实时推送; - 创建
avatarsStorage bucket 并配置公开读取/任意上传策略。
3. 获取 API URL 与密钥
在 Project Settings(齿轮图标)→ API 页签中找到:
- Project URL / API URL:形如
https://<project-ref>.supabase.co的项目接口地址; anon/ publishable key:客户端 API 密钥。它允许用户在登录前对数据库进行「匿名访问」;一旦用户完成登录,密钥会切换为该用户自己的登录令牌,从而让 Postgres 行级安全(RLS)生效(详见下文「Postgres 行级安全」小节)。
安全警示:
secret(service_role)密钥拥有绕过一切安全策略的完整数据访问权限,必须严格保密,只允许在服务端环境中使用,绝不能放进客户端或浏览器代码。示例中的.env.example与supabaseClient.ts只引用客户端密钥。
4. 配置环境变量
从 .env.example 复制生成 .env.local,并把上面的 URL 与密钥填入:
# .env.example 的内容(见 examples/user-management/svelte-user-management/.env.example)
VITE_SUPABASE_URL=https://your-project-ref.supabase.co
VITE_SUPABASE_PUBLISHABLE_KEY=your-publishable-key
.env.local 是本地开发私有文件,通常应加入 .gitignore,避免泄露密钥。Vite 会以 import.meta.env 的形式向客户端注入以 VITE_ 前缀开头的环境变量。
5. 运行应用
执行 npm run dev 并在浏览器打开 http://localhost:5173/,即进入可交互的登录界面。
客户端初始化与运行环境读取
src/supabaseClient.ts 是连接 Supabase 的唯一入口:
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
const supabasePublishableKey = import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY
export const supabase = createClient(supabaseUrl, supabasePublishableKey)
- 通过 Vite 的类型化环境变量
import.meta.env.VITE_*读取配置(类型声明位于 src/vite-env.d.ts); createClient(url, key)返回的supabase单例被各个组件共享,无需在组件内重复创建。
命名提示:SDK 新版本将此类客户端密钥统称为 publishable key,与旧文档中的
anonkey 指代同一角色(公开、仅能触发 RLS 校验)。若在旧版文档中看到VITE_SUPABASE_ANON_KEY之类的命名,其职责等价。
会话状态管理与条件渲染
src/App.svelte 负责「是否已登录」的全局判断,是整条鉴权流程的入口:
<script lang="ts">
import { onMount } from 'svelte'
import { supabase } from './supabaseClient'
import type { AuthSession } from '@supabase/supabase-js'
import Account from './lib/Account.svelte'
import Auth from './lib/Auth.svelte'
let session = $state<AuthSession | null>(null)
onMount(() => {
supabase.auth.getSession().then(({ data }) => {
session = data.session
})
supabase.auth.onAuthStateChange((_event, _session) => {
session = _session
})
})
</script>
<div class="container" style="padding: 50px 0 100px 0">
{#if !session}
<Auth />
{:else}
<Account {session} />
{/if}
</div>
可以对照官方用户管理模板的典型模式理解其内部机制:
- 首次加载恢复会话:
onMount中调用supabase.auth.getSession()读取本地持久化的登录态,把session从null更新为真实会话(或保持null); - 订阅会话变化:
supabase.auth.onAuthStateChange注册监听器,登录、登出、令牌刷新等事件都会触发回调并把最新_session写入响应式状态,保证 UI 与真实登录态始终一致; - Svelte 5 runes:
let session = $state(...)声明响应式变量,配合{#if}实现「未登录渲染<Auth />、已登录渲染<Account {session} />」的条件分支,同时把当前会话对象作为 prop 传给子组件。
应用入口 src/main.ts 使用 Svelte 5 新的 mount API 把 App 挂载到 #app 节点并导入全局样式 app.css。
邮箱 Magic Link 无密码登录
src/lib/Auth.svelte 实现免密码登录:用户只需输入邮箱,点击发送 Magic Link,然后在邮箱中点击一次性登录链接即可完成登录。
<script lang="ts">
import { supabase } from "../supabaseClient";
let loading = $state(false);
let email = $state("");
const handleLogin = async () => {
try {
loading = true;
const { error } = await supabase.auth.signInWithOtp({ email });
if (error) throw error;
alert("Check your email for login link!");
} catch (error) {
if (error instanceof Error) {
alert(error.message);
}
} finally {
loading = false;
}
};
</script>
<form class="form-widget" onsubmit={(e) => { e.preventDefault(); handleLogin(); }}>
<input id="email" type="email" placeholder="Your email" bind:value={email} />
<button type="submit" disabled={loading}>
{loading ? "Loading" : "Send magic link"}
</button>
</form>
核心调用是 supabase.auth.signInWithOtp({ email }):
- 发送请求期间通过
loading状态禁用按钮并展示 “Loading”,用finally确保无论成败都会复位,避免重复提交; - 错误统一经
alert弹出提示; - 表单通过
onsubmit+preventDefault()拦截默认提交行为,转由handleLogin处理; - Magic Link 的启用/关闭取决于 Supabase 项目 Auth 配置中是否开启邮件 OTP,本示例默认走这一路径。
账户资料:读取与 upsert 更新
src/lib/Account.svelte 接收从 App.svelte 传入的 session,负责展示并编辑当前用户的资料。
读取当前用户资料(getProfile):
const getProfile = async () => {
loading = true;
const { user } = session;
const { data, error, status } = await supabase
.from("profiles")
.select("username, website, avatar_url")
.eq("id", user.id)
.single();
if (error && status !== 406) throw error;
if (data) {
username = data.username;
website = data.website;
avatarUrl = data.avatar_url;
}
};
- 从
session.user取出登录用户的id(该 UUID 与auth.users一致); - 以
.eq("id", user.id)过滤出当前用户的行,.single()期望恰好返回一行; - 特殊处理 406:当该用户还没有资料行时,查询返回
406 Not Acceptable,此时不视为错误抛出,仅保持表单为空,等待用户新建资料。
保存资料(updateProfile):
const updates = {
id: user.id,
username,
website,
avatar_url: avatarUrl,
updated_at: new Date().toISOString(),
};
const { error } = await supabase.from("profiles").upsert(updates);
使用 upsert(存在则更新、不存在则插入)把表单内容写回 profiles 表,并手动刷新 updated_at 时间戳。这种「upsert + 主键即 user.id」的写法既避免了「先查后写」的竞态,也正好利用了 profiles.id 引用 auth.users.id 的主键约束。
页面同时提供 Sign Out 按钮,点击即调用 supabase.auth.signOut(),触发 onAuthStateChange 回调后 App.svelte 的 session 置空,界面自动切回登录视图。
头像上传:Storage bucket 与 Blob 预览
src/lib/Avatar.svelte 封装了一个可复用的头像组件,对外暴露 url(bindable)与 onupload 回调,账户页在头像更新完成后会触发 updateProfile 一并把新的 avatar_url 落库。
下载并预览(downloadImage):
const { data, error } = await supabase.storage
.from("avatars")
.download(path);
const url = URL.createObjectURL(data);
avatarUrl = url;
- 从
avatarsbucket 按对象路径下载文件; - 用
URL.createObjectURL生成本地 Blob URL 直接预览,无需对象公开 URL; $effect(() => { if (url) downloadImage(url); })是 Svelte 5 的响应式副作用:每当传入的url变化(如新头像上传成功)就重新拉取图片。
上传文件(uploadAvatar):
const file = files[0];
const fileExt = file.name.split(".").pop();
const filePath = `${Math.random()}.${fileExt}`;
const { error } = await supabase.storage
.from("avatars")
.upload(filePath, file);
url = filePath;
onupload?.();
- 取用户选择的第一个文件,用
Math.random()+ 原扩展名生成随机对象路径,避免同名文件互相覆盖与缓存冲突(生产项目更推荐 UUID、时间戳等方案); - 上传成功后把
url回写给父组件($bindable)并调用onupload?.()通知保存资料; - 模板用隐藏的原生
<input type="file" accept="image/*">触发文件选择,样式上以按钮呈现。
整个链路可总结为:上传头像到 avatars bucket → 把对象路径存进 profiles.avatar_url → 下次进入时按路径 download 并 createObjectURL 展示。
Postgres 行级安全(RLS)原理
该示例的高层授权完全建立在 Postgres 行级安全(Row Level Security)之上,这是整个方案安全性的根基:
- Supabase 中的每个 Postgres 数据库都预置了
authschema 及若干辅助函数; - 用户登录后会获得一个携带角色
authenticated与用户 UUID 的 JWT; - Supabase 据此对「每个用户能做什么、不能做什么」进行细粒度控制:数据库层面的 RLS 策略会基于 JWT 中的
auth.uid()校验每一行数据的读写权限,而不是依赖客户端「隐藏界面元素」这类不可信的防御手段。
User Management Starter 生成的表结构与策略
以下是该示例所依赖的精简版 schema 及全部策略(与官方 Quickstart SQL 一致):
-- 创建公开资料表(Public Profiles)
create table
profiles (
id uuid references auth.users not null,
updated_at timestamp with time zone,
username text unique,
avatar_url text,
website text,
primary key (id),
unique (username),
constraint username_length check (char_length(username) >= 3)
);
alter table profiles enable row level security;
-- 所有人可查看公开资料(read 策略)
create policy "Public profiles are viewable by everyone." on profiles for
select
using (true);
-- 用户只能插入自己的资料(insert 策略)
create policy "Users can insert their own profile." on profiles for insert
with
check ((select auth.uid()) = id);
-- 用户只能更新自己的资料(update 策略)
create policy "Users can update own profile." on profiles for
update
using ((select auth.uid()) = id);
-- 启用 Realtime:将 profiles 表加入 supabase_realtime 发布订阅
begin;
drop publication if exists supabase_realtime;
create publication supabase_realtime;
commit;
alter publication supabase_realtime add table profiles;
-- 创建 avatars Storage bucket
insert into
storage.buckets (id, name)
values
('avatars', 'avatars');
-- Storage 访问控制:允许以公开方式下载头像对象
create policy "Avatar images are publicly accessible." on storage.objects for
select
using (bucket_id = 'avatars' and storage.allow_any_operation(array['object.get_authenticated_info', 'object.get_authenticated']));
-- 允许任意人上传头像
create policy "Anyone can upload an avatar." on storage.objects for insert
with
check (bucket_id = 'avatars');
逐个策略解读:
| 对象 | 策略 | 含义 |
|---|---|---|
profiles 表 |
enable row level security |
开启 RLS,此后所有普通客户端访问都必须经过策略裁决 |
profiles SELECT |
using (true) |
公开资料对所有人可读(含未登录的 anon 角色),这是公开展示用户名的前提 |
profiles INSERT |
with check (auth.uid() = id) |
只允许用户插入 id 等于自己 UUID 的行,防止越权创建他人资料 |
profiles UPDATE |
using (auth.uid() = id) |
只允许用户更新自己的行;未提供 DELETE 策略,即默认禁止删除 |
profiles 表 |
加入 supabase_realtime 发布 |
开启该表的 Realtime 变更推送能力 |
storage.buckets |
插入 avatars |
创建公开的 avatars bucket |
storage.objects SELECT |
bucket_id = 'avatars' |
允许公开下载 avatars bucket 中的对象 |
storage.objects INSERT |
bucket_id = 'avatars' |
允许向 avatars bucket 上传(示例保持开放以便快速体验,生产建议收紧为 auth.uid() = (storage.foldername(name))[1]::uuid 之类按用户隔离的策略) |
理解这张表,也就理解了这个示例为何「安全不用在前端做任何权限判断」:即便恶意用户直接构造 API 请求,也会在数据库层被 RLS 拒绝。账户页对 status !== 406 的错误统一抛出、Storage 随机对象路径等细节,都是对这套权限模型的补充。
从源码到文档:示例目录结构速览
examples/user-management/svelte-user-management/
├── .env.example # 环境变量模板(VITE_SUPABASE_URL / VITE_SUPABASE_PUBLISHABLE_KEY)
├── index.html
├── package.json # 脚本与依赖声明
├── svelte.config.js
├── tsconfig*.json # 应用与 Node 侧分离的 TS 配置
├── vite.config.ts # 仅注册 svelte() 插件
└── src/
├── main.ts # mount 入口
├── App.svelte # 会话管理 + 登录/资料条件渲染
├── supabaseClient.ts # createClient 单例
├── app.css # 全局样式
└── lib/
├── Auth.svelte # Magic Link 邮箱登录
├── Account.svelte # 资料读取与 upsert 更新、登出
└── Avatar.svelte # Storage 头像上传与 Blob 预览
同系列的其他语言实现(如 nextjs-user-management、expo-user-management 等)均复用同一份 RLS schema 与用户管理交互模式,可互为参照。
进阶方向
在把本示例用于真实产品前,可以继续深化:
- 收紧上传策略:让用户只能把对象上传到以自己
auth.uid()命名的目录(avatars/<uid>/...),并把storage.objects的 SELECT/INSERT 策略改为按目录校验; - 补充资料删除与账户注销:为
profiles增加 DELETE 策略,或通过 Database Trigger 在用户删除时级联清理资料与头像; - 接入 Email 之外的登录方式:
signInWithOtp之外,supabase.auth还支持 OAuth(signInWithOAuth)、密码(signInWithPassword)等方式; - 使用 Supabase CLI 管理迁移:把上述 Quickstart SQL 落到
supabase/migrations目录,实现 schema 的版本化——仓库中其他 user-management 示例(如 nextjs、flutter)均以 migrations 的形式维护这份初始化 SQL,可对照参考。
整体来看,本示例的精髓在于「薄客户端 + 厚数据库」:前端只负责调用 Auth、读写 profiles、存取 Storage,而数据安全完全交由 Postgres RLS 在服务端裁决。把这份 schema 与组件模式迁移到自己的 Supabase + Svelte 项目,即可快速获得一套安全、可扩展的用户体系。
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 StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00