场景代入:用户传了张头像,你存哪?直接塞数据库字段?图片几 MB,放进 Postgres 的 bytea 会把表撑爆、备份变慢、查询变卡。正确做法是对象存储:文件本身放 S3 兼容层,元数据(路径、大小、归属)放数据库,权限由 RLS 管。这一节我们用 Storage 把这个流程走通。
Storage 里最高层是 bucket(桶),相当于一个独立的文件空间。对象以路径存,如 avatars/用户id/me.png。对象的访问权限由两个东西决定:
storage.objects 表上的 RLS 策略。public bucket 的文件有固定公开 URL;private bucket 必须靠带签名的临时 URL 或经 RLS 校验的接口访问。
先写一条策略:用户只能管理自己 userId 目录下的文件(用路径第一段比对):
create policy "头像仅本人" on storage.objects for all using ( auth.uid()::text = (storage.foldername(name))[1] ) with check ( auth.uid()::text = (storage.foldername(name))[1] );
storage.foldername(name) 把路径按 / 拆成数组,[1] 取第一段。这样 avatars/用户A/me.png 只有用户 A 能碰。
前端上传:
const userId = (await supabase.auth.getUser()).data.user!.id const file = document.querySelector<HTMLInputElement>('#file')!.files![0] const { data, error } = await supabase.storage .from('avatars') .upload(`${userId}/me.png`, file, { upsert: true, contentType: 'image/png' }) if (!error) { const { data: url } = supabase.storage.from('avatars').getPublicUrl(`${userId}/me.png`) console.log('公开地址:', url.publicUrl) }
upsert: true 允许覆盖同名文件。若 bucket 是 private,改用 createSignedUrl 拿有时效的临时链接。
// 列举某用户目录下的文件 const { data: list } = await supabase.storage .from('avatars') .list(userId, { limit: 20 }) // 删除 await supabase.storage.from('avatars').remove([`${userId}/me.png`]) // 私有桶拿临时下载链接(60 秒有效) const { data: signed } = await supabase.storage .from('avatars') .createSignedUrl(`${userId}/me.png`, 60) console.log('临时链接:', signed?.signedUrl)
下面 SVG 画了"上传请求如何被策略收窄到本人目录"的流程,和 CRUD 一节同理,只是作用在 storage 上:

Storage 支持分片上传大文件,也支持图片变换(如缩略图),通过 URL 参数指定尺寸。这类变换在云端处理,不用你写压缩逻辑。
// 取一个 200x200 的缩略图(需 bucket 开启图片变换) const { data } = supabase.storage .from('avatars') .getPublicUrl(`${userId}/me.png`, { transform: { width: 200, height: 200 } }) console.log(data.publicUrl) // URL 带尺寸参数
背景:做一个相册,用户只能看自己的照片,且照片不公开。
操作过程:
photos,写策略:用户只能操作自己目录:create policy "相册本人" on storage.objects for all using ( auth.uid()::text = (storage.foldername(name))[1] ) with check ( auth.uid()::text = (storage.foldername(name))[1] );
photos/${userId}/2024-01.png。createSignedUrl 拿临时链接渲染,链接 60 秒过期,不会长期泄露。结果:即使有人猜到别人照片的文件名,没有对应 JWT 也取不到;且因为走 signed URL,链接本身有时效。
解读:private bucket + 路径首段比对 + signed URL,是相册类隐私场景的标准组合。若误设成 public bucket,任何拿到 URL 的人都能看,隐私直接破防。
变式:若要做"共享相册给特定好友",就不要在路径首段放 userId,而是建一张 album_shares 表,策略改用 exists (select 1 from album_shares where ...),把"谁能看"从路径约定升级为显式关系表——更灵活也更安全。
⚠️ 不要用 public bucket 存隐私文件,哪怕"链接很长猜不到"。公开桶的 URL 可被爬虫、Referer 日志、CDN 边缘节点记录。隐私内容一律 private + signed URL。
💡 我们建议图片缩略图直接用 Storage 的 transform 参数,别在前端或函数里自己压缩——云端处理省算力,且原图始终保留可重新生成任意尺寸。
下一节讲函数计算:哪些逻辑该放进 Edge Functions,怎么写、怎么部署、怎么安全地用管理员密钥。