AI 生成服务端
问题
AI 如何生成完整的服务端代码?v0.dev 为什么选择 Supabase 作为后端方案?如何用 AI 生成数据库 Schema、API 接口、认证系统和文件存储?BaaS(Backend as a Service)在 AI 全栈生成中扮演什么角色?
答案
AI 生成服务端是 AI 全栈开发的关键一环。传统后端开发涉及数据库设计、API 编写、认证授权、文件存储、实时通信等多个领域,AI 工具通过结合 BaaS 平台(特别是 Supabase)大幅降低了这些工作的门槛。v0.dev、Bolt.new、Lovable 等工具之所以选择 Supabase,是因为它基于 PostgreSQL、类型安全、API 自动生成,天然适合 AI 代码生成的范式。
AI 生成服务端的本质不是"让 AI 从零写 Express 服务器",而是让 AI 操作声明式的 BaaS 平台——用 SQL 定义 Schema、用 RLS 策略定义权限、用 Edge Functions 写业务逻辑,一切都是"配置即后端"。这种声明式范式和 AI 的生成能力高度契合。
一、AI 全栈生成的趋势与工具格局
1.1 主流 AI 全栈生成工具
| 工具 | 前端框架 | 默认后端 | AI 模型 | 特点 |
|---|---|---|---|---|
| v0.dev | Next.js + shadcn/ui | Supabase | Claude 3.5/4 | UI 生成起家,全栈能力最成熟 |
| Bolt.new | React/Vue/Svelte | 多种 | Claude/GPT | WebContainer 全栈环境 |
| Lovable | React + Vite | Supabase | GPT-4o | 端到端全栈,一键部署 |
| Replit Agent | 多种 | Neon + Replit DB | Claude | 完整 IDE + 部署环境 |
1.2 为什么 BaaS 是 AI 生成服务端的最佳搭档
传统后端 vs BaaS 在 AI 生成场景中的对比:
| 维度 | 传统后端(Express/NestJS) | BaaS(Supabase) |
|---|---|---|
| AI 生成难度 | 高——需要完整项目结构、中间件、ORM | 低——声明式 SQL + 配置 |
| 类型安全 | 需手动定义、容易漏 | 从 Schema 自动生成 TypeScript 类型 |
| API 层 | 需要手写路由、控制器 | PostgREST 自动从表生成 REST API |
| 认证 | 需集成 Passport.js / NextAuth | 内置 Auth,一行代码 |
| 部署 | 需要 Docker、PM2、Nginx | 零部署,托管服务 |
| AI 可验证性 | 需启动服务器才能验证 | SQL 可直接执行验证 |
- SQL 是 LLM 最擅长的语言之一——训练数据中 SQL 占比极高,生成质量远超框架代码
- 声明式 > 命令式——RLS 策略、Schema DDL 都是声明式的,AI 不需要理解运行时逻辑
- 类型自动生成——
supabase gen types typescript直接从数据库 Schema 生成 TypeScript 类型,前后端类型一致 - 开源可自托管——不锁定,也方便 AI 工具集成
二、Supabase 核心架构
2.1 架构总览
2.2 Supabase SDK 初始化
import { createClient } from '@supabase/supabase-js';
import type { Database } from '@/types/supabase'; // AI 生成 / supabase gen types 生成
// 客户端实例(浏览器端,受 RLS 保护)
export const supabase = createClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
// 服务端实例(Server Component / API Route,绕过 RLS)
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';
export async function createServerSupabase() {
const cookieStore = await cookies();
return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll: () => cookieStore.getAll(),
setAll: (cookies) => {
cookies.forEach(({ name, value, options }) => {
cookieStore.set(name, value, options);
});
},
},
}
);
}
- 客户端
createClient:在浏览器运行,所有请求都经过 RLS(Row Level Security),只能访问当前用户有权限的数据 - 服务端
createServerClient:在 Server Component / API Route 中运行,携带用户的 Cookie 进行身份验证,但仍受 RLS 约束 - Admin Client:使用
service_rolekey,绕过 RLS,只在可信的服务端环境中使用(如 Edge Functions、webhook 处理)
三、AI 生成数据库 Schema
数据库设计是 AI 生成服务端最核心的部分——一个好的 Schema 直接决定了 API、权限、查询的质量。
3.1 自然语言到 SQL DDL
AI 擅长将需求描述转化为结构化的 SQL DDL:
-- AI Prompt: "我需要一个博客系统,支持用户发帖、评论、点赞"
-- AI 生成的 Schema:
-- 启用必要的扩展
create extension if not exists "uuid-ossp";
-- 用户 Profile(扩展 auth.users)
create table public.profiles (
id uuid references auth.users on delete cascade primary key,
username text unique not null,
avatar_url text,
bio text,
created_at timestamptz default now() not null,
updated_at timestamptz default now() not null
);
-- 文章
create table public.posts (
id uuid default uuid_generate_v4() primary key,
author_id uuid references public.profiles(id) on delete cascade not null,
title text not null,
content text not null,
slug text unique not null,
published boolean default false,
cover_image text, -- Supabase Storage URL
tags text[] default '{}',
view_count integer default 0,
created_at timestamptz default now() not null,
updated_at timestamptz default now() not null
);
-- 评论
create table public.comments (
id uuid default uuid_generate_v4() primary key,
post_id uuid references public.posts(id) on delete cascade not null,
author_id uuid references public.profiles(id) on delete cascade not null,
content text not null,
parent_id uuid references public.comments(id) on delete cascade, -- 支持嵌套评论
created_at timestamptz default now() not null
);
-- 点赞(使用唯一约束防止重复点赞)
create table public.likes (
id uuid default uuid_generate_v4() primary key,
post_id uuid references public.posts(id) on delete cascade not null,
user_id uuid references public.profiles(id) on delete cascade not null,
created_at timestamptz default now() not null,
unique(post_id, user_id) -- 每个用户对每篇文章只能点赞一次
);
-- 索引优化
create index idx_posts_author on public.posts(author_id);
create index idx_posts_slug on public.posts(slug);
create index idx_posts_published on public.posts(published) where published = true;
create index idx_comments_post on public.comments(post_id);
create index idx_likes_post on public.likes(post_id);
-- 自动更新 updated_at
create or replace function update_updated_at()
returns trigger as $$
begin
new.updated_at = now();
return new;
end;
$$ language plpgsql;
create trigger posts_updated_at
before update on public.posts
for each row execute function update_updated_at();
create trigger profiles_updated_at
before update on public.profiles
for each row execute function update_updated_at();
3.2 AI 生成 RLS 策略
Row Level Security 是 Supabase 的核心安全模型——AI 生成 RLS 策略比手写更可靠,因为 SQL 策略是声明式的:
-- 启用 RLS
alter table public.profiles enable row level security;
alter table public.posts enable row level security;
alter table public.comments enable row level security;
alter table public.likes enable row level security;
-- Profiles: 所有人可读,只有本人可改
create policy "Profiles are viewable by everyone"
on public.profiles for select using (true);
create policy "Users can update own profile"
on public.profiles for update
using (auth.uid() = id)
with check (auth.uid() = id);
-- Posts: 已发布的文章所有人可读,作者可 CRUD
create policy "Published posts are viewable by everyone"
on public.posts for select
using (published = true or auth.uid() = author_id);
create policy "Authors can create posts"
on public.posts for insert
with check (auth.uid() = author_id);
create policy "Authors can update own posts"
on public.posts for update
using (auth.uid() = author_id);
create policy "Authors can delete own posts"
on public.posts for delete
using (auth.uid() = author_id);
-- Comments: 登录用户可评论,本人可删
create policy "Comments are viewable by everyone"
on public.comments for select using (true);
create policy "Authenticated users can comment"
on public.comments for insert
with check (auth.uid() = author_id);
create policy "Authors can delete own comments"
on public.comments for delete
using (auth.uid() = author_id);
-- Likes: 登录用户可点赞/取消
create policy "Likes are viewable by everyone"
on public.likes for select using (true);
create policy "Authenticated users can like"
on public.likes for insert
with check (auth.uid() = user_id);
create policy "Users can unlike"
on public.likes for delete
using (auth.uid() = user_id);
3.3 类型自动生成
Supabase CLI 从数据库 Schema 自动生成 TypeScript 类型,这是 AI 全栈生成的关键——前端代码可以直接使用:
- npm
- Yarn
- pnpm
- Bun
npm install supabase --save-dev
yarn add supabase --dev
pnpm add supabase --save-dev
bun add supabase --dev
npx supabase gen types typescript --project-id your-project-id > types/supabase.ts
生成的类型文件:
export type Database = {
public: {
Tables: {
profiles: {
Row: {
id: string;
username: string;
avatar_url: string | null;
bio: string | null;
created_at: string;
updated_at: string;
};
Insert: {
id: string;
username: string;
avatar_url?: string | null;
bio?: string | null;
};
Update: {
username?: string;
avatar_url?: string | null;
bio?: string | null;
};
};
posts: {
Row: {
id: string;
author_id: string;
title: string;
content: string;
slug: string;
published: boolean;
cover_image: string | null;
tags: string[];
view_count: number;
created_at: string;
updated_at: string;
};
Insert: {
author_id: string;
title: string;
content: string;
slug: string;
published?: boolean;
cover_image?: string | null;
tags?: string[];
};
Update: {
title?: string;
content?: string;
slug?: string;
published?: boolean;
cover_image?: string | null;
tags?: string[];
};
};
// ... comments, likes 类似
};
};
};
// 便捷类型别名
type Tables<T extends keyof Database['public']['Tables']> =
Database['public']['Tables'][T]['Row'];
type InsertDTO<T extends keyof Database['public']['Tables']> =
Database['public']['Tables'][T]['Insert'];
type UpdateDTO<T extends keyof Database['public']['Tables']> =
Database['public']['Tables'][T]['Update'];
// 使用:
type Post = Tables<'posts'>; // 完整的行类型
type NewPost = InsertDTO<'posts'>; // 插入时需要的字段
有了自动生成的类型,所有 Supabase 查询都有完整的类型提示:
supabase.from('posts').select('*')的返回值自动是Post[]supabase.from('posts').insert({...})会检查必填字段- 重命名字段后,TypeScript 编译器会标出所有需要修改的地方
这使得 AI 生成的前端代码和后端 Schema 之间类型始终一致。
四、AI 生成 API 接口
Supabase 提供两层 API:PostgREST 自动 API 和 Edge Functions 自定义逻辑。
4.1 PostgREST 自动 API
创建表后,Supabase 自动生成 REST API——无需编写任何路由代码:
import { supabase } from '@/lib/supabase';
import type { Tables, InsertDTO } from '@/types/supabase';
// 获取文章列表(自动分页)
export async function getPosts(page = 1, pageSize = 10) {
const from = (page - 1) * pageSize;
const to = from + pageSize - 1;
const { data, error, count } = await supabase
.from('posts')
.select('*, profiles(username, avatar_url), likes(count)', {
count: 'exact', // 返回总数用于分页
})
.eq('published', true)
.order('created_at', { ascending: false })
.range(from, to); // 分页
if (error) throw error;
return { posts: data, total: count };
}
// 获取单篇文章(含评论和点赞数)
export async function getPost(slug: string) {
const { data, error } = await supabase
.from('posts')
.select(`
*,
profiles(username, avatar_url),
comments(
id, content, created_at,
profiles(username, avatar_url),
parent_id
),
likes(count)
`)
.eq('slug', slug)
.single(); // 期望返回一条记录
if (error) throw error;
return data;
}
// 创建文章
export async function createPost(post: InsertDTO<'posts'>) {
const { data, error } = await supabase
.from('posts')
.insert(post)
.select()
.single();
if (error) throw error;
return data;
}
// 切换点赞
export async function toggleLike(postId: string, userId: string) {
// 先检查是否已点赞
const { data: existing } = await supabase
.from('likes')
.select('id')
.eq('post_id', postId)
.eq('user_id', userId)
.maybeSingle();
if (existing) {
// 取消点赞
await supabase.from('likes').delete().eq('id', existing.id);
return { liked: false };
} else {
// 点赞
await supabase.from('likes').insert({ post_id: postId, user_id: userId });
return { liked: true };
}
}
// 全文搜索(使用 PostgreSQL 内置的 tsvector)
export async function searchPosts(query: string) {
const { data, error } = await supabase
.from('posts')
.select('id, title, slug, created_at')
.textSearch('title', query, { type: 'websearch' })
.eq('published', true)
.limit(20);
if (error) throw error;
return data;
}
4.2 Supabase Edge Functions(Deno)
对于 PostgREST 无法满足的复杂逻辑(如调用外部 API、发送邮件、AI 处理),使用 Edge Functions:
// Edge Functions 使用 Deno 运行时
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};
serve(async (req: Request) => {
// CORS 预检
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders });
}
try {
// 从请求头获取用户的 JWT,创建受 RLS 保护的客户端
const authHeader = req.headers.get('Authorization')!;
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!,
{ global: { headers: { Authorization: authHeader } } }
);
const { postId } = await req.json();
// 获取文章内容
const { data: post } = await supabase
.from('posts')
.select('title, content')
.eq('id', postId)
.single();
if (!post) {
return new Response(JSON.stringify({ error: 'Post not found' }), {
status: 404,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
// 调用 AI 生成摘要
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${Deno.env.get('OPENAI_API_KEY')}`,
},
body: JSON.stringify({
model: 'gpt-4o-mini',
messages: [
{ role: 'system', content: '用中文生成简洁的文章摘要,不超过 200 字' },
{ role: 'user', content: `标题:${post.title}\n\n内容:${post.content}` },
],
max_tokens: 300,
}),
});
const aiResult = await response.json();
const summary = aiResult.choices[0].message.content;
// 更新文章摘要
await supabase
.from('posts')
.update({ summary })
.eq('id', postId);
return new Response(JSON.stringify({ summary }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
});
客户端调用 Edge Function:
export async function generateSummary(postId: string) {
const { data, error } = await supabase.functions.invoke('generate-summary', {
body: { postId },
});
if (error) throw error;
return data.summary as string;
}
4.3 Database Functions(RPC)
对于纯数据库逻辑,用 PostgreSQL Function + RPC 比 Edge Function 更高效:
-- 获取热门文章(点赞数 + 浏览量加权排序)
create or replace function get_trending_posts(
time_range interval default '7 days',
limit_count integer default 10
)
returns table (
id uuid,
title text,
slug text,
author_name text,
like_count bigint,
view_count integer,
score numeric -- 综合热度评分
) as $$
begin
return query
select
p.id,
p.title,
p.slug,
pr.username as author_name,
count(l.id) as like_count,
p.view_count,
-- 热度评分公式:点赞权重 3 + 浏览权重 1,时间衰减
(count(l.id) * 3 + p.view_count) *
exp(-extract(epoch from now() - p.created_at) / 86400 / 7) as score
from public.posts p
join public.profiles pr on p.author_id = pr.id
left join public.likes l on p.id = l.post_id
where p.published = true
and p.created_at > now() - time_range
group by p.id, pr.username
order by score desc
limit limit_count;
end;
$$ language plpgsql stable;
TypeScript 调用 RPC:
export async function getTrendingPosts() {
const { data, error } = await supabase
.rpc('get_trending_posts', { time_range: '7 days', limit_count: 10 });
if (error) throw error;
return data;
}
五、AI 生成认证系统
5.1 Supabase Auth 集成
Supabase Auth(基于 GoTrue)提供开箱即用的认证方案,AI 可以直接生成完整的登录/注册流程:
'use server';
import { createServerSupabase } from '@/lib/supabase';
import { redirect } from 'next/navigation';
// 邮箱密码注册
export async function signUp(formData: FormData) {
const supabase = await createServerSupabase();
const email = formData.get('email') as string;
const password = formData.get('password') as string;
const username = formData.get('username') as string;
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
data: { username }, // 存入 auth.users.raw_user_meta_data
},
});
if (error) return { error: error.message };
redirect('/auth/verify-email');
}
// OAuth 登录(GitHub / Google)
export async function signInWithOAuth(provider: 'github' | 'google') {
const supabase = await createServerSupabase();
const { data, error } = await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
},
});
if (error) return { error: error.message };
redirect(data.url);
}
// 登出
export async function signOut() {
const supabase = await createServerSupabase();
await supabase.auth.signOut();
redirect('/');
}
5.2 自动创建 Profile(Database Trigger)
注册时自动在 profiles 表创建对应记录——这是 AI 经常生成的模式:
-- 用户注册时自动创建 profile
create or replace function public.handle_new_user()
returns trigger as $$
begin
insert into public.profiles (id, username, avatar_url)
values (
new.id,
coalesce(new.raw_user_meta_data->>'username', split_part(new.email, '@', 1)),
new.raw_user_meta_data->>'avatar_url'
);
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();
5.3 Next.js Middleware 路由保护
import { createServerClient } from '@supabase/ssr';
import { NextResponse, type NextRequest } from 'next/server';
export async function middleware(request: NextRequest) {
let response = NextResponse.next({ request });
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll: () => request.cookies.getAll(),
setAll: (cookies) => {
cookies.forEach(({ name, value, options }) => {
response.cookies.set(name, value, options);
});
},
},
}
);
// 刷新 session(重要!否则 token 过期后用户会被登出)
const { data: { user } } = await supabase.auth.getUser();
// 未登录用户访问受保护页面 → 重定向到登录
const protectedPaths = ['/dashboard', '/posts/new', '/settings'];
const isProtected = protectedPaths.some(path =>
request.nextUrl.pathname.startsWith(path)
);
if (isProtected && !user) {
return NextResponse.redirect(new URL('/auth/login', request.url));
}
return response;
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|api/).*)'],
};
六、AI 生成文件存储
6.1 Supabase Storage 文件上传
import { supabase } from '@/lib/supabase';
// 上传图片到 Supabase Storage
export async function uploadImage(
file: File,
bucket: string = 'images',
folder: string = 'posts'
) {
// 生成唯一文件名
const ext = file.name.split('.').pop();
const fileName = `${folder}/${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`;
const { data, error } = await supabase.storage
.from(bucket)
.upload(fileName, file, {
cacheControl: '3600',
upsert: false,
contentType: file.type,
});
if (error) throw error;
// 获取公开 URL
const { data: { publicUrl } } = supabase.storage
.from(bucket)
.getPublicUrl(data.path);
return publicUrl;
}
// 上传头像(带裁剪 + 压缩)
export async function uploadAvatar(file: File, userId: string) {
// 压缩图片
const compressed = await compressImage(file, { maxWidth: 256, quality: 0.8 });
const fileName = `avatars/${userId}.webp`;
const { error } = await supabase.storage
.from('avatars')
.upload(fileName, compressed, { upsert: true }); // 覆盖旧头像
if (error) throw error;
const { data: { publicUrl } } = supabase.storage
.from('avatars')
.getPublicUrl(fileName);
// 同步更新 profile
await supabase
.from('profiles')
.update({ avatar_url: publicUrl })
.eq('id', userId);
return publicUrl;
}
// 辅助:图片压缩
async function compressImage(
file: File,
opts: { maxWidth: number; quality: number }
): Promise<Blob> {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
const ratio = Math.min(1, opts.maxWidth / img.width);
canvas.width = img.width * ratio;
canvas.height = img.height * ratio;
const ctx = canvas.getContext('2d')!;
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
canvas.toBlob((blob) => resolve(blob!), 'image/webp', opts.quality);
};
img.src = URL.createObjectURL(file);
});
}
6.2 Storage Bucket RLS 策略
-- 创建存储桶
insert into storage.buckets (id, name, public) values ('images', 'images', true);
insert into storage.buckets (id, name, public) values ('avatars', 'avatars', true);
-- images 桶策略:登录用户可上传,所有人可读
create policy "Anyone can view images"
on storage.objects for select
using (bucket_id = 'images');
create policy "Authenticated users can upload images"
on storage.objects for insert
with check (
bucket_id = 'images'
and auth.role() = 'authenticated'
and (storage.foldername(name))[1] = 'posts' -- 只能上传到 posts/ 目录
);
-- avatars 桶策略:只能上传/修改自己的头像
create policy "Anyone can view avatars"
on storage.objects for select
using (bucket_id = 'avatars');
create policy "Users can upload own avatar"
on storage.objects for insert
with check (
bucket_id = 'avatars'
and (storage.foldername(name))[1] = 'avatars'
and auth.uid()::text = split_part(storage.filename(name), '.', 1)
);
create policy "Users can update own avatar"
on storage.objects for update
using (
bucket_id = 'avatars'
and auth.uid()::text = split_part(storage.filename(name), '.', 1)
);
七、AI 生成 Realtime 功能
Supabase Realtime 提供三种实时机制,AI 可以根据需求自动选择:
| 机制 | 原理 | 适用场景 | 示例 |
|---|---|---|---|
| Postgres Changes | 监听表的 INSERT/UPDATE/DELETE | 数据变更同步 | 新评论实时显示 |
| Broadcast | 服务器转发客户端消息 | 临时通信 | 在线协作光标 |
| Presence | 共享状态同步 | 在线状态 | "3 人正在查看" |
import { useEffect, useState } from 'react';
import { supabase } from '@/lib/supabase';
import type { Tables } from '@/types/supabase';
type Comment = Tables<'comments'> & {
profiles: { username: string; avatar_url: string | null };
};
export function useRealtimeComments(postId: string) {
const [comments, setComments] = useState<Comment[]>([]);
useEffect(() => {
// 初始加载
supabase
.from('comments')
.select('*, profiles(username, avatar_url)')
.eq('post_id', postId)
.order('created_at')
.then(({ data }) => {
if (data) setComments(data as Comment[]);
});
// 实时监听新评论
const channel = supabase
.channel(`comments:${postId}`)
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'comments',
filter: `post_id=eq.${postId}`,
},
async (payload) => {
// 新评论到达,查询完整数据(包含 profile)
const { data } = await supabase
.from('comments')
.select('*, profiles(username, avatar_url)')
.eq('id', payload.new.id)
.single();
if (data) {
setComments(prev => [...prev, data as Comment]);
}
}
)
.on(
'postgres_changes',
{
event: 'DELETE',
schema: 'public',
table: 'comments',
filter: `post_id=eq.${postId}`,
},
(payload) => {
setComments(prev => prev.filter(c => c.id !== payload.old.id));
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, [postId]);
return comments;
}
在线状态(Presence):
import { useEffect, useState } from 'react';
import { supabase } from '@/lib/supabase';
interface OnlineUser {
userId: string;
username: string;
avatarUrl: string | null;
}
export function useOnlineUsers(roomId: string, currentUser: OnlineUser) {
const [users, setUsers] = useState<OnlineUser[]>([]);
useEffect(() => {
const channel = supabase.channel(`room:${roomId}`);
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState<OnlineUser>();
const onlineUsers = Object.values(state).flat();
setUsers(onlineUsers);
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track(currentUser);
}
});
return () => {
supabase.removeChannel(channel);
};
}, [roomId, currentUser]);
return users;
}
八、v0.dev 全栈生成实战
8.1 从 Prompt 到完整应用
v0.dev 生成全栈应用的典型流程:
8.2 v0.dev 生成的项目结构
my-blog/
├── app/
│ ├── layout.tsx # Root layout + Supabase Provider
│ ├── page.tsx # 首页(Server Component)
│ ├── auth/
│ │ ├── login/page.tsx # 登录页
│ │ ├── signup/page.tsx # 注册页
│ │ ├── callback/route.ts # OAuth 回调
│ │ └── actions.ts # Server Actions
│ ├── posts/
│ │ ├── page.tsx # 文章列表
│ │ ├── [slug]/page.tsx # 文章详情
│ │ └── new/page.tsx # 新建文章
│ └── dashboard/
│ └── page.tsx # 用户面板
├── components/
│ ├── ui/ # shadcn/ui 组件
│ ├── post-card.tsx
│ ├── comment-section.tsx
│ ├── like-button.tsx
│ └── markdown-editor.tsx
├── lib/
│ ├── supabase.ts # Supabase 客户端
│ └── api/ # 数据访问层
│ ├── posts.ts
│ ├── comments.ts
│ └── storage.ts
├── hooks/
│ ├── useRealtimeComments.ts
│ └── useOnlineUsers.ts
├── types/
│ └── supabase.ts # 自动生成的类型
├── supabase/
│ ├── config.toml
│ └── migrations/
│ ├── 001_initial_schema.sql
│ ├── 002_rls_policies.sql
│ ├── 003_functions.sql
│ ├── 004_auth_trigger.sql
│ └── 005_storage_policies.sql
├── middleware.ts # 路由保护
└── .env.local # Supabase 密钥
8.3 Server Component 数据获取模式
v0.dev 生成的 Next.js + Supabase 代码大量使用 Server Components 直接查询数据库:
import { createServerSupabase } from '@/lib/supabase';
import { notFound } from 'next/navigation';
import { CommentSection } from '@/components/comment-section';
import { LikeButton } from '@/components/like-button';
import { Markdown } from '@/components/markdown';
// Server Component:直接在服务端查询,零客户端 JS
export default async function PostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const supabase = await createServerSupabase();
// 获取当前用户
const { data: { user } } = await supabase.auth.getUser();
// 获取文章
const { data: post } = await supabase
.from('posts')
.select(`
*,
profiles(username, avatar_url),
likes(count)
`)
.eq('slug', slug)
.single();
if (!post) notFound();
// 检查当前用户是否已点赞
let hasLiked = false;
if (user) {
const { data } = await supabase
.from('likes')
.select('id')
.eq('post_id', post.id)
.eq('user_id', user.id)
.maybeSingle();
hasLiked = !!data;
}
// 增加浏览量(fire-and-forget)
supabase.rpc('increment_view_count', { post_id: post.id });
return (
<article className="max-w-3xl mx-auto py-8">
<h1 className="text-3xl font-bold">{post.title}</h1>
<div className="flex items-center gap-2 mt-4 text-muted-foreground">
<span>{post.profiles.username}</span>
<span>·</span>
<time>{new Date(post.created_at).toLocaleDateString('zh-CN')}</time>
</div>
<Markdown content={post.content} />
{/* highlight-start */}
{/* Client Components:需要交互的部分 */}
<LikeButton
postId={post.id}
initialCount={post.likes[0]?.count ?? 0}
initialLiked={hasLiked}
/>
<CommentSection postId={post.id} />
{/* highlight-end */}
</article>
);
}
九、BaaS 平台对比
| 维度 | Supabase | Firebase | PlanetScale | Neon |
|---|---|---|---|---|
| 数据库 | PostgreSQL | Firestore(NoSQL) | MySQL(Vitess) | PostgreSQL |
| API 自动生成 | PostgREST(自动 REST) | Firebase SDK | 无 | 无 |
| 认证 | GoTrue(内置) | Firebase Auth(内置) | 无 | 无 |
| 文件存储 | Storage(S3 兼容) | Cloud Storage | 无 | 无 |
| 实时 | Realtime(WebSocket) | Realtime Database | 无 | 无 |
| Serverless 函数 | Edge Functions(Deno) | Cloud Functions | 无 | 无 |
| AI 生成友好度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ |
| 类型安全 | supabase gen types | 弱(需手动定义) | Prisma | Prisma/Drizzle |
| 开源 | 是 | 否 | 否 | 是 |
| 定价 | 免费额度大方 | 免费额度中等 | 免费 Hobby 计划 | 免费 0.5GB |
| SQL 支持 | 完整 PostgreSQL | 无(NoSQL) | MySQL | PostgreSQL |
| AI 工具集成 | v0、Lovable、Bolt | Bolt、Firebase Studio | 无 | Replit |
- SQL 是 LLM 最擅长的语言——Firebase 的 NoSQL 查询语法训练数据少,AI 生成质量低
- 全栈一体化——不需要拼凑多个服务(Auth0 + S3 + Pusher),减少 AI 生成的出错概率
- RLS 声明式权限——比中间件代码更适合 AI 生成和验证
supabase gen types——自动类型生成让前后端类型一致,AI 生成的代码不会有类型错误
十、AI 生成服务端的最佳实践
10.1 Prompt 设计原则
// v0.dev 内部使用的 System Prompt 核心原则(推测):
const backendGenerationPrompt = `
你是一个全栈开发助手,使用 Next.js + Supabase 技术栈。
## 数据库设计原则
1. 所有表使用 UUID 主键(uuid_generate_v4)
2. 包含 created_at 和 updated_at 时间戳
3. 外键使用 ON DELETE CASCADE
4. 为频繁查询的字段创建索引
5. 使用 PostgreSQL 数组和 JSONB 代替关联表(适用于简单场景)
## RLS 策略原则
1. 所有表必须启用 RLS
2. SELECT 策略:公开数据用 using(true),私有数据用 using(auth.uid() = user_id)
3. INSERT 策略:with check(auth.uid() = user_id)
4. UPDATE/DELETE 策略:using(auth.uid() = user_id)
5. 管理员操作使用 service_role 客户端
## API 层原则
1. 优先使用 PostgREST 自动 API(supabase.from().select())
2. 复杂查询使用 Database Function + RPC
3. 涉及外部服务调用时使用 Edge Functions
4. 所有 Edge Functions 包含 CORS 头
## 类型安全原则
1. 始终使用 supabase gen types 生成的类型
2. 定义 Tables<T>、InsertDTO<T>、UpdateDTO<T> 便捷别名
3. API 函数的入参和返回值都必须有类型
`;
10.2 Migration 管理
# 本地开发流程
npx supabase start # 启动本地 Supabase
npx supabase migration new add_tags # 创建新 migration
npx supabase db reset # 重置并重跑所有 migrations
npx supabase gen types typescript # 重新生成类型
# 部署到生产
npx supabase link --project-ref xxx # 连接远程项目
npx supabase db push # 推送 migrations
- 永远不要修改已推送的 migration——只创建新 migration 做变更
- RLS 策略要分开写——Schema 和 RLS 分成不同的 migration 文件,方便调试
- 测试 RLS——用不同身份执行查询,确认权限正确。AI 生成的 RLS 策略中
using和with check容易混淆 - 检查索引——AI 可能生成冗余索引,或遗漏关键索引
常见面试问题
Q1: 为什么 v0.dev、Lovable 等 AI 工具选择 Supabase 作为默认后端?
答案:
核心原因是 Supabase 的声明式特性与 AI 代码生成的范式高度契合:
| 维度 | Supabase 的优势 | 传统后端的劣势 |
|---|---|---|
| Schema 定义 | SQL DDL——LLM 训练数据中 SQL 占比极高 | ORM 配置——各框架语法差异大 |
| API 层 | PostgREST 自动生成——建表即有 API | 手写路由/控制器——代码量大、易出错 |
| 权限 | RLS 声明式策略——SQL 语句 | 中间件代码——逻辑分散、难验证 |
| 类型 | supabase gen types——自动生成 | 手动定义——前后端易不一致 |
| 认证 | 内置 Auth——一行代码 | 集成 NextAuth/Passport——配置复杂 |
| 验证 | SQL 可直接执行验证 | 需启动服务器 |
关键洞察:AI 生成的代码需要可验证性。SQL 可以直接在数据库中执行验证结果是否正确,而传统后端代码需要启动整个服务器才能测试。这种"写了就能验证"的特性是 AI 工具选择 BaaS 的根本原因。
Q2: Supabase 的 Row Level Security(RLS)是什么?为什么它比传统中间件更适合 AI 生成?
答案:
RLS 是 PostgreSQL 原生的行级安全策略,控制哪些用户可以访问哪些行数据。每条策略是一个 SQL 布尔表达式,数据库会自动在所有查询中附加这个条件。
-- 传统方式:中间件检查
-- app.get('/posts', authMiddleware, async (req, res) => {
-- const posts = await db.query('SELECT * FROM posts WHERE author_id = $1', [req.user.id]);
-- });
-- Supabase RLS:声明式策略
create policy "Users see own posts"
on posts for select
using (auth.uid() = author_id);
-- 之后 supabase.from('posts').select('*') 自动只返回当前用户的文章
RLS 比中间件更适合 AI 生成的原因:
- 不会遗漏——RLS 在数据库层面强制执行,即使 AI 忘了在某个 API 中加权限检查也不会泄露数据
- 声明式——一条策略覆盖所有查询路径(API、RPC、Realtime),中间件需要每个路由都加
- 可审计——所有策略在
pg_policies表中集中管理,可以SELECT * FROM pg_policies一次性审查 - 可测试——切换不同 JWT 执行同一查询即可验证
Q3: Supabase 客户端、服务端、Admin 三种客户端有什么区别?什么时候用哪个?
答案:
// 1. 客户端 Client(浏览器)
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(url, anonKey);
// - 用 anon key(公开的,限制低权限)
// - 受 RLS 完全约束
// - 用于 Client Components 的数据获取和用户交互
// 2. 服务端 Client(Server Component / API Route / Middleware)
import { createServerClient } from '@supabase/ssr';
const supabase = createServerClient(url, anonKey, { cookies: {...} });
// - 用 anon key + 用户的 Cookie/JWT
// - 受 RLS 约束,但以当前登录用户身份执行
// - 用于 Server Components 数据获取、Middleware 路由保护
// 3. Admin Client(仅后端可信环境)
const supabaseAdmin = createClient(url, serviceRoleKey);
// - 用 service_role key(机密,绝不暴露给前端)
// - 绕过 RLS
// - 用于 Edge Functions 中的管理操作、Webhook 处理、数据迁移
| 客户端类型 | Key | RLS | 运行环境 | 典型场景 |
|---|---|---|---|---|
| Browser Client | anon | 受限 | 浏览器 | 用户操作(点赞、评论) |
| Server Client | anon + cookie | 受限 | Node.js | SSR 数据获取 |
| Admin Client | service_role | 绕过 | Edge Functions | 后台任务、Webhook |
service_role key 有完整的数据库权限,绝不能暴露到客户端。AI 生成的代码中如果在客户端使用了 service_role key,这是严重的安全漏洞。审查 AI 代码时首先检查这一点。
Q4: PostgREST 自动 API 和 Edge Functions 各自的使用场景是什么?
答案:
| 维度 | PostgREST(自动 API) | Edge Functions |
|---|---|---|
| 原理 | 自动将 PostgreSQL 表/视图映射为 REST API | Deno 运行时执行自定义逻辑 |
| 代码量 | 零——建表即有 API | 需要编写函数代码 |
| 性能 | 极快——直接 SQL 查询 | 有冷启动(约 200-500ms) |
| 适用 | CRUD、关联查询、分页、过滤、排序 | 调用外部 API、发邮件、AI 处理、复杂业务逻辑 |
| 权限 | RLS 控制 | JWT 验证 + 自定义检查 |
| 限制 | 只能操作数据库 | 可以做任何事 |
选择策略:
需要操作数据库?
├── 是 → 能用 supabase.from().select/insert/update/delete 完成?
│ ├── 是 → 用 PostgREST(零代码)
│ └── 否 → 需要复杂计算/聚合?
│ ├── 是 → 用 Database Function + RPC
│ └── 否 → 用 Edge Functions
└── 否 → 需要调用外部服务(发邮件、AI API、支付)?
└── 是 → 用 Edge Functions
Q5: 如何用 AI 生成类型安全的 Supabase 数据库 Schema?有什么最佳实践?
答案:
最佳实践是让 AI 按以下模式生成 Schema:
- 每个 Migration 单一职责:Schema → RLS → Functions → Triggers 分开
- 使用标准模式:
- UUID 主键(
uuid_generate_v4()) created_at/updated_at时间戳- 外键
ON DELETE CASCADE - 频繁查询字段加索引
- UUID 主键(
- 类型生成流水线:
# Schema 变更 → 类型生成 → 前端使用
npx supabase db reset # 应用所有 migrations
npx supabase gen types typescript --local > types/supabase.ts
- 定义便捷类型别名:
// 从生成的类型中提取便捷别名
type Tables<T extends keyof Database['public']['Tables']> =
Database['public']['Tables'][T]['Row'];
type Enums<T extends keyof Database['public']['Enums']> =
Database['public']['Enums'][T];
// 使用时
type Post = Tables<'posts'>; // { id: string; title: string; ... }
type PostStatus = Enums<'post_status'>; // 'draft' | 'published' | 'archived'
- AI 生成后必须检查:
- RLS 策略是否覆盖所有表和操作(SELECT/INSERT/UPDATE/DELETE)
- 索引是否覆盖高频查询路径
- 外键约束是否正确
- 是否有不必要的冗余字段
Q6: Supabase Realtime 有哪些机制?分别适用什么场景?
答案:
三种机制及其适用场景:
1. Postgres Changes(数据库变更监听)
// 监听 posts 表的新增
supabase
.channel('new-posts')
.on('postgres_changes', {
event: 'INSERT',
schema: 'public',
table: 'posts',
filter: 'published=eq.true', // 可选过滤
}, (payload) => {
console.log('新文章:', payload.new);
})
.subscribe();
- 适用:评论实时显示、订单状态更新、消息通知
- 原理:PostgreSQL WAL → Realtime 服务 → WebSocket 推送
- 注意:受 RLS 约束,用户只能收到有权限查看的行的变更
2. Broadcast(广播消息)
// 发送光标位置
channel.send({
type: 'broadcast',
event: 'cursor-move',
payload: { x: 100, y: 200, userId: 'xxx' },
});
// 接收
channel.on('broadcast', { event: 'cursor-move' }, (payload) => {
updateCursor(payload.payload);
});
- 适用:协同编辑光标、游戏状态同步、临时通知
- 原理:客户端 → 服务器 → 广播给同频道其他客户端
- 不经过数据库,不持久化
3. Presence(在线状态)
// 进入房间
channel.track({ userId: 'xxx', username: 'Alice', status: 'online' });
// 同步所有在线用户
channel.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState();
// { userId1: [{ userId, username, status }], ... }
});
- 适用:"谁在线"、"3 人正在编辑"、打字状态指示器
- 原理:基于 CRDT 的状态同步,服务器维护最终一致状态
Q7: AI 生成的 Supabase 后端代码,有哪些常见的安全问题需要人工审查?
答案:
| 风险等级 | 问题 | AI 常犯的错误 | 检查方法 |
|---|---|---|---|
| 🔴 严重 | service_role key 暴露 | 在客户端代码中使用 service_role | 搜索 SUPABASE_SERVICE_ROLE 在非服务端文件中的使用 |
| 🔴 严重 | RLS 未启用 | 建表后忘记 enable row level security | SELECT tablename FROM pg_tables WHERE NOT rowsecurity |
| 🟡 高 | RLS 策略缺失 | 有 SELECT 策略但缺少 INSERT/UPDATE/DELETE | 对每个表检查四种操作的策略 |
| 🟡 高 | 过度宽松的策略 | using(true) 用在不应公开的表上 | 审查所有 using(true) 策略 |
| 🟠 中 | Storage 策略缺失 | 上传桶没有策略,任何人可上传 | 检查 storage.objects 的策略 |
| 🟠 中 | 缺少输入验证 | Edge Function 没有验证请求体 | 审查所有 req.json() 后的代码 |
| 🟢 低 | 缺少索引 | 查询频繁但没有索引 | EXPLAIN ANALYZE 关键查询 |
安全审查清单:
# 1. 检查所有表是否启用 RLS
SELECT schemaname, tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public';
# 2. 检查所有 RLS 策略
SELECT tablename, policyname, cmd, qual, with_check
FROM pg_policies
WHERE schemaname = 'public';
# 3. 检查 Storage 策略
SELECT * FROM storage.objects;
Q8: Supabase Edge Functions 和 Vercel API Routes / Next.js Server Actions 有什么区别?什么时候用哪个?
答案:
| 维度 | Edge Functions | Vercel API Routes | Server Actions |
|---|---|---|---|
| 运行时 | Deno | Node.js | Node.js |
| 部署 | Supabase 平台 | Vercel 平台 | Vercel 平台 |
| 触发方式 | HTTP 请求 / Webhook | HTTP 请求 | 表单提交 / 函数调用 |
| Supabase 访问 | 直接访问(同网络) | 通过 SDK(外部网络) | 通过 SDK(外部网络) |
| 冷启动 | ~200ms | ~50ms(Edge Runtime)/ ~500ms(Node) | 同 API Routes |
| 适用 | Webhook 处理、定时任务、Supabase 生态内 | 第三方 API 集成 | 表单操作、数据变更 |
选择策略:
- 操作 Supabase 数据 + 需要 Admin 权限:Edge Functions(与 Supabase 同网络,延迟更低)
- 处理 Supabase Webhook:Edge Functions(Supabase 原生支持)
- 集成第三方服务(Stripe 支付、SendGrid 邮件):API Routes(Node.js 生态更完整)
- 表单提交、简单数据变更:Server Actions(最简洁,无需定义 API 路由)
Q9: 如何在 AI 生成的项目中管理 Supabase Migrations?
答案:
Migration 管理是 AI 全栈项目的关键环节:
# 开发流程
supabase start # 启动本地 Supabase
supabase migration new create_posts # 创建新 migration 文件
# AI 生成 SQL 写入 migration 文件
supabase db reset # 重建本地数据库
supabase gen types typescript --local > types/supabase.ts # 更新类型
# 部署流程
supabase link --project-ref xxx # 连接远程项目
supabase db push # 推送 migrations 到生产
关键规则:
- 已推送的 migration 不可修改——只能创建新 migration 做变更
- AI 生成的 migration 必须 review——特别关注 RLS 策略和破坏性操作(DROP)
- 使用
supabase db diff检测差异——如果在 Dashboard 上手动修改了 Schema,用 diff 生成 migration - Seed 数据分离——测试数据放在
supabase/seed.sql,不要和 Schema migration 混在一起
Q10: BaaS(Supabase)和传统自建后端(NestJS/Express),在什么场景下分别更合适?
答案:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| MVP / 原型验证 | Supabase | 开发速度最快,零部署 |
| AI 生成全栈应用 | Supabase | 声明式、类型安全、AI 友好 |
| CRUD 为主的业务 | Supabase | PostgREST 自动 API 足够 |
| 复杂业务逻辑 | NestJS/Express | 需要完整的中间件、拦截器、守卫 |
| 微服务架构 | NestJS | 需要模块化、依赖注入、消息队列 |
| 多数据库 | NestJS + Prisma | Supabase 绑定 PostgreSQL |
| 高度定制化 API | NestJS/Express | PostgREST 的查询方式有限制 |
| 合规要求(数据不出境) | Supabase 自托管 / NestJS | 托管 Supabase 数据在海外 |
混合方案(v0.dev 的实践):
- Supabase 负责:数据库、Auth、Storage、Realtime
- Next.js API Routes / Server Actions 负责:复杂业务逻辑、第三方集成
- Edge Functions 负责:Webhook 处理、定时任务
这种混合架构兼顾了开发效率和灵活性。
Q11: 如何优化 Supabase 查询性能?
答案:
- 选择性 Select(避免
select('*')):
// 差:获取所有字段
const { data } = await supabase.from('posts').select('*');
// 好:只获取需要的字段
const { data } = await supabase.from('posts').select('id, title, slug, created_at');
// 好:关联查询也只取需要的字段
const { data } = await supabase
.from('posts')
.select('id, title, profiles(username)');
- 使用数据库函数替代多次查询:
-- 差:前端发 3 次请求(文章 + 评论数 + 点赞数)
-- 好:一个 RPC 返回所有信息
create function get_post_detail(post_slug text)
returns json as $$
select json_build_object(
'post', row_to_json(p),
'comment_count', (select count(*) from comments where post_id = p.id),
'like_count', (select count(*) from likes where post_id = p.id)
)
from posts p where p.slug = post_slug;
$$ language sql stable;
-
分页用
range()而非limit()+offset():range(0, 9)内部使用 LIMIT/OFFSET,适合前几页- 大数据集用 cursor-based 分页(keyset pagination)
-
索引优化:
- 高频 WHERE 字段加索引
- 组合查询加复合索引
- 全文搜索用 GIN 索引 +
tsvector
-
使用
count: 'exact'谨慎——大表的 COUNT 很慢,考虑用count: 'estimated'
Q12: 描述一下 v0.dev 从用户输入 Prompt 到生成完整全栈应用的技术流程。
答案:
各阶段详解:
- 需求理解:LLM 从 Prompt 中提取实体(用户、文章、评论)、关系(一对多、多对多)、行为(CRUD、搜索、实时)
- 数据库层:先生成 Schema DDL,再生成 RLS 策略,然后生成辅助函数(触发器、RPC),最后自动生成 TypeScript 类型
- API 层:根据类型生成类型安全的数据访问函数,简单 CRUD 用 PostgREST,表单用 Server Actions,复杂逻辑用 Edge Functions
- 前端层:数据获取逻辑放在 Server Components(零 JS),交互放在 Client Components,UI 使用 shadcn/ui
- 基础设施:配置认证流程(OAuth 回调、Middleware 路由保护)、存储桶(Bucket + 策略)、实时通道
关键优化:v0.dev 不是一次性生成所有代码,而是分轮迭代——先做最核心的(数据库 + CRUD),用户确认后再做增强(Auth、Realtime、搜索),这样每轮的上下文更集中,生成质量更高。