supabase-backend-platform by bobmatnyc/claude-mpm-skills
npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill supabase-backend-platform基于以下技术构建的开源 Firebase 替代方案:
# 安装 Supabase 客户端
npm install @supabase/supabase-js
# 安装 CLI 用于本地开发
npm install -D supabase
# TypeScript 类型
npm install -D @supabase/supabase-js
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
// 使用 TypeScript 类型
import { Database } from '@/types/supabase'
export const supabase = createClient<Database>(
supabaseUrl,
supabaseAnonKey
)
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
Supabase 从 Postgres 模式自动生成 REST API:
// SELECT * FROM posts
const { data, error } = await supabase
.from('posts')
.select('*')
// 带过滤器的 SELECT
const { data } = await supabase
.from('posts')
.select('*')
.eq('status', 'published')
.order('created_at', { ascending: false })
.limit(10)
// 带连接的 SELECT
const { data } = await supabase
.from('posts')
.select(`
*,
author:profiles(name, avatar),
comments(count)
`)
// INSERT
const { data, error } = await supabase
.from('posts')
.insert({ title: 'Hello', content: 'World' })
.select()
.single()
// UPDATE
const { data } = await supabase
.from('posts')
.update({ status: 'published' })
.eq('id', postId)
.select()
// DELETE
const { error } = await supabase
.from('posts')
.delete()
.eq('id', postId)
// UPSERT
const { data } = await supabase
.from('posts')
.upsert({ id: 1, title: 'Updated' })
.select()
// 全文搜索
const { data } = await supabase
.from('posts')
.select('*')
.textSearch('title', 'postgresql', {
type: 'websearch',
config: 'english'
})
// 范围查询
const { data } = await supabase
.from('posts')
.select('*')
.gte('created_at', '2024-01-01')
.lte('created_at', '2024-12-31')
// 数组包含
const { data } = await supabase
.from('posts')
.select('*')
.contains('tags', ['postgres', 'supabase'])
// JSON 操作
const { data } = await supabase
.from('users')
.select('*')
.eq('metadata->theme', 'dark')
// 仅计数,不获取数据
const { count } = await supabase
.from('posts')
.select('*', { count: 'exact', head: true })
// 分页
const pageSize = 10
const page = 2
const { data } = await supabase
.from('posts')
.select('*')
.range(page * pageSize, (page + 1) * pageSize - 1)
// 调用 Postgres 函数
const { data, error } = await supabase
.rpc('get_trending_posts', {
days: 7,
min_score: 10
})
// SQL 中的函数示例
/*
CREATE OR REPLACE FUNCTION get_trending_posts(
days INTEGER,
min_score INTEGER
)
RETURNS TABLE (
id UUID,
title TEXT,
score INTEGER
) AS $$
BEGIN
RETURN QUERY
SELECT p.id, p.title, COUNT(v.id)::INTEGER as score
FROM posts p
LEFT JOIN votes v ON p.id = v.post_id
WHERE p.created_at > NOW() - INTERVAL '1 day' * days
GROUP BY p.id
HAVING COUNT(v.id) >= min_score
ORDER BY score DESC;
END;
$$ LANGUAGE plpgsql;
*/
// 注册
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'secure-password',
options: {
data: {
name: 'John Doe',
avatar_url: 'https://...'
}
}
})
// 登录
const { data, error } = await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'secure-password'
})
// 登出
const { error } = await supabase.auth.signOut()
// 获取当前用户
const { data: { user } } = await supabase.auth.getUser()
// 获取会话
const { data: { session } } = await supabase.auth.getSession()
// 使用 OAuth 登录
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: 'http://localhost:3000/auth/callback',
scopes: 'repo user'
}
})
// 可用的提供商
// github, google, gitlab, bitbucket, azure, discord, facebook,
// linkedin, notion, slack, spotify, twitch, twitter, apple
// 发送魔法链接
const { data, error } = await supabase.auth.signInWithOtp({
email: 'user@example.com',
options: {
emailRedirectTo: 'http://localhost:3000/auth/callback'
}
})
// 验证 OTP
const { data, error } = await supabase.auth.verifyOtp({
email: 'user@example.com',
token: '123456',
type: 'email'
})
// 使用手机登录
const { data, error } = await supabase.auth.signInWithOtp({
phone: '+1234567890'
})
// 验证手机 OTP
const { data, error } = await supabase.auth.verifyOtp({
phone: '+1234567890',
token: '123456',
type: 'sms'
})
// 监听认证变更
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_IN') {
console.log('用户已登录:', session?.user)
}
if (event === 'SIGNED_OUT') {
console.log('用户已登出')
}
if (event === 'TOKEN_REFRESHED') {
console.log('令牌已刷新')
}
})
// 更新用户元数据
const { data, error } = await supabase.auth.updateUser({
data: { theme: 'dark' }
})
// 更改密码
const { data, error } = await supabase.auth.updateUser({
password: 'new-password'
})
Postgres 行级安全在数据库级别控制数据访问:
-- 在表上启用 RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- 策略:用户可以读取所有已发布的帖子
CREATE POLICY "所有人都可以查看公开帖子"
ON posts FOR SELECT
USING (status = 'published');
-- 策略:用户只能更新自己的帖子
CREATE POLICY "用户可以更新自己的帖子"
ON posts FOR UPDATE
USING (auth.uid() = author_id);
-- 策略:已认证用户可以插入帖子
CREATE POLICY "已认证用户可以创建帖子"
ON posts FOR INSERT
WITH CHECK (auth.uid() = author_id);
-- 策略:用户可以删除自己的帖子
CREATE POLICY "用户可以删除自己的帖子"
ON posts FOR DELETE
USING (auth.uid() = author_id);
-- 公开读取,已认证写入
CREATE POLICY "任何人都可以查看帖子"
ON posts FOR SELECT
USING (true);
CREATE POLICY "已认证用户可以创建帖子"
ON posts FOR INSERT
WITH CHECK (auth.uid() IS NOT NULL);
-- 基于组织的访问
CREATE POLICY "用户可以查看组织数据"
ON documents FOR SELECT
USING (
organization_id IN (
SELECT organization_id
FROM memberships
WHERE user_id = auth.uid()
)
);
-- 基于角色的访问
CREATE POLICY "管理员可以执行任何操作"
ON posts FOR ALL
USING (
EXISTS (
SELECT 1 FROM user_roles
WHERE user_id = auth.uid()
AND role = 'admin'
)
);
-- 基于时间的访问
CREATE POLICY "查看已发布或已安排的帖子"
ON posts FOR SELECT
USING (
status = 'published'
OR (status = 'scheduled' AND publish_at <= NOW())
);
-- 获取当前用户 ID
SELECT auth.uid();
-- 获取当前用户 JWT
SELECT auth.jwt();
-- 获取特定声明
SELECT auth.jwt()->>'email';
-- 自定义声明
SELECT auth.jwt()->'app_metadata'->>'role';
// 上传文件
const { data, error } = await supabase.storage
.from('avatars')
.upload('public/avatar1.png', file, {
cacheControl: '3600',
upsert: false
})
// 带进度条的上传
const { data, error } = await supabase.storage
.from('avatars')
.upload('public/avatar1.png', file, {
onUploadProgress: (progress) => {
console.log(`${progress.loaded}/${progress.total}`)
}
})
// 从 URL 上传
const { data, error } = await supabase.storage
.from('avatars')
.uploadToSignedUrl('path', token, file)
// 下载文件
const { data, error } = await supabase.storage
.from('avatars')
.download('public/avatar1.png')
// 获取公开 URL
const { data } = supabase.storage
.from('avatars')
.getPublicUrl('public/avatar1.png')
// 创建签名 URL(临时访问)
const { data, error } = await supabase.storage
.from('avatars')
.createSignedUrl('private/document.pdf', 3600) // 1 小时
// 列出文件
const { data, error } = await supabase.storage
.from('avatars')
.list('public', {
limit: 100,
offset: 0,
sortBy: { column: 'name', order: 'asc' }
})
// 删除文件
const { data, error } = await supabase.storage
.from('avatars')
.remove(['public/avatar1.png'])
// 移动文件
const { data, error } = await supabase.storage
.from('avatars')
.move('public/avatar1.png', 'public/avatar2.png')
// 转换图片
const { data } = supabase.storage
.from('avatars')
.getPublicUrl('avatar1.png', {
transform: {
width: 200,
height: 200,
resize: 'cover',
quality: 80
}
})
// 可用的转换
// width, height, resize (cover|contain|fill),
// quality (1-100), format (origin|jpeg|png|webp)
-- 在存储上启用 RLS
CREATE POLICY "头像图片可公开访问"
ON storage.objects FOR SELECT
USING (bucket_id = 'avatars' AND (storage.foldername(name))[1] = 'public');
CREATE POLICY "用户可以上传自己的头像"
ON storage.objects FOR INSERT
WITH CHECK (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = auth.uid()::text
);
CREATE POLICY "用户可以删除自己的头像"
ON storage.objects FOR DELETE
USING (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = auth.uid()::text
);
// 订阅插入操作
const channel = supabase
.channel('posts-insert')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'posts'
},
(payload) => {
console.log('新帖子:', payload.new)
}
)
.subscribe()
// 订阅更新操作
const channel = supabase
.channel('posts-update')
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'posts',
filter: 'id=eq.1'
},
(payload) => {
console.log('已更新:', payload.new)
console.log('之前:', payload.old)
}
)
.subscribe()
// 订阅所有变更
const channel = supabase
.channel('posts-all')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'posts'
},
(payload) => {
console.log('变更:', payload)
}
)
.subscribe()
// 取消订阅
supabase.removeChannel(channel)
// 追踪在线状态
const channel = supabase.channel('room-1')
// 追踪当前用户
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState()
console.log('在线用户:', state)
})
.on('presence', { event: 'join' }, ({ key, newPresences }) => {
console.log('用户加入:', newPresences)
})
.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
console.log('用户离开:', leftPresences)
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track({
user_id: userId,
online_at: new Date().toISOString()
})
}
})
// 取消追踪
await channel.untrack()
// 广播消息
const channel = supabase.channel('chat-room')
channel
.on('broadcast', { event: 'message' }, (payload) => {
console.log('消息:', payload)
})
.subscribe()
// 发送消息
await channel.send({
type: 'broadcast',
event: 'message',
payload: { text: 'Hello', user: 'John' }
})
基于 Deno 运行时的无服务器函数:
# 创建函数
supabase functions new my-function
# 本地运行
supabase functions serve
# 部署
supabase functions deploy my-function
// supabase/functions/my-function/index.ts
import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
serve(async (req) => {
try {
// 获取认证头
const authHeader = req.headers.get('Authorization')!
// 创建 Supabase 客户端
const supabase = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
{ global: { headers: { Authorization: authHeader } } }
)
// 验证用户
const { data: { user }, error } = await supabase.auth.getUser()
if (error) throw error
// 处理请求
const { data } = await supabase
.from('posts')
.select('*')
.eq('author_id', user.id)
return new Response(
JSON.stringify({ data }),
{ headers: { 'Content-Type': 'application/json' } }
)
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
)
}
})
// 从客户端调用
const { data, error } = await supabase.functions.invoke('my-function', {
body: { name: 'John' }
})
// 带认证
const { data, error } = await supabase.functions.invoke('my-function', {
headers: {
Authorization: `Bearer ${session.access_token}`
},
body: { name: 'John' }
})
// lib/supabase/client.ts (客户端组件)
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
// lib/supabase/server.ts (服务器组件)
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient() {
const cookieStore = await cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
},
set(name: string, value: string, options: CookieOptions) {
cookieStore.set({ name, value, ...options })
},
remove(name: string, options: CookieOptions) {
cookieStore.set({ name, value: '', ...options })
},
},
}
)
}
// lib/supabase/middleware.ts (中间件)
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function updateSession(request: NextRequest) {
let response = NextResponse.next({
request: {
headers: request.headers,
},
})
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return request.cookies.get(name)?.value
},
set(name: string, value: string, options: CookieOptions) {
request.cookies.set({ name, value, ...options })
response = NextResponse.next({
request: { headers: request.headers },
})
response.cookies.set({ name, value, ...options })
},
remove(name: string, options: CookieOptions) {
request.cookies.set({ name, value: '', ...options })
response = NextResponse.next({
request: { headers: request.headers },
})
response.cookies.set({ name, value: '', ...options })
},
},
}
)
await supabase.auth.getUser()
return response
}
// middleware.ts
import { updateSession } from '@/lib/supabase/middleware'
export async function middleware(request: NextRequest) {
return await updateSession(request)
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}
// app/posts/page.tsx
import { createClient } from '@/lib/supabase/server'
export default async function PostsPage() {
const supabase = await createClient()
const { data: posts } = await supabase
.from('posts')
.select('*')
.order('created_at', { ascending: false })
return (
<div>
{posts?.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
</article>
))}
</div>
)
}
// app/components/new-post.tsx
'use client'
import { useState } from 'react'
import { createClient } from '@/lib/supabase/client'
export function NewPost() {
const [title, setTitle] = useState('')
const supabase = createClient()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return
const { error } = await supabase
.from('posts')
.insert({ title, author_id: user.id })
if (!error) {
setTitle('')
}
}
return (
<form onSubmit={handleSubmit}>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="帖子标题"
/>
<button>创建</button>
</form>
)
}
// app/actions/posts.ts
'use server'
import { revalidatePath } from 'next/cache'
import { createClient } from '@/lib/supabase/server'
export async function createPost(formData: FormData) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return { error: '未认证' }
}
const title = formData.get('title') as string
const { error } = await supabase
.from('posts')
.insert({ title, author_id: user.id })
if (error) {
return { error: error.message }
}
revalidatePath('/posts')
return { success: true }
}
# 安装 CLI
npm install -D supabase
# 登录
npx supabase login
# 链接项目
npx supabase link --project-ref your-project-ref
# 生成类型
npx supabase gen types typescript --project-id your-project-ref > types/supabase.ts
# 或从本地数据库生成
npx supabase gen types typescript --local > types/supabase.ts
// types/supabase.ts (已生成)
export type Database = {
public: {
Tables: {
posts: {
Row: {
id: string
title: string
content: string | null
author_id: string
created_at: string
}
Insert: {
id?: string
title: string
content?: string | null
author_id: string
created_at?: string
}
Update: {
id?: string
title?: string
content?: string | null
author_id?: string
created_at?: string
}
}
}
}
}
// 使用
import { createClient } from '@supabase/supabase-js'
import { Database } from '@/types/supabase'
const supabase = createClient<Database>(url, key)
// 类型安全的查询
const { data } = await supabase
.from('posts') // TypeScript 知道这个表存在
.select('title, content') // 列的自动补全
.single()
// data 的类型为 { title: string; content: string | null }
# 初始化 Supabase
npx supabase init
# 启动本地 Supabase (Postgres, Auth, Storage 等)
npx supabase start
# 停止
npx supabase stop
# 重置数据库
npx supabase db reset
# 状态
npx supabase status
# 创建迁移
npx supabase migration new create_posts_table
# 编辑迁移文件
# supabase/migrations/20240101000000_create_posts_table.sql
# 应用迁移
npx supabase db push
# 拉取远程模式
npx supabase db pull
# 比较本地与远程差异
npx supabase db diff
-- supabase/migrations/20240101000000_create_posts_table.sql
CREATE TABLE posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL,
content TEXT,
author_id UUID NOT NULL REFERENCES auth.users(id),
status TEXT NOT NULL DEFAULT 'draft',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 启用 RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- 策略
CREATE POLICY "任何人都可以查看已发布的帖子"
ON posts FOR SELECT
USING (status = 'published');
CREATE POLICY "用户可以创建自己的帖子"
ON posts FOR INSERT
WITH CHECK (auth.uid() = author_id);
CREATE POLICY "用户可以更新自己的帖子"
ON posts FOR UPDATE
USING (auth.uid() = author_id);
-- 索引
CREATE INDEX posts_author_id_idx ON posts(author_id);
CREATE INDEX posts_status_idx ON posts(status);
-- 用于 updated_at 的触发器
CREATE TRIGGER set_updated_at
BEFORE UPDATE ON posts
FOR EACH ROW
EXECUTE FUNCTION moddatetime(updated_at);
// 切勿在客户端暴露 service_role 密钥
// 客户端使用 anon key
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! // 公开
)
// service_role 密钥仅在服务器端使用
const supabaseAdmin = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!, // 密钥,绕过 RLS
{ auth: { persistSession: false } }
)
-- 始终启用 RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- 默认拒绝(无策略 = 无访问权限)
-- 通过策略显式授予访问权限
-- 以不同用户测试策略
SET request.jwt.claims.sub = 'user-id';
SELECT * FROM posts; -- 以此用户身份测试
-- 仅对管理员操作禁用 RLS
-- 从服务器使用 service_role 密钥,切勿在客户端使用
// 在客户端和服务器端验证
function validatePost(data: unknown) {
const schema = z.object({
title: z.string().min(1).max(200),
content: z.string().max(10000).optional()
})
return schema.parse(data)
}
// 边缘函数中的服务器端验证
serve(async (req) => {
const body = await req.json()
try {
const validated = validatePost(body)
// 处理已验证的数据
} catch (error) {
return new Response(
JSON.stringify({ error: '无效输入' }),
{ status: 400 }
)
}
})
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ... # 公开
SUPABASE_SERVICE_ROLE_KEY=eyJ... # 密钥,仅限服务器
# 生产环境:在托管平台中使用环境变量
# 切勿将 .env 文件提交到 git
-- 为常见查询添加索引
CREATE INDEX posts_created_at_idx ON posts(created_at DESC);
CREATE INDEX posts_author_status_idx ON posts(author_id, status);
-- 优化全文搜索
CREATE INDEX posts_title_search_idx ON posts
USING GIN (to_tsvector('english', title));
-- 分析查询性能
EXPLAIN ANALYZE
SELECT * FROM posts WHERE author_id = 'xxx';
-- 清理和分析
VACUUM ANALYZE posts;
// 为无服务器使用连接池
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(url, key, {
db: {
schema: 'public',
},
auth: {
persistSession: true,
autoRefreshToken: true,
},
global: {
headers: { 'x-my-custom-header': 'my-value' },
},
})
// 在 Supabase 仪表板中配置连接池
// 设置 > 数据库 > 连接池
// 启用查询日志记录
const supabase = createClient(url, key, {
global: {
fetch: async (url, options) => {
console.log('查询:', url)
return fetch(url, options)
}
}
})
// 在 Supabase 仪表板中监控
// - 数据库性能
// - API 使用情况
// - 存储使用情况
// - 认证活动
# 自动备份 (Pro 计划及以上)
# 每日备份,支持时间点恢复
# 手动备份
pg_dump -h db.xxx.supabase.co -U postgres -d postgres > backup.sql
# 恢复
psql -h db.xxx.supabase.co -U postgres -d postgres < backup.sql
数据库
查询
安全
开源
定价
生态系统
// Firestore 集合查询
const snapshot = await db
.collection('posts')
.where('status', '==', 'published')
.orderBy('createdAt', 'desc')
.limit(10)
.get()
// 等效的 Supabase 查询
const { data } = await supabase
.from('posts')
.select('*')
.eq('status', 'published')
.order('created_at', { ascending: false })
.limit(10)
// 复杂查询在 Supabase 中更容易
const { data } = await supabase
.from('posts')
.select(`
*,
author:profiles!inner(name),
comments(count)
`)
.gte('created_at', startDate)
.lte('created_at', endDate)
.order('created_at', { ascending: false })
// Firebase 需要多次查询 + 客户端连接
'use client'
import { useState, useOptimistic } from 'react'
import { createClient } from '@/lib/supabase/client'
export function PostList({ initialPosts }: { initialPosts: Post[] }) {
const [posts, setPosts] = useState(initialPosts)
const [optimisticPosts, addOptimisticPost] = useOptimistic(
posts,
(state, newPost: Post) => [...state, newPost]
)
const supabase = createClient()
const createPost = async (title: string) => {
const tempPost = {
id: crypto.randomUUID(),
title,
created_at: new Date().toISOString()
}
addOptimisticPost(tempPost)
const { data } = await supabase
.from('posts')
.insert({ title })
.select()
.single()
if (data) {
setPosts([...posts, data])
}
}
return (
<div>
{optimisticPosts.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
)
}
'use client'
import { useState, useEffect } from 'react'
import { createClient } from '@/lib/supabase/client'
const PAGE_SIZE = 20
export function InfinitePostList() {
const [posts, setPosts] = useState<
Open-source Firebase alternative built on:
# Install Supabase client
npm install @supabase/supabase-js
# Install CLI for local development
npm install -D supabase
# TypeScript types
npm install -D @supabase/supabase-js
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
// With TypeScript types
import { Database } from '@/types/supabase'
export const supabase = createClient<Database>(
supabaseUrl,
supabaseAnonKey
)
Supabase auto-generates REST API from Postgres schema:
// SELECT * FROM posts
const { data, error } = await supabase
.from('posts')
.select('*')
// SELECT with filters
const { data } = await supabase
.from('posts')
.select('*')
.eq('status', 'published')
.order('created_at', { ascending: false })
.limit(10)
// SELECT with joins
const { data } = await supabase
.from('posts')
.select(`
*,
author:profiles(name, avatar),
comments(count)
`)
// INSERT
const { data, error } = await supabase
.from('posts')
.insert({ title: 'Hello', content: 'World' })
.select()
.single()
// UPDATE
const { data } = await supabase
.from('posts')
.update({ status: 'published' })
.eq('id', postId)
.select()
// DELETE
const { error } = await supabase
.from('posts')
.delete()
.eq('id', postId)
// UPSERT
const { data } = await supabase
.from('posts')
.upsert({ id: 1, title: 'Updated' })
.select()
// Full-text search
const { data } = await supabase
.from('posts')
.select('*')
.textSearch('title', 'postgresql', {
type: 'websearch',
config: 'english'
})
// Range queries
const { data } = await supabase
.from('posts')
.select('*')
.gte('created_at', '2024-01-01')
.lte('created_at', '2024-12-31')
// Array contains
const { data } = await supabase
.from('posts')
.select('*')
.contains('tags', ['postgres', 'supabase'])
// JSON operations
const { data } = await supabase
.from('users')
.select('*')
.eq('metadata->theme', 'dark')
// Count without data
const { count } = await supabase
.from('posts')
.select('*', { count: 'exact', head: true })
// Pagination
const pageSize = 10
const page = 2
const { data } = await supabase
.from('posts')
.select('*')
.range(page * pageSize, (page + 1) * pageSize - 1)
// Call Postgres function
const { data, error } = await supabase
.rpc('get_trending_posts', {
days: 7,
min_score: 10
})
// Example function in SQL
/*
CREATE OR REPLACE FUNCTION get_trending_posts(
days INTEGER,
min_score INTEGER
)
RETURNS TABLE (
id UUID,
title TEXT,
score INTEGER
) AS $$
BEGIN
RETURN QUERY
SELECT p.id, p.title, COUNT(v.id)::INTEGER as score
FROM posts p
LEFT JOIN votes v ON p.id = v.post_id
WHERE p.created_at > NOW() - INTERVAL '1 day' * days
GROUP BY p.id
HAVING COUNT(v.id) >= min_score
ORDER BY score DESC;
END;
$$ LANGUAGE plpgsql;
*/
// Sign up
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'secure-password',
options: {
data: {
name: 'John Doe',
avatar_url: 'https://...'
}
}
})
// Sign in
const { data, error } = await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'secure-password'
})
// Sign out
const { error } = await supabase.auth.signOut()
// Get current user
const { data: { user } } = await supabase.auth.getUser()
// Get session
const { data: { session } } = await supabase.auth.getSession()
// Sign in with OAuth
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: 'http://localhost:3000/auth/callback',
scopes: 'repo user'
}
})
// Available providers
// github, google, gitlab, bitbucket, azure, discord, facebook,
// linkedin, notion, slack, spotify, twitch, twitter, apple
// Send magic link
const { data, error } = await supabase.auth.signInWithOtp({
email: 'user@example.com',
options: {
emailRedirectTo: 'http://localhost:3000/auth/callback'
}
})
// Verify OTP
const { data, error } = await supabase.auth.verifyOtp({
email: 'user@example.com',
token: '123456',
type: 'email'
})
// Sign in with phone
const { data, error } = await supabase.auth.signInWithOtp({
phone: '+1234567890'
})
// Verify phone OTP
const { data, error } = await supabase.auth.verifyOtp({
phone: '+1234567890',
token: '123456',
type: 'sms'
})
// Listen to auth changes
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_IN') {
console.log('User signed in:', session?.user)
}
if (event === 'SIGNED_OUT') {
console.log('User signed out')
}
if (event === 'TOKEN_REFRESHED') {
console.log('Token refreshed')
}
})
// Update user metadata
const { data, error } = await supabase.auth.updateUser({
data: { theme: 'dark' }
})
// Change password
const { data, error } = await supabase.auth.updateUser({
password: 'new-password'
})
Postgres Row Level Security controls data access at the database level:
-- Enable RLS on table
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Policy: Users can read all published posts
CREATE POLICY "Public posts are viewable by everyone"
ON posts FOR SELECT
USING (status = 'published');
-- Policy: Users can only update their own posts
CREATE POLICY "Users can update own posts"
ON posts FOR UPDATE
USING (auth.uid() = author_id);
-- Policy: Authenticated users can insert posts
CREATE POLICY "Authenticated users can create posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = author_id);
-- Policy: Users can delete their own posts
CREATE POLICY "Users can delete own posts"
ON posts FOR DELETE
USING (auth.uid() = author_id);
-- Public read, authenticated write
CREATE POLICY "Anyone can view posts"
ON posts FOR SELECT
USING (true);
CREATE POLICY "Authenticated users can create posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() IS NOT NULL);
-- Organization-based access
CREATE POLICY "Users can view org data"
ON documents FOR SELECT
USING (
organization_id IN (
SELECT organization_id
FROM memberships
WHERE user_id = auth.uid()
)
);
-- Role-based access
CREATE POLICY "Admins can do anything"
ON posts FOR ALL
USING (
EXISTS (
SELECT 1 FROM user_roles
WHERE user_id = auth.uid()
AND role = 'admin'
)
);
-- Time-based access
CREATE POLICY "View published or scheduled posts"
ON posts FOR SELECT
USING (
status = 'published'
OR (status = 'scheduled' AND publish_at <= NOW())
);
-- Get current user ID
SELECT auth.uid();
-- Get current user JWT
SELECT auth.jwt();
-- Get specific claim
SELECT auth.jwt()->>'email';
-- Custom claims
SELECT auth.jwt()->'app_metadata'->>'role';
// Upload file
const { data, error } = await supabase.storage
.from('avatars')
.upload('public/avatar1.png', file, {
cacheControl: '3600',
upsert: false
})
// Upload with progress
const { data, error } = await supabase.storage
.from('avatars')
.upload('public/avatar1.png', file, {
onUploadProgress: (progress) => {
console.log(`${progress.loaded}/${progress.total}`)
}
})
// Upload from URL
const { data, error } = await supabase.storage
.from('avatars')
.uploadToSignedUrl('path', token, file)
// Download file
const { data, error } = await supabase.storage
.from('avatars')
.download('public/avatar1.png')
// Get public URL
const { data } = supabase.storage
.from('avatars')
.getPublicUrl('public/avatar1.png')
// Create signed URL (temporary access)
const { data, error } = await supabase.storage
.from('avatars')
.createSignedUrl('private/document.pdf', 3600) // 1 hour
// List files
const { data, error } = await supabase.storage
.from('avatars')
.list('public', {
limit: 100,
offset: 0,
sortBy: { column: 'name', order: 'asc' }
})
// Delete file
const { data, error } = await supabase.storage
.from('avatars')
.remove(['public/avatar1.png'])
// Move file
const { data, error } = await supabase.storage
.from('avatars')
.move('public/avatar1.png', 'public/avatar2.png')
// Transform image
const { data } = supabase.storage
.from('avatars')
.getPublicUrl('avatar1.png', {
transform: {
width: 200,
height: 200,
resize: 'cover',
quality: 80
}
})
// Available transformations
// width, height, resize (cover|contain|fill),
// quality (1-100), format (origin|jpeg|png|webp)
-- Enable RLS on storage
CREATE POLICY "Avatar images are publicly accessible"
ON storage.objects FOR SELECT
USING (bucket_id = 'avatars' AND (storage.foldername(name))[1] = 'public');
CREATE POLICY "Users can upload their own avatar"
ON storage.objects FOR INSERT
WITH CHECK (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = auth.uid()::text
);
CREATE POLICY "Users can delete their own avatar"
ON storage.objects FOR DELETE
USING (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = auth.uid()::text
);
// Subscribe to inserts
const channel = supabase
.channel('posts-insert')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'posts'
},
(payload) => {
console.log('New post:', payload.new)
}
)
.subscribe()
// Subscribe to updates
const channel = supabase
.channel('posts-update')
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'posts',
filter: 'id=eq.1'
},
(payload) => {
console.log('Updated:', payload.new)
console.log('Previous:', payload.old)
}
)
.subscribe()
// Subscribe to all changes
const channel = supabase
.channel('posts-all')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'posts'
},
(payload) => {
console.log('Change:', payload)
}
)
.subscribe()
// Unsubscribe
supabase.removeChannel(channel)
// Track presence
const channel = supabase.channel('room-1')
// Track current user
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState()
console.log('Online users:', state)
})
.on('presence', { event: 'join' }, ({ key, newPresences }) => {
console.log('User joined:', newPresences)
})
.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
console.log('User left:', leftPresences)
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track({
user_id: userId,
online_at: new Date().toISOString()
})
}
})
// Untrack
await channel.untrack()
// Broadcast messages
const channel = supabase.channel('chat-room')
channel
.on('broadcast', { event: 'message' }, (payload) => {
console.log('Message:', payload)
})
.subscribe()
// Send message
await channel.send({
type: 'broadcast',
event: 'message',
payload: { text: 'Hello', user: 'John' }
})
Serverless functions on Deno runtime:
# Create function
supabase functions new my-function
# Serve locally
supabase functions serve
# Deploy
supabase functions deploy my-function
// supabase/functions/my-function/index.ts
import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
serve(async (req) => {
try {
// Get auth header
const authHeader = req.headers.get('Authorization')!
// Create Supabase client
const supabase = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
{ global: { headers: { Authorization: authHeader } } }
)
// Verify user
const { data: { user }, error } = await supabase.auth.getUser()
if (error) throw error
// Process request
const { data } = await supabase
.from('posts')
.select('*')
.eq('author_id', user.id)
return new Response(
JSON.stringify({ data }),
{ headers: { 'Content-Type': 'application/json' } }
)
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
)
}
})
// From client
const { data, error } = await supabase.functions.invoke('my-function', {
body: { name: 'John' }
})
// With auth
const { data, error } = await supabase.functions.invoke('my-function', {
headers: {
Authorization: `Bearer ${session.access_token}`
},
body: { name: 'John' }
})
// lib/supabase/client.ts (Client Component)
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
// lib/supabase/server.ts (Server Component)
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient() {
const cookieStore = await cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
},
set(name: string, value: string, options: CookieOptions) {
cookieStore.set({ name, value, ...options })
},
remove(name: string, options: CookieOptions) {
cookieStore.set({ name, value: '', ...options })
},
},
}
)
}
// lib/supabase/middleware.ts (Middleware)
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function updateSession(request: NextRequest) {
let response = NextResponse.next({
request: {
headers: request.headers,
},
})
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return request.cookies.get(name)?.value
},
set(name: string, value: string, options: CookieOptions) {
request.cookies.set({ name, value, ...options })
response = NextResponse.next({
request: { headers: request.headers },
})
response.cookies.set({ name, value, ...options })
},
remove(name: string, options: CookieOptions) {
request.cookies.set({ name, value: '', ...options })
response = NextResponse.next({
request: { headers: request.headers },
})
response.cookies.set({ name, value: '', ...options })
},
},
}
)
await supabase.auth.getUser()
return response
}
// middleware.ts
import { updateSession } from '@/lib/supabase/middleware'
export async function middleware(request: NextRequest) {
return await updateSession(request)
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}
// app/posts/page.tsx
import { createClient } from '@/lib/supabase/server'
export default async function PostsPage() {
const supabase = await createClient()
const { data: posts } = await supabase
.from('posts')
.select('*')
.order('created_at', { ascending: false })
return (
<div>
{posts?.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
</article>
))}
</div>
)
}
// app/components/new-post.tsx
'use client'
import { useState } from 'react'
import { createClient } from '@/lib/supabase/client'
export function NewPost() {
const [title, setTitle] = useState('')
const supabase = createClient()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return
const { error } = await supabase
.from('posts')
.insert({ title, author_id: user.id })
if (!error) {
setTitle('')
}
}
return (
<form onSubmit={handleSubmit}>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Post title"
/>
<button>Create</button>
</form>
)
}
// app/actions/posts.ts
'use server'
import { revalidatePath } from 'next/cache'
import { createClient } from '@/lib/supabase/server'
export async function createPost(formData: FormData) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return { error: 'Not authenticated' }
}
const title = formData.get('title') as string
const { error } = await supabase
.from('posts')
.insert({ title, author_id: user.id })
if (error) {
return { error: error.message }
}
revalidatePath('/posts')
return { success: true }
}
# Install CLI
npm install -D supabase
# Login
npx supabase login
# Link project
npx supabase link --project-ref your-project-ref
# Generate types
npx supabase gen types typescript --project-id your-project-ref > types/supabase.ts
# Or from local database
npx supabase gen types typescript --local > types/supabase.ts
// types/supabase.ts (generated)
export type Database = {
public: {
Tables: {
posts: {
Row: {
id: string
title: string
content: string | null
author_id: string
created_at: string
}
Insert: {
id?: string
title: string
content?: string | null
author_id: string
created_at?: string
}
Update: {
id?: string
title?: string
content?: string | null
author_id?: string
created_at?: string
}
}
}
}
}
// Usage
import { createClient } from '@supabase/supabase-js'
import { Database } from '@/types/supabase'
const supabase = createClient<Database>(url, key)
// Type-safe queries
const { data } = await supabase
.from('posts') // TypeScript knows this table exists
.select('title, content') // Autocomplete for columns
.single()
// data is typed as { title: string; content: string | null }
# Initialize Supabase
npx supabase init
# Start local Supabase (Postgres, Auth, Storage, etc.)
npx supabase start
# Stop
npx supabase stop
# Reset database
npx supabase db reset
# Status
npx supabase status
# Create migration
npx supabase migration new create_posts_table
# Edit migration file
# supabase/migrations/20240101000000_create_posts_table.sql
# Apply migrations
npx supabase db push
# Pull remote schema
npx supabase db pull
# Diff local vs remote
npx supabase db diff
-- supabase/migrations/20240101000000_create_posts_table.sql
CREATE TABLE posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL,
content TEXT,
author_id UUID NOT NULL REFERENCES auth.users(id),
status TEXT NOT NULL DEFAULT 'draft',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Enable RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Policies
CREATE POLICY "Anyone can view published posts"
ON posts FOR SELECT
USING (status = 'published');
CREATE POLICY "Users can create their own posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = author_id);
CREATE POLICY "Users can update their own posts"
ON posts FOR UPDATE
USING (auth.uid() = author_id);
-- Indexes
CREATE INDEX posts_author_id_idx ON posts(author_id);
CREATE INDEX posts_status_idx ON posts(status);
-- Trigger for updated_at
CREATE TRIGGER set_updated_at
BEFORE UPDATE ON posts
FOR EACH ROW
EXECUTE FUNCTION moddatetime(updated_at);
// NEVER expose service_role key in client
// Use anon key for client-side
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! // Public
)
// Service role key only on server
const supabaseAdmin = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!, // Secret, bypasses RLS
{ auth: { persistSession: false } }
)
-- Always enable RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Default deny (no policy = no access)
-- Explicitly grant access with policies
-- Test policies as different users
SET request.jwt.claims.sub = 'user-id';
SELECT * FROM posts; -- Test as this user
-- Disable RLS only for admin operations
-- Use service_role key from server, never client
// Validate on client and server
function validatePost(data: unknown) {
const schema = z.object({
title: z.string().min(1).max(200),
content: z.string().max(10000).optional()
})
return schema.parse(data)
}
// Server-side validation in Edge Function
serve(async (req) => {
const body = await req.json()
try {
const validated = validatePost(body)
// Process validated data
} catch (error) {
return new Response(
JSON.stringify({ error: 'Invalid input' }),
{ status: 400 }
)
}
})
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ... # Public
SUPABASE_SERVICE_ROLE_KEY=eyJ... # Secret, server-only
# Production: Use environment variables in hosting platform
# Never commit .env files to git
-- Add indexes for common queries
CREATE INDEX posts_created_at_idx ON posts(created_at DESC);
CREATE INDEX posts_author_status_idx ON posts(author_id, status);
-- Optimize full-text search
CREATE INDEX posts_title_search_idx ON posts
USING GIN (to_tsvector('english', title));
-- Analyze query performance
EXPLAIN ANALYZE
SELECT * FROM posts WHERE author_id = 'xxx';
-- Vacuum and analyze
VACUUM ANALYZE posts;
// Use connection pooling for serverless
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(url, key, {
db: {
schema: 'public',
},
auth: {
persistSession: true,
autoRefreshToken: true,
},
global: {
headers: { 'x-my-custom-header': 'my-value' },
},
})
// Configure pool in Supabase dashboard
// Settings > Database > Connection pooling
// Enable query logging
const supabase = createClient(url, key, {
global: {
fetch: async (url, options) => {
console.log('Query:', url)
return fetch(url, options)
}
}
})
// Monitor in Supabase Dashboard
// - Database performance
// - API usage
// - Storage usage
// - Auth activity
# Automatic backups (Pro plan+)
# Daily backups with point-in-time recovery
# Manual backup
pg_dump -h db.xxx.supabase.co -U postgres -d postgres > backup.sql
# Restore
psql -h db.xxx.supabase.co -U postgres -d postgres < backup.sql
Database
Queries
Security
Open Source
Pricing
Ecosystem
// Firestore collection query
const snapshot = await db
.collection('posts')
.where('status', '==', 'published')
.orderBy('createdAt', 'desc')
.limit(10)
.get()
// Equivalent Supabase query
const { data } = await supabase
.from('posts')
.select('*')
.eq('status', 'published')
.order('created_at', { ascending: false })
.limit(10)
// Complex queries easier in Supabase
const { data } = await supabase
.from('posts')
.select(`
*,
author:profiles!inner(name),
comments(count)
`)
.gte('created_at', startDate)
.lte('created_at', endDate)
.order('created_at', { ascending: false })
// Firebase would require multiple queries + client-side joins
'use client'
import { useState, useOptimistic } from 'react'
import { createClient } from '@/lib/supabase/client'
export function PostList({ initialPosts }: { initialPosts: Post[] }) {
const [posts, setPosts] = useState(initialPosts)
const [optimisticPosts, addOptimisticPost] = useOptimistic(
posts,
(state, newPost: Post) => [...state, newPost]
)
const supabase = createClient()
const createPost = async (title: string) => {
const tempPost = {
id: crypto.randomUUID(),
title,
created_at: new Date().toISOString()
}
addOptimisticPost(tempPost)
const { data } = await supabase
.from('posts')
.insert({ title })
.select()
.single()
if (data) {
setPosts([...posts, data])
}
}
return (
<div>
{optimisticPosts.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
)
}
'use client'
import { useState, useEffect } from 'react'
import { createClient } from '@/lib/supabase/client'
const PAGE_SIZE = 20
export function InfinitePostList() {
const [posts, setPosts] = useState<Post[]>([])
const [page, setPage] = useState(0)
const [hasMore, setHasMore] = useState(true)
const supabase = createClient()
useEffect(() => {
const loadMore = async () => {
const { data } = await supabase
.from('posts')
.select('*')
.range(page * PAGE_SIZE, (page + 1) * PAGE_SIZE - 1)
.order('created_at', { ascending: false })
if (data) {
setPosts([...posts, ...data])
setHasMore(data.length === PAGE_SIZE)
}
}
loadMore()
}, [page])
return (
<div>
{posts.map((post) => (
<div key={post.id}>{post.title}</div>
))}
{hasMore && (
<button onClick={() => setPage(page + 1)}>
Load More
</button>
)}
</div>
)
}
'use client'
import { useState, useEffect } from 'react'
import { createClient } from '@/lib/supabase/client'
import { useDebounce } from '@/hooks/use-debounce'
export function SearchPosts() {
const [query, setQuery] = useState('')
const [results, setResults] = useState<Post[]>([])
const debouncedQuery = useDebounce(query, 300)
const supabase = createClient()
useEffect(() => {
if (!debouncedQuery) {
setResults([])
return
}
const search = async () => {
const { data } = await supabase
.from('posts')
.select('*')
.textSearch('title', debouncedQuery)
.limit(10)
if (data) setResults(data)
}
search()
}, [debouncedQuery])
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search posts..."
/>
{results.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
)
}
Supabase provides a complete backend platform with:
Use Supabase when a full-featured backend with the power of Postgres, built-in auth, and realtime capabilities is needed, all with excellent TypeScript and Next.js integration.
Weekly Installs
177
Repository
GitHub Stars
18
First Seen
Jan 23, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
opencode151
gemini-cli145
codex141
claude-code141
cursor132
github-copilot130
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
106,200 周安装