mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2025-12-16 05:25:11 +00:00
This commit adds complete production-ready templates optimized for Next.js App Router + Supabase stack. Part 1: LOVABLE_CLONE_NEXTJS_SUPABASE.md (~925 lines) - Complete project structure for Next.js 14 App Router - Full Supabase database schema (SQL migration) * Tables: profiles, projects, conversations, messages, deployments, usage, project_files * Row Level Security (RLS) policies for all tables * Helper functions (get_monthly_usage, can_generate) * Triggers for updated_at timestamps * Realtime enabled for messages, project_files, deployments * Proper indexes and foreign keys - Supabase client setup * Browser client with SSR * Server client with cookies * Admin client with service role - Middleware for auth session refresh - Complete authentication system * Email/password signup & login * OAuth (Google + GitHub) * Auth helpers and hooks * Protected routes * Login page with full UI Part 2: LOVABLE_CLONE_NEXTJS_SUPABASE_PART2.md (~850 lines) - Database operations & React hooks * useProjects - CRUD for projects * useProject - Single project management * useConversation - Messages management * Full TypeScript types from Supabase - Realtime subscriptions * useRealtimeMessages - Live chat updates * useRealtimeProjectFiles - Collaborative editing * Channel management - File storage with Supabase Storage * Upload/delete/list helpers * Public URL generation * Image upload component - API routes integrated with Supabase * Chat API with usage tracking * Streaming chat with Server-Sent Events * User authentication verification * Rate limiting based on subscription - Complete integrated components * Dashboard with server-side rendering * Project list with real-time updates * Project editor layout * Full TypeScript integration Features included: ✅ Next.js 14 App Router with Server Components ✅ Supabase Auth (Email + OAuth) ✅ PostgreSQL database with RLS ✅ Real-time subscriptions ✅ File storage ✅ Usage tracking & rate limiting ✅ API routes with streaming ✅ TypeScript end-to-end ✅ Production-ready security Total: ~1,775 lines of production-ready code Stack: Next.js 14 + Supabase + TypeScript + Tailwind CSS Ready to copy-paste and start building!
26 KiB
26 KiB
🚀 Lovable Clone - Next.js + Supabase Complete Guide
Production-ready templates cho Next.js 14 App Router + Supabase stack
📋 Table of Contents
- Project Structure
- Supabase Database Schema
- Supabase Client Setup
- Authentication
- Database Operations
- Realtime Features
- File Storage
- API Routes
- Complete Components
📁 I. PROJECT STRUCTURE
lovable-clone/
├── src/
│ ├── app/
│ │ ├── (auth)/
│ │ │ ├── login/
│ │ │ │ └── page.tsx
│ │ │ └── signup/
│ │ │ └── page.tsx
│ │ ├── (dashboard)/
│ │ │ ├── layout.tsx
│ │ │ ├── page.tsx
│ │ │ └── project/
│ │ │ └── [id]/
│ │ │ └── page.tsx
│ │ ├── api/
│ │ │ ├── auth/
│ │ │ │ └── callback/
│ │ │ │ └── route.ts
│ │ │ ├── chat/
│ │ │ │ └── route.ts
│ │ │ └── codegen/
│ │ │ └── route.ts
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── components/
│ │ ├── chat/
│ │ │ ├── chat-panel.tsx
│ │ │ └── chat-message.tsx
│ │ ├── preview/
│ │ │ ├── live-preview.tsx
│ │ │ └── console-panel.tsx
│ │ ├── sidebar/
│ │ │ ├── sidebar.tsx
│ │ │ ├── sections-panel.tsx
│ │ │ └── theme-panel.tsx
│ │ └── ui/
│ │ └── ... (shadcn components)
│ ├── lib/
│ │ ├── supabase/
│ │ │ ├── client.ts
│ │ │ ├── server.ts
│ │ │ └── middleware.ts
│ │ ├── ai/
│ │ │ ├── agent.ts
│ │ │ └── tools.ts
│ │ ├── hooks/
│ │ │ ├── use-chat.ts
│ │ │ ├── use-project.ts
│ │ │ └── use-realtime.ts
│ │ └── utils.ts
│ ├── types/
│ │ ├── database.types.ts (auto-generated)
│ │ └── index.ts
│ └── middleware.ts
├── supabase/
│ ├── migrations/
│ │ └── 00000000000000_initial_schema.sql
│ ├── functions/
│ │ └── chat-completion/
│ │ └── index.ts
│ └── config.toml
├── .env.local
├── next.config.js
├── package.json
└── tsconfig.json
🗄️ II. SUPABASE DATABASE SCHEMA
1. Initial Migration
File: supabase/migrations/00000000000000_initial_schema.sql
-- Enable necessary extensions
create extension if not exists "uuid-ossp";
-- ============================================
-- USERS & AUTH
-- ============================================
-- Profiles table (extends auth.users)
create table public.profiles (
id uuid references auth.users on delete cascade primary key,
email text unique not null,
full_name text,
avatar_url text,
-- Subscription
subscription_plan text default 'free' check (subscription_plan in ('free', 'pro', 'enterprise')),
subscription_status text default 'active' check (subscription_status in ('active', 'canceled', 'past_due')),
-- Limits
monthly_tokens integer default 50000,
monthly_projects integer default 3,
-- Stripe
stripe_customer_id text unique,
stripe_subscription_id text unique,
created_at timestamp with time zone default timezone('utc'::text, now()) not null,
updated_at timestamp with time zone default timezone('utc'::text, now()) not null
);
-- Enable RLS
alter table public.profiles enable row level security;
-- Policies
create policy "Users can view own profile"
on public.profiles for select
using (auth.uid() = id);
create policy "Users can update own profile"
on public.profiles for update
using (auth.uid() = id);
-- Trigger to create profile on signup
create or replace function public.handle_new_user()
returns trigger as $$
begin
insert into public.profiles (id, email, full_name, avatar_url)
values (
new.id,
new.email,
new.raw_user_meta_data->>'full_name',
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 procedure public.handle_new_user();
-- ============================================
-- PROJECTS
-- ============================================
create table public.projects (
id uuid default uuid_generate_v4() primary key,
user_id uuid references public.profiles(id) on delete cascade not null,
name text not null,
description text,
framework text default 'next' check (framework in ('next', 'vite', 'remix')),
-- Project data (JSONB for flexibility)
file_tree jsonb default '{}'::jsonb,
design_system jsonb default '{}'::jsonb,
dependencies jsonb default '{}'::jsonb,
-- Conversation
conversation_id uuid,
created_at timestamp with time zone default timezone('utc'::text, now()) not null,
updated_at timestamp with time zone default timezone('utc'::text, now()) not null
);
-- Enable RLS
alter table public.projects enable row level security;
-- Policies
create policy "Users can view own projects"
on public.projects for select
using (auth.uid() = user_id);
create policy "Users can insert own projects"
on public.projects for insert
with check (auth.uid() = user_id);
create policy "Users can update own projects"
on public.projects for update
using (auth.uid() = user_id);
create policy "Users can delete own projects"
on public.projects for delete
using (auth.uid() = user_id);
-- Indexes
create index projects_user_id_idx on public.projects(user_id);
create index projects_updated_at_idx on public.projects(updated_at desc);
-- ============================================
-- CONVERSATIONS & MESSAGES
-- ============================================
create table public.conversations (
id uuid default uuid_generate_v4() primary key,
user_id uuid references public.profiles(id) on delete cascade not null,
created_at timestamp with time zone default timezone('utc'::text, now()) not null,
updated_at timestamp with time zone default timezone('utc'::text, now()) not null
);
alter table public.conversations enable row level security;
create policy "Users can view own conversations"
on public.conversations for select
using (auth.uid() = user_id);
create policy "Users can insert own conversations"
on public.conversations for insert
with check (auth.uid() = user_id);
create table public.messages (
id uuid default uuid_generate_v4() primary key,
conversation_id uuid references public.conversations(id) on delete cascade not null,
role text not null check (role in ('user', 'assistant', 'system')),
content text not null,
tool_calls jsonb,
created_at timestamp with time zone default timezone('utc'::text, now()) not null
);
alter table public.messages enable row level security;
create policy "Users can view messages in own conversations"
on public.messages for select
using (
exists (
select 1 from public.conversations
where conversations.id = messages.conversation_id
and conversations.user_id = auth.uid()
)
);
create policy "Users can insert messages in own conversations"
on public.messages for insert
with check (
exists (
select 1 from public.conversations
where conversations.id = messages.conversation_id
and conversations.user_id = auth.uid()
)
);
-- Indexes
create index messages_conversation_id_idx on public.messages(conversation_id);
create index messages_created_at_idx on public.messages(created_at);
-- Add foreign key to projects
alter table public.projects
add constraint projects_conversation_id_fkey
foreign key (conversation_id)
references public.conversations(id)
on delete set null;
-- ============================================
-- DEPLOYMENTS
-- ============================================
create table public.deployments (
id uuid default uuid_generate_v4() primary key,
project_id uuid references public.projects(id) on delete cascade not null,
provider text not null check (provider in ('vercel', 'netlify', 'cloudflare')),
url text not null,
status text default 'pending' check (status in ('pending', 'building', 'ready', 'error')),
build_logs text,
error text,
created_at timestamp with time zone default timezone('utc'::text, now()) not null,
updated_at timestamp with time zone default timezone('utc'::text, now()) not null
);
alter table public.deployments enable row level security;
create policy "Users can view deployments of own projects"
on public.deployments for select
using (
exists (
select 1 from public.projects
where projects.id = deployments.project_id
and projects.user_id = auth.uid()
)
);
create policy "Users can insert deployments for own projects"
on public.deployments for insert
with check (
exists (
select 1 from public.projects
where projects.id = deployments.project_id
and projects.user_id = auth.uid()
)
);
-- Indexes
create index deployments_project_id_idx on public.deployments(project_id);
create index deployments_status_idx on public.deployments(status);
-- ============================================
-- USAGE TRACKING
-- ============================================
create table public.usage (
id uuid default uuid_generate_v4() primary key,
user_id uuid references public.profiles(id) on delete cascade not null,
tokens integer not null,
type text not null check (type in ('generation', 'chat')),
created_at timestamp with time zone default timezone('utc'::text, now()) not null
);
alter table public.usage enable row level security;
create policy "Users can view own usage"
on public.usage for select
using (auth.uid() = user_id);
create policy "System can insert usage"
on public.usage for insert
with check (true);
-- Indexes
create index usage_user_id_idx on public.usage(user_id);
create index usage_created_at_idx on public.usage(created_at desc);
-- ============================================
-- PROJECT FILES (for WebContainer sync)
-- ============================================
create table public.project_files (
id uuid default uuid_generate_v4() primary key,
project_id uuid references public.projects(id) on delete cascade not null,
file_path text not null,
content text not null,
created_at timestamp with time zone default timezone('utc'::text, now()) not null,
updated_at timestamp with time zone default timezone('utc'::text, now()) not null,
unique(project_id, file_path)
);
alter table public.project_files enable row level security;
create policy "Users can manage files in own projects"
on public.project_files for all
using (
exists (
select 1 from public.projects
where projects.id = project_files.project_id
and projects.user_id = auth.uid()
)
);
-- Indexes
create index project_files_project_id_idx on public.project_files(project_id);
-- ============================================
-- FUNCTIONS
-- ============================================
-- Function to get monthly usage
create or replace function get_monthly_usage(target_user_id uuid)
returns integer as $$
select coalesce(sum(tokens), 0)::integer
from public.usage
where user_id = target_user_id
and created_at >= date_trunc('month', now());
$$ language sql security definer;
-- Function to check if user can generate (has tokens left)
create or replace function can_generate(target_user_id uuid, required_tokens integer)
returns boolean as $$
declare
user_monthly_tokens integer;
used_tokens integer;
begin
select monthly_tokens into user_monthly_tokens
from public.profiles
where id = target_user_id;
select get_monthly_usage(target_user_id) into used_tokens;
return (user_monthly_tokens - used_tokens) >= required_tokens;
end;
$$ language plpgsql security definer;
-- Function to update updated_at timestamp
create or replace function update_updated_at_column()
returns trigger as $$
begin
new.updated_at = now();
return new;
end;
$$ language plpgsql;
-- Triggers for updated_at
create trigger update_profiles_updated_at before update on public.profiles
for each row execute procedure update_updated_at_column();
create trigger update_projects_updated_at before update on public.projects
for each row execute procedure update_updated_at_column();
create trigger update_conversations_updated_at before update on public.conversations
for each row execute procedure update_updated_at_column();
create trigger update_deployments_updated_at before update on public.deployments
for each row execute procedure update_updated_at_column();
create trigger update_project_files_updated_at before update on public.project_files
for each row execute procedure update_updated_at_column();
-- ============================================
-- REALTIME
-- ============================================
-- Enable realtime for necessary tables
alter publication supabase_realtime add table public.messages;
alter publication supabase_realtime add table public.project_files;
alter publication supabase_realtime add table public.deployments;
2. Generate TypeScript Types
# Install Supabase CLI
npm install -g supabase
# Login to Supabase
supabase login
# Link your project
supabase link --project-ref your-project-ref
# Generate types
npx supabase gen types typescript --project-id your-project-ref > src/types/database.types.ts
⚙️ III. SUPABASE CLIENT SETUP
1. Install Dependencies
npm install @supabase/supabase-js @supabase/ssr
2. Environment Variables
File: .env.local
# Supabase
NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJxxx...
# Optional: Service role key (for admin operations)
SUPABASE_SERVICE_ROLE_KEY=eyJxxx...
# AI Provider
AI_PROVIDER=openai
OPENAI_API_KEY=sk-...
# or
ANTHROPIC_API_KEY=sk-ant-...
# Optional: Edge Function URL
NEXT_PUBLIC_SUPABASE_EDGE_FUNCTION_URL=https://xxxxx.supabase.co/functions/v1
3. Client-side Supabase Client
File: src/lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr';
import { Database } from '@/types/database.types';
export function createClient() {
return createBrowserClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
}
4. Server-side Supabase Client
File: src/lib/supabase/server.ts
import { createServerClient, type CookieOptions } from '@supabase/ssr';
import { cookies } from 'next/headers';
import { Database } from '@/types/database.types';
export async function createClient() {
const cookieStore = await cookies();
return createServerClient<Database>(
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) {
try {
cookieStore.set({ name, value, ...options });
} catch (error) {
// The `set` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
remove(name: string, options: CookieOptions) {
try {
cookieStore.set({ name, value: '', ...options });
} catch (error) {
// The `delete` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
}
}
}
);
}
// Admin client (uses service role key)
export function createAdminClient() {
return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
{
cookies: {}
}
);
}
5. Middleware for Auth
File: src/middleware.ts
import { createServerClient, type CookieOptions } from '@supabase/ssr';
import { NextResponse, type NextRequest } from 'next/server';
export async function middleware(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
});
}
}
}
);
// Refresh session if expired
const { data: { user } } = await supabase.auth.getUser();
// Protected routes
if (!user && request.nextUrl.pathname.startsWith('/project')) {
return NextResponse.redirect(new URL('/login', request.url));
}
// Redirect to dashboard if already logged in
if (user && (request.nextUrl.pathname === '/login' || request.nextUrl.pathname === '/signup')) {
return NextResponse.redirect(new URL('/', request.url));
}
return response;
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'
]
};
🔐 IV. AUTHENTICATION
1. Auth Helpers
File: src/lib/supabase/auth.ts
import { createClient } from './client';
import { createClient as createServerClient } from './server';
// Client-side auth
export async function signUp(email: string, password: string, fullName: string) {
const supabase = createClient();
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
data: {
full_name: fullName
}
}
});
if (error) throw error;
return data;
}
export async function signIn(email: string, password: string) {
const supabase = createClient();
const { data, error } = await supabase.auth.signInWithPassword({
email,
password
});
if (error) throw error;
return data;
}
export async function signOut() {
const supabase = createClient();
const { error } = await supabase.auth.signOut();
if (error) throw error;
}
export async function signInWithGoogle() {
const supabase = createClient();
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/auth/callback`
}
});
if (error) throw error;
return data;
}
export async function signInWithGithub() {
const supabase = createClient();
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: `${window.location.origin}/auth/callback`
}
});
if (error) throw error;
return data;
}
// Server-side auth
export async function getUser() {
const supabase = await createServerClient();
const { data: { user } } = await supabase.auth.getUser();
return user;
}
export async function getSession() {
const supabase = await createServerClient();
const { data: { session } } = await supabase.auth.getSession();
return session;
}
2. Auth Callback Route
File: src/app/auth/callback/route.ts
import { createClient } from '@/lib/supabase/server';
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const requestUrl = new URL(request.url);
const code = requestUrl.searchParams.get('code');
if (code) {
const supabase = await createClient();
await supabase.auth.exchangeCodeForSession(code);
}
// Redirect to home page
return NextResponse.redirect(new URL('/', request.url));
}
3. Login Page
File: src/app/(auth)/login/page.tsx
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { signIn, signInWithGoogle, signInWithGithub } from '@/lib/supabase/auth';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Github } from 'lucide-react';
import Link from 'next/link';
export default function LoginPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const router = useRouter();
const handleEmailLogin = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
try {
await signIn(email, password);
router.push('/');
router.refresh();
} catch (err) {
setError((err as Error).message);
} finally {
setLoading(false);
}
};
const handleGoogleLogin = async () => {
try {
await signInWithGoogle();
} catch (err) {
setError((err as Error).message);
}
};
const handleGithubLogin = async () => {
try {
await signInWithGithub();
} catch (err) {
setError((err as Error).message);
}
};
return (
<div className="flex min-h-screen items-center justify-center bg-background">
<div className="w-full max-w-md space-y-8 p-8">
<div className="text-center">
<h1 className="text-3xl font-bold">Welcome back</h1>
<p className="mt-2 text-muted-foreground">
Sign in to your Lovable account
</p>
</div>
<div className="space-y-4">
{/* OAuth Buttons */}
<Button
variant="outline"
className="w-full"
onClick={handleGoogleLogin}
>
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24">
<path
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
fill="#4285F4"
/>
<path
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
fill="#34A853"
/>
<path
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
fill="#FBBC05"
/>
<path
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
fill="#EA4335"
/>
</svg>
Continue with Google
</Button>
<Button
variant="outline"
className="w-full"
onClick={handleGithubLogin}
>
<Github className="mr-2 h-4 w-4" />
Continue with GitHub
</Button>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">
Or continue with
</span>
</div>
</div>
{/* Email/Password Form */}
<form onSubmit={handleEmailLogin} className="space-y-4">
<div>
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
/>
</div>
<div>
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
/>
</div>
{error && (
<div className="text-sm text-destructive">{error}</div>
)}
<Button type="submit" className="w-full" disabled={loading}>
{loading ? 'Signing in...' : 'Sign in'}
</Button>
</form>
<p className="text-center text-sm text-muted-foreground">
Don't have an account?{' '}
<Link href="/signup" className="text-primary hover:underline">
Sign up
</Link>
</p>
</div>
</div>
</div>
);
}
Continue trong message tiếp theo với Database Operations, Realtime, và Complete Components...