# πŸš€ Lovable Clone - GitHub Integration & Pricing Plans > Complete guide để tΓ­ch hợp GitHub vΓ  thiαΊΏt kαΊΏ subscription model --- # πŸ“‘ Table of Contents 1. [GitHub Integration Overview](#github-integration-overview) 2. [GitHub OAuth Setup](#github-oauth-setup) 3. [Repository Management](#repository-management) 4. [Push Code to GitHub](#push-code-to-github) 5. [Webhook Integration](#webhook-integration) 6. [Pricing Plans Design](#pricing-plans-design) 7. [Stripe Integration](#stripe-integration) 8. [Feature Gating](#feature-gating) 9. [Usage Tracking & Quotas](#usage-tracking--quotas) 10. [Admin Dashboard](#admin-dashboard) --- # πŸ”— I. GITHUB INTEGRATION OVERVIEW ## Features ### Core Functionality - βœ… Connect GitHub account via OAuth - βœ… List user's repositories - βœ… Create new repository from app - βœ… Push generated code to GitHub - βœ… Auto-commit vα»›i AI-generated messages - βœ… Branch management - βœ… Pull request creation - βœ… Webhook for 2-way sync ### User Flow ```mermaid sequenceDiagram User->>App: Click "Connect GitHub" App->>GitHub: OAuth authorization GitHub->>User: Grant permission GitHub->>App: Access token App->>Database: Save token User->>App: Select/Create repo User->>App: Generate code App->>GitHub: Push code GitHub->>Webhook: Notify changes Webhook->>App: Sync status ``` --- # πŸ” II. GITHUB OAUTH SETUP ## 1. Create GitHub OAuth App **File: `.env.local`** ```env # GitHub OAuth GITHUB_CLIENT_ID=Iv1.your_client_id GITHUB_CLIENT_SECRET=your_client_secret GITHUB_REDIRECT_URI=http://localhost:3000/api/auth/github/callback # Production NEXT_PUBLIC_APP_URL=https://yourdomain.com ``` ## 2. Database Schema **File: `supabase/migrations/20240115000000_github_integration.sql`** ```sql -- GitHub connections table CREATE TABLE public.github_connections ( id uuid default uuid_generate_v4() primary key, user_id uuid references public.profiles(id) on delete cascade not null, github_user_id text not null, github_username text not null, access_token text not null, -- Encrypted refresh_token text, token_expires_at timestamptz, scopes text[] not null, avatar_url text, created_at timestamptz default now(), updated_at timestamptz default now(), unique(user_id) ); -- GitHub repositories table CREATE TABLE public.github_repositories ( id uuid default uuid_generate_v4() primary key, project_id uuid references public.projects(id) on delete cascade not null, github_connection_id uuid references public.github_connections(id) on delete cascade not null, repo_id bigint not null, repo_name text not null, repo_full_name text not null, repo_url text not null, default_branch text default 'main', is_private boolean default true, last_push_at timestamptz, created_at timestamptz default now(), unique(project_id) ); -- Sync history CREATE TABLE public.github_sync_history ( id uuid default uuid_generate_v4() primary key, github_repository_id uuid references public.github_repositories(id) on delete cascade not null, commit_sha text not null, commit_message text not null, files_changed integer not null, status text not null, -- 'success' | 'failed' | 'pending' error_message text, created_at timestamptz default now() ); -- Indexes CREATE INDEX idx_github_connections_user_id ON public.github_connections(user_id); CREATE INDEX idx_github_repositories_project_id ON public.github_repositories(project_id); CREATE INDEX idx_github_sync_history_repo_id ON public.github_sync_history(github_repository_id); -- RLS Policies ALTER TABLE public.github_connections ENABLE ROW LEVEL SECURITY; ALTER TABLE public.github_repositories ENABLE ROW LEVEL SECURITY; ALTER TABLE public.github_sync_history ENABLE ROW LEVEL SECURITY; CREATE POLICY "Users can manage own GitHub connections" ON public.github_connections FOR ALL USING (auth.uid() = user_id); CREATE POLICY "Users can manage own repositories" ON public.github_repositories FOR ALL USING ( EXISTS ( SELECT 1 FROM public.projects p WHERE p.id = github_repositories.project_id AND p.user_id = auth.uid() ) ); CREATE POLICY "Users can view own sync history" ON public.github_sync_history FOR SELECT USING ( EXISTS ( SELECT 1 FROM public.github_repositories gr JOIN public.projects p ON p.id = gr.project_id WHERE gr.id = github_sync_history.github_repository_id AND p.user_id = auth.uid() ) ); ``` ## 3. GitHub OAuth Flow **File: `src/lib/github/oauth.ts`** ```typescript import { createClient } from '@/lib/supabase/server'; import { encrypt, decrypt } from '@/lib/crypto'; interface GitHubUser { id: number; login: string; avatar_url: string; name: string; email: string; } interface GitHubTokenResponse { access_token: string; token_type: string; scope: string; refresh_token?: string; expires_in?: number; } export class GitHubOAuth { private clientId: string; private clientSecret: string; private redirectUri: string; constructor() { this.clientId = process.env.GITHUB_CLIENT_ID!; this.clientSecret = process.env.GITHUB_CLIENT_SECRET!; this.redirectUri = process.env.GITHUB_REDIRECT_URI!; } /** * Get authorization URL */ getAuthorizationUrl(state: string): string { const params = new URLSearchParams({ client_id: this.clientId, redirect_uri: this.redirectUri, scope: 'repo,user:email,workflow', state, allow_signup: 'true' }); return `https://github.com/login/oauth/authorize?${params.toString()}`; } /** * Exchange code for access token */ async exchangeCodeForToken(code: string): Promise { const response = await fetch('https://github.com/login/oauth/access_token', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({ client_id: this.clientId, client_secret: this.clientSecret, code, redirect_uri: this.redirectUri }) }); if (!response.ok) { throw new Error('Failed to exchange code for token'); } return response.json(); } /** * Get GitHub user info */ async getUserInfo(accessToken: string): Promise { const response = await fetch('https://api.github.com/user', { headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json' } }); if (!response.ok) { throw new Error('Failed to get user info'); } return response.json(); } /** * Save GitHub connection to database */ async saveConnection( userId: string, tokenData: GitHubTokenResponse, userData: GitHubUser ): Promise { const supabase = createClient(); // Encrypt access token const encryptedToken = await encrypt(tokenData.access_token); const expiresAt = tokenData.expires_in ? new Date(Date.now() + tokenData.expires_in * 1000) : null; await supabase .from('github_connections') .upsert({ user_id: userId, github_user_id: userData.id.toString(), github_username: userData.login, access_token: encryptedToken, refresh_token: tokenData.refresh_token, token_expires_at: expiresAt?.toISOString(), scopes: tokenData.scope.split(','), avatar_url: userData.avatar_url, updated_at: new Date().toISOString() }); } /** * Get user's GitHub connection */ async getConnection(userId: string) { const supabase = createClient(); const { data, error } = await supabase .from('github_connections') .select('*') .eq('user_id', userId) .single(); if (error || !data) { return null; } // Decrypt access token const accessToken = await decrypt(data.access_token); return { ...data, access_token: accessToken }; } } ``` ## 4. OAuth API Routes **File: `src/app/api/auth/github/route.ts`** ```typescript import { NextRequest, NextResponse } from 'next/server'; import { createClient } from '@/lib/supabase/server'; import { GitHubOAuth } from '@/lib/github/oauth'; import { nanoid } from 'nanoid'; export async function GET(request: NextRequest) { const supabase = createClient(); // Check if user is authenticated const { data: { user } } = await supabase.auth.getUser(); if (!user) { return NextResponse.redirect(new URL('/login', request.url)); } // Generate state for CSRF protection const state = nanoid(); // Store state in session const response = NextResponse.redirect( new GitHubOAuth().getAuthorizationUrl(state) ); response.cookies.set('github_oauth_state', state, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', maxAge: 600 // 10 minutes }); return response; } ``` **File: `src/app/api/auth/github/callback/route.ts`** ```typescript import { NextRequest, NextResponse } from 'next/server'; import { createClient } from '@/lib/supabase/server'; import { GitHubOAuth } from '@/lib/github/oauth'; export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams; const code = searchParams.get('code'); const state = searchParams.get('state'); const storedState = request.cookies.get('github_oauth_state')?.value; // Verify state if (!state || !storedState || state !== storedState) { return NextResponse.redirect( new URL('/dashboard?error=invalid_state', request.url) ); } if (!code) { return NextResponse.redirect( new URL('/dashboard?error=no_code', request.url) ); } try { const supabase = createClient(); const { data: { user } } = await supabase.auth.getUser(); if (!user) { return NextResponse.redirect(new URL('/login', request.url)); } const github = new GitHubOAuth(); // Exchange code for token const tokenData = await github.exchangeCodeForToken(code); // Get user info const userData = await github.getUserInfo(tokenData.access_token); // Save connection await github.saveConnection(user.id, tokenData, userData); // Redirect to dashboard const response = NextResponse.redirect( new URL('/dashboard?github=connected', request.url) ); // Clear state cookie response.cookies.delete('github_oauth_state'); return response; } catch (error) { console.error('GitHub OAuth error:', error); return NextResponse.redirect( new URL('/dashboard?error=github_auth_failed', request.url) ); } } ``` --- # πŸ“ III. REPOSITORY MANAGEMENT ## 1. GitHub API Client **File: `src/lib/github/client.ts`** ```typescript import { Octokit } from '@octokit/rest'; export interface GitHubRepository { id: number; name: string; full_name: string; private: boolean; html_url: string; default_branch: string; description: string | null; } export class GitHubClient { private octokit: Octokit; constructor(accessToken: string) { this.octokit = new Octokit({ auth: accessToken }); } /** * List user's repositories */ async listRepositories( options: { type?: 'all' | 'owner' | 'member'; sort?: 'created' | 'updated' | 'pushed' | 'full_name'; per_page?: number; } = {} ): Promise { const { data } = await this.octokit.repos.listForAuthenticatedUser({ type: options.type || 'owner', sort: options.sort || 'updated', per_page: options.per_page || 100 }); return data; } /** * Create a new repository */ async createRepository(options: { name: string; description?: string; private?: boolean; auto_init?: boolean; }): Promise { const { data } = await this.octokit.repos.createForAuthenticatedUser({ name: options.name, description: options.description || 'Created with Lovable Clone', private: options.private ?? true, auto_init: options.auto_init ?? true }); return data; } /** * Get repository */ async getRepository(owner: string, repo: string): Promise { const { data } = await this.octokit.repos.get({ owner, repo }); return data; } /** * Create or update file */ async createOrUpdateFile( owner: string, repo: string, path: string, content: string, message: string, branch: string = 'main' ): Promise { try { // Try to get existing file const { data: existingFile } = await this.octokit.repos.getContent({ owner, repo, path, ref: branch }); // Update existing file await this.octokit.repos.createOrUpdateFileContents({ owner, repo, path, message, content: Buffer.from(content).toString('base64'), branch, sha: 'sha' in existingFile ? existingFile.sha : undefined }); } catch (error: any) { if (error.status === 404) { // Create new file await this.octokit.repos.createOrUpdateFileContents({ owner, repo, path, message, content: Buffer.from(content).toString('base64'), branch }); } else { throw error; } } } /** * Create multiple files (batch) */ async createTree( owner: string, repo: string, files: Array<{ path: string; content: string }>, branch: string = 'main' ): Promise { // Get latest commit const { data: ref } = await this.octokit.git.getRef({ owner, repo, ref: `heads/${branch}` }); const latestCommitSha = ref.object.sha; // Create blobs for each file const blobs = await Promise.all( files.map(async (file) => { const { data } = await this.octokit.git.createBlob({ owner, repo, content: Buffer.from(file.content).toString('base64'), encoding: 'base64' }); return { path: file.path, mode: '100644' as const, type: 'blob' as const, sha: data.sha }; }) ); // Create tree const { data: tree } = await this.octokit.git.createTree({ owner, repo, base_tree: latestCommitSha, tree: blobs }); return tree.sha; } /** * Create commit */ async createCommit( owner: string, repo: string, message: string, treeSha: string, parentSha: string ): Promise { const { data: commit } = await this.octokit.git.createCommit({ owner, repo, message, tree: treeSha, parents: [parentSha] }); return commit.sha; } /** * Update branch reference */ async updateRef( owner: string, repo: string, branch: string, sha: string ): Promise { await this.octokit.git.updateRef({ owner, repo, ref: `heads/${branch}`, sha, force: false }); } } ``` ## 2. Repository Management API **File: `src/app/api/github/repositories/route.ts`** ```typescript import { NextResponse } from 'next/server'; import { createClient } from '@/lib/supabase/server'; import { GitHubOAuth } from '@/lib/github/oauth'; import { GitHubClient } from '@/lib/github/client'; export async function GET() { const supabase = createClient(); const { data: { user } } = await supabase.auth.getUser(); if (!user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } try { // Get GitHub connection const github = new GitHubOAuth(); const connection = await github.getConnection(user.id); if (!connection) { return NextResponse.json( { error: 'GitHub not connected' }, { status: 400 } ); } // List repositories const client = new GitHubClient(connection.access_token); const repositories = await client.listRepositories(); return NextResponse.json({ repositories }); } catch (error: any) { console.error('Failed to list repositories:', error); return NextResponse.json( { error: 'Failed to list repositories' }, { status: 500 } ); } } export async function POST(request: Request) { const supabase = createClient(); const { data: { user } } = await supabase.auth.getUser(); if (!user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } try { const { name, description, isPrivate, projectId } = await request.json(); // Get GitHub connection const github = new GitHubOAuth(); const connection = await github.getConnection(user.id); if (!connection) { return NextResponse.json( { error: 'GitHub not connected' }, { status: 400 } ); } // Create repository const client = new GitHubClient(connection.access_token); const repo = await client.createRepository({ name, description, private: isPrivate, auto_init: true }); // Save to database await supabase.from('github_repositories').insert({ project_id: projectId, github_connection_id: connection.id, repo_id: repo.id, repo_name: repo.name, repo_full_name: repo.full_name, repo_url: repo.html_url, default_branch: repo.default_branch, is_private: repo.private }); return NextResponse.json({ repository: repo }); } catch (error: any) { console.error('Failed to create repository:', error); return NextResponse.json( { error: error.message || 'Failed to create repository' }, { status: 500 } ); } } ``` --- # ⬆️ IV. PUSH CODE TO GITHUB ## 1. Push Service **File: `src/lib/github/push-service.ts`** ```typescript import { createClient } from '@/lib/supabase/server'; import { GitHubOAuth } from './oauth'; import { GitHubClient } from './client'; export interface PushOptions { projectId: string; userId: string; commitMessage?: string; branch?: string; } export class GitHubPushService { /** * Push project files to GitHub */ async pushProject(options: PushOptions): Promise<{ success: boolean; commitSha?: string; url?: string; error?: string; }> { const supabase = createClient(); try { // 1. Get GitHub connection const github = new GitHubOAuth(); const connection = await github.getConnection(options.userId); if (!connection) { return { success: false, error: 'GitHub not connected' }; } // 2. Get GitHub repository const { data: githubRepo } = await supabase .from('github_repositories') .select('*') .eq('project_id', options.projectId) .single(); if (!githubRepo) { return { success: false, error: 'Repository not linked' }; } // 3. Get project files const { data: projectFiles } = await supabase .from('project_files') .select('*') .eq('project_id', options.projectId); if (!projectFiles || projectFiles.length === 0) { return { success: false, error: 'No files to push' }; } // 4. Push files to GitHub const client = new GitHubClient(connection.access_token); const [owner, repo] = githubRepo.repo_full_name.split('/'); const branch = options.branch || githubRepo.default_branch; // Get latest commit const { data: ref } = await client.octokit.git.getRef({ owner, repo, ref: `heads/${branch}` }); const latestCommitSha = ref.object.sha; // Create tree with all files const treeSha = await client.createTree( owner, repo, projectFiles.map(file => ({ path: file.path, content: file.content })), branch ); // Create commit const commitMessage = options.commitMessage || this.generateCommitMessage(projectFiles); const commitSha = await client.createCommit( owner, repo, commitMessage, treeSha, latestCommitSha ); // Update branch await client.updateRef(owner, repo, branch, commitSha); // 5. Update sync history await supabase.from('github_sync_history').insert({ github_repository_id: githubRepo.id, commit_sha: commitSha, commit_message: commitMessage, files_changed: projectFiles.length, status: 'success' }); // 6. Update last push timestamp await supabase .from('github_repositories') .update({ last_push_at: new Date().toISOString() }) .eq('id', githubRepo.id); return { success: true, commitSha, url: `${githubRepo.repo_url}/commit/${commitSha}` }; } catch (error: any) { console.error('Push to GitHub failed:', error); // Log error to sync history const { data: githubRepo } = await supabase .from('github_repositories') .select('id') .eq('project_id', options.projectId) .single(); if (githubRepo) { await supabase.from('github_sync_history').insert({ github_repository_id: githubRepo.id, commit_sha: '', commit_message: options.commitMessage || '', files_changed: 0, status: 'failed', error_message: error.message }); } return { success: false, error: error.message }; } } /** * Generate AI commit message */ private generateCommitMessage(files: any[]): string { // Simple commit message generation // TODO: Use AI to generate better commit messages const fileCount = files.length; const fileTypes = new Set(files.map(f => f.path.split('.').pop())); return `Update ${fileCount} file${fileCount > 1 ? 's' : ''} (${Array.from(fileTypes).join(', ')})`; } } ``` ## 2. Push API Route **File: `src/app/api/github/push/route.ts`** ```typescript import { NextResponse } from 'next/server'; import { createClient } from '@/lib/supabase/server'; import { GitHubPushService } from '@/lib/github/push-service'; export async function POST(request: Request) { const supabase = createClient(); const { data: { user } } = await supabase.auth.getUser(); if (!user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } try { const { projectId, commitMessage, branch } = await request.json(); if (!projectId) { return NextResponse.json( { error: 'Project ID is required' }, { status: 400 } ); } // Verify project ownership const { data: project } = await supabase .from('projects') .select('id') .eq('id', projectId) .eq('user_id', user.id) .single(); if (!project) { return NextResponse.json( { error: 'Project not found' }, { status: 404 } ); } // Push to GitHub const pushService = new GitHubPushService(); const result = await pushService.pushProject({ projectId, userId: user.id, commitMessage, branch }); if (!result.success) { return NextResponse.json( { error: result.error }, { status: 500 } ); } return NextResponse.json({ success: true, commitSha: result.commitSha, url: result.url }); } catch (error: any) { console.error('Push failed:', error); return NextResponse.json( { error: error.message || 'Push failed' }, { status: 500 } ); } } ``` ## 3. React Component **File: `src/components/github/push-button.tsx`** ```typescript 'use client'; import { useState } from 'react'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { GitBranch, Upload } from 'lucide-react'; import { toast } from 'sonner'; interface PushButtonProps { projectId: string; disabled?: boolean; } export function PushButton({ projectId, disabled }: PushButtonProps) { const [open, setOpen] = useState(false); const [pushing, setPushing] = useState(false); const [commitMessage, setCommitMessage] = useState(''); const [branch, setBranch] = useState('main'); async function handlePush() { if (!commitMessage.trim()) { toast.error('Please enter a commit message'); return; } setPushing(true); try { const response = await fetch('/api/github/push', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ projectId, commitMessage, branch }) }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Push failed'); } toast.success('Pushed to GitHub successfully!', { description: `View commit: ${data.url}`, action: { label: 'View', onClick: () => window.open(data.url, '_blank') } }); setOpen(false); setCommitMessage(''); } catch (error: any) { toast.error(error.message || 'Failed to push to GitHub'); } finally { setPushing(false); } } return ( Push to GitHub Commit and push your changes to GitHub
setCommitMessage(e.target.value)} />
setBranch(e.target.value)} />
); } ``` --- # πŸ”” V. WEBHOOK INTEGRATION ## 1. Setup GitHub Webhook **Webhook URL**: `https://yourdomain.com/api/webhooks/github` **Events to subscribe**: - `push` - Detect external changes - `pull_request` - PR notifications - `repository` - Repo updates ## 2. Webhook Handler **File: `src/app/api/webhooks/github/route.ts`** ```typescript import { NextRequest, NextResponse } from 'next/server'; import { createClient } from '@/lib/supabase/server'; import { verifyWebhookSignature } from '@/lib/github/webhook-verify'; export async function POST(request: NextRequest) { try { // Verify webhook signature const payload = await request.text(); const signature = request.headers.get('x-hub-signature-256'); const event = request.headers.get('x-github-event'); if (!verifyWebhookSignature(payload, signature)) { return NextResponse.json( { error: 'Invalid signature' }, { status: 401 } ); } const data = JSON.parse(payload); // Handle different events switch (event) { case 'push': await handlePushEvent(data); break; case 'pull_request': await handlePullRequestEvent(data); break; case 'repository': await handleRepositoryEvent(data); break; } return NextResponse.json({ success: true }); } catch (error) { console.error('Webhook error:', error); return NextResponse.json( { error: 'Webhook processing failed' }, { status: 500 } ); } } async function handlePushEvent(data: any) { const supabase = createClient(); // Find repository in database const { data: repo } = await supabase .from('github_repositories') .select('*') .eq('repo_id', data.repository.id) .single(); if (!repo) return; // Log sync event await supabase.from('github_sync_history').insert({ github_repository_id: repo.id, commit_sha: data.after, commit_message: data.head_commit?.message || 'External push', files_changed: data.head_commit?.modified?.length || 0, status: 'success' }); // TODO: Optionally sync files back to app } async function handlePullRequestEvent(data: any) { // TODO: Handle PR notifications console.log('PR event:', data.action, data.pull_request.title); } async function handleRepositoryEvent(data: any) { // TODO: Handle repo updates (renamed, deleted, etc.) console.log('Repository event:', data.action); } ``` **File: `src/lib/github/webhook-verify.ts`** ```typescript import { createHmac } from 'crypto'; export function verifyWebhookSignature( payload: string, signature: string | null ): boolean { if (!signature) return false; const secret = process.env.GITHUB_WEBHOOK_SECRET!; const hmac = createHmac('sha256', secret); const digest = 'sha256=' + hmac.update(payload).digest('hex'); return signature === digest; } ``` --- # πŸ’° VI. PRICING PLANS DESIGN ## Pricing Tiers ### πŸ†“ FREE TIER **Price**: $0/month **Features**: - βœ… 3 projects - βœ… 50,000 AI tokens/month - βœ… Basic components library - βœ… GitHub integration - βœ… Export code (ZIP) - ❌ Custom domains - ❌ Team collaboration - ❌ Priority support **Target**: Hobbyists, students, testing --- ### ⭐ PRO TIER **Price**: $29/month or $290/year (save $58) **Features**: - βœ… **Unlimited projects** - βœ… **500,000 AI tokens/month** - βœ… Advanced components library - βœ… GitHub integration - βœ… Export code (ZIP) - βœ… **1 custom domain** - βœ… **Vercel/Netlify auto-deploy** - βœ… **Priority support** - βœ… **Code history (30 days)** - βœ… **Custom design system** - ❌ Team collaboration - ❌ SSO **Target**: Freelancers, indie developers --- ### πŸ‘₯ TEAM TIER **Price**: $99/month or $990/year (save $198) **Features**: - βœ… Everything in Pro - βœ… **2,000,000 AI tokens/month** - βœ… **5 team members** - βœ… **Unlimited custom domains** - βœ… **Team collaboration** - βœ… **Shared component library** - βœ… **Role-based permissions** - βœ… **Code review workflow** - βœ… **Activity logs** - βœ… **API access** - ❌ SSO - ❌ Dedicated support **Target**: Small teams, agencies --- ### 🏒 ENTERPRISE TIER **Price**: Custom (starting at $499/month) **Features**: - βœ… Everything in Team - βœ… **Unlimited AI tokens** - βœ… **Unlimited team members** - βœ… **SSO (SAML, OAuth)** - βœ… **Dedicated support** - βœ… **SLA guarantee (99.9%)** - βœ… **Custom AI models** - βœ… **On-premise deployment** - βœ… **Advanced analytics** - βœ… **Custom integrations** - βœ… **Training & onboarding** **Target**: Large companies, enterprises --- ## Feature Comparison Table | Feature | Free | Pro | Team | Enterprise | |---------|------|-----|------|------------| | **Projects** | 3 | Unlimited | Unlimited | Unlimited | | **AI Tokens/mo** | 50K | 500K | 2M | Unlimited | | **Team Members** | 1 | 1 | 5 | Unlimited | | **Custom Domains** | 0 | 1 | Unlimited | Unlimited | | **GitHub Integration** | βœ… | βœ… | βœ… | βœ… | | **Auto Deploy** | ❌ | βœ… | βœ… | βœ… | | **Code History** | 7 days | 30 days | 90 days | 1 year | | **Priority Support** | ❌ | βœ… | βœ… | βœ… | | **Team Collaboration** | ❌ | ❌ | βœ… | βœ… | | **API Access** | ❌ | ❌ | βœ… | βœ… | | **SSO** | ❌ | ❌ | ❌ | βœ… | | **SLA** | ❌ | ❌ | ❌ | 99.9% | --- ## Database Schema **File: `supabase/migrations/20240116000000_pricing_subscriptions.sql`** ```sql -- Subscription plans CREATE TABLE public.subscription_plans ( id text primary key, -- 'free', 'pro', 'team', 'enterprise' name text not null, description text, price_monthly integer not null, -- in cents price_yearly integer not null, stripe_price_id_monthly text, stripe_price_id_yearly text, features jsonb not null default '{}'::jsonb, limits jsonb not null default '{}'::jsonb, is_active boolean default true, created_at timestamptz default now() ); -- Insert default plans INSERT INTO public.subscription_plans (id, name, description, price_monthly, price_yearly, limits, features) VALUES ('free', 'Free', 'Perfect for trying out', 0, 0, '{"projects": 3, "tokens_per_month": 50000, "team_members": 1, "custom_domains": 0}'::jsonb, '["GitHub integration", "Export code", "Basic components"]'::jsonb ), ('pro', 'Pro', 'For professional developers', 2900, 29000, '{"projects": -1, "tokens_per_month": 500000, "team_members": 1, "custom_domains": 1}'::jsonb, '["Everything in Free", "Unlimited projects", "500K tokens/mo", "Priority support", "Auto-deploy", "Custom domain"]'::jsonb ), ('team', 'Team', 'For small teams', 9900, 99000, '{"projects": -1, "tokens_per_month": 2000000, "team_members": 5, "custom_domains": -1}'::jsonb, '["Everything in Pro", "2M tokens/mo", "5 team members", "Team collaboration", "API access"]'::jsonb ), ('enterprise', 'Enterprise', 'For large organizations', 49900, 499000, '{"projects": -1, "tokens_per_month": -1, "team_members": -1, "custom_domains": -1}'::jsonb, '["Everything in Team", "Unlimited everything", "SSO", "SLA", "Dedicated support"]'::jsonb ); -- Subscriptions CREATE TABLE public.subscriptions ( id uuid default uuid_generate_v4() primary key, user_id uuid references public.profiles(id) on delete cascade not null, plan_id text references public.subscription_plans(id) not null, status text not null, -- 'active', 'canceled', 'past_due', 'trialing' billing_cycle text not null, -- 'monthly', 'yearly' stripe_customer_id text, stripe_subscription_id text, current_period_start timestamptz not null, current_period_end timestamptz not null, cancel_at_period_end boolean default false, canceled_at timestamptz, trial_end timestamptz, created_at timestamptz default now(), updated_at timestamptz default now(), unique(user_id) ); -- Payment history CREATE TABLE public.payment_history ( id uuid default uuid_generate_v4() primary key, subscription_id uuid references public.subscriptions(id) on delete cascade not null, amount integer not null, -- in cents currency text default 'usd', status text not null, -- 'succeeded', 'failed', 'pending' stripe_payment_intent_id text, stripe_invoice_id text, receipt_url text, created_at timestamptz default now() ); -- Indexes CREATE INDEX idx_subscriptions_user_id ON public.subscriptions(user_id); CREATE INDEX idx_subscriptions_status ON public.subscriptions(status); CREATE INDEX idx_payment_history_subscription_id ON public.payment_history(subscription_id); -- RLS Policies ALTER TABLE public.subscription_plans ENABLE ROW LEVEL SECURITY; ALTER TABLE public.subscriptions ENABLE ROW LEVEL SECURITY; ALTER TABLE public.payment_history ENABLE ROW LEVEL SECURITY; CREATE POLICY "Anyone can view plans" ON public.subscription_plans FOR SELECT USING (true); CREATE POLICY "Users can view own subscription" ON public.subscriptions FOR SELECT USING (auth.uid() = user_id); CREATE POLICY "Users can view own payment history" ON public.payment_history FOR SELECT USING ( EXISTS ( SELECT 1 FROM public.subscriptions s WHERE s.id = payment_history.subscription_id AND s.user_id = auth.uid() ) ); -- Update profiles table ALTER TABLE public.profiles ADD COLUMN IF NOT EXISTS subscription_plan text default 'free' references public.subscription_plans(id); ``` --- # πŸ’³ VII. STRIPE INTEGRATION ## 1. Setup Stripe **Install dependencies**: ```bash npm install stripe @stripe/stripe-js ``` **Environment variables**: ```env # Stripe STRIPE_SECRET_KEY=sk_test_... STRIPE_WEBHOOK_SECRET=whsec_... NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_... ``` ## 2. Stripe Client **File: `src/lib/stripe/client.ts`** ```typescript import Stripe from 'stripe'; export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: '2023-10-16', typescript: true }); /** * Create or retrieve Stripe customer */ export async function getOrCreateCustomer( userId: string, email: string ): Promise { // Check if customer exists in database const { data: subscription } = await supabase .from('subscriptions') .select('stripe_customer_id') .eq('user_id', userId) .single(); if (subscription?.stripe_customer_id) { return subscription.stripe_customer_id; } // Create new customer const customer = await stripe.customers.create({ email, metadata: { user_id: userId } }); return customer.id; } /** * Create checkout session */ export async function createCheckoutSession( userId: string, email: string, planId: string, billingCycle: 'monthly' | 'yearly' ): Promise { const customerId = await getOrCreateCustomer(userId, email); // Get plan const { data: plan } = await supabase .from('subscription_plans') .select('*') .eq('id', planId) .single(); if (!plan) { throw new Error('Plan not found'); } const priceId = billingCycle === 'monthly' ? plan.stripe_price_id_monthly : plan.stripe_price_id_yearly; // Create checkout session const session = await stripe.checkout.sessions.create({ customer: customerId, mode: 'subscription', payment_method_types: ['card'], line_items: [ { price: priceId, quantity: 1 } ], success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?payment=success`, cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing?payment=canceled`, metadata: { user_id: userId, plan_id: planId } }); return session.url!; } /** * Create billing portal session */ export async function createPortalSession( customerId: string ): Promise { const session = await stripe.billingPortal.sessions.create({ customer: customerId, return_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard/billing` }); return session.url; } ``` ## 3. Checkout API **File: `src/app/api/stripe/checkout/route.ts`** ```typescript import { NextResponse } from 'next/server'; import { createClient } from '@/lib/supabase/server'; import { createCheckoutSession } from '@/lib/stripe/client'; export async function POST(request: Request) { const supabase = createClient(); const { data: { user } } = await supabase.auth.getUser(); if (!user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } try { const { planId, billingCycle } = await request.json(); if (!planId || !billingCycle) { return NextResponse.json( { error: 'Missing required fields' }, { status: 400 } ); } // Create checkout session const checkoutUrl = await createCheckoutSession( user.id, user.email!, planId, billingCycle ); return NextResponse.json({ url: checkoutUrl }); } catch (error: any) { console.error('Checkout error:', error); return NextResponse.json( { error: error.message || 'Checkout failed' }, { status: 500 } ); } } ``` ## 4. Stripe Webhook Handler **File: `src/app/api/webhooks/stripe/route.ts`** ```typescript import { NextRequest, NextResponse } from 'next/server'; import { stripe } from '@/lib/stripe/client'; import { createClient } from '@/lib/supabase/server'; import Stripe from 'stripe'; export async function POST(request: NextRequest) { const body = await request.text(); const signature = request.headers.get('stripe-signature')!; let event: Stripe.Event; try { event = stripe.webhooks.constructEvent( body, signature, process.env.STRIPE_WEBHOOK_SECRET! ); } catch (error: any) { console.error('Webhook signature verification failed:', error.message); return NextResponse.json( { error: 'Invalid signature' }, { status: 400 } ); } // Handle event try { switch (event.type) { case 'checkout.session.completed': await handleCheckoutCompleted(event.data.object as Stripe.Checkout.Session); break; case 'customer.subscription.created': case 'customer.subscription.updated': await handleSubscriptionUpdate(event.data.object as Stripe.Subscription); break; case 'customer.subscription.deleted': await handleSubscriptionDeleted(event.data.object as Stripe.Subscription); break; case 'invoice.payment_succeeded': await handlePaymentSucceeded(event.data.object as Stripe.Invoice); break; case 'invoice.payment_failed': await handlePaymentFailed(event.data.object as Stripe.Invoice); break; } return NextResponse.json({ received: true }); } catch (error) { console.error('Webhook handler error:', error); return NextResponse.json( { error: 'Webhook handler failed' }, { status: 500 } ); } } async function handleCheckoutCompleted(session: Stripe.Checkout.Session) { const supabase = createClient(); const userId = session.metadata?.user_id; const planId = session.metadata?.plan_id; if (!userId || !planId) return; // Get subscription const subscription = await stripe.subscriptions.retrieve( session.subscription as string ); // Save to database await supabase.from('subscriptions').upsert({ user_id: userId, plan_id: planId, status: subscription.status, billing_cycle: subscription.items.data[0].price.recurring?.interval === 'year' ? 'yearly' : 'monthly', stripe_customer_id: subscription.customer as string, stripe_subscription_id: subscription.id, current_period_start: new Date(subscription.current_period_start * 1000).toISOString(), current_period_end: new Date(subscription.current_period_end * 1000).toISOString() }); // Update profile await supabase .from('profiles') .update({ subscription_plan: planId }) .eq('id', userId); } async function handleSubscriptionUpdate(subscription: Stripe.Subscription) { const supabase = createClient(); const userId = subscription.metadata?.user_id; if (!userId) return; await supabase .from('subscriptions') .update({ status: subscription.status, current_period_start: new Date(subscription.current_period_start * 1000).toISOString(), current_period_end: new Date(subscription.current_period_end * 1000).toISOString(), cancel_at_period_end: subscription.cancel_at_period_end, updated_at: new Date().toISOString() }) .eq('stripe_subscription_id', subscription.id); } async function handleSubscriptionDeleted(subscription: Stripe.Subscription) { const supabase = createClient(); await supabase .from('subscriptions') .update({ status: 'canceled', canceled_at: new Date().toISOString() }) .eq('stripe_subscription_id', subscription.id); // Downgrade to free plan const { data: sub } = await supabase .from('subscriptions') .select('user_id') .eq('stripe_subscription_id', subscription.id) .single(); if (sub) { await supabase .from('profiles') .update({ subscription_plan: 'free' }) .eq('id', sub.user_id); } } async function handlePaymentSucceeded(invoice: Stripe.Invoice) { const supabase = createClient(); const { data: subscription } = await supabase .from('subscriptions') .select('id') .eq('stripe_subscription_id', invoice.subscription as string) .single(); if (!subscription) return; await supabase.from('payment_history').insert({ subscription_id: subscription.id, amount: invoice.amount_paid, currency: invoice.currency, status: 'succeeded', stripe_payment_intent_id: invoice.payment_intent as string, stripe_invoice_id: invoice.id, receipt_url: invoice.hosted_invoice_url }); } async function handlePaymentFailed(invoice: Stripe.Invoice) { const supabase = createClient(); const { data: subscription } = await supabase .from('subscriptions') .select('id') .eq('stripe_subscription_id', invoice.subscription as string) .single(); if (!subscription) return; await supabase.from('payment_history').insert({ subscription_id: subscription.id, amount: invoice.amount_due, currency: invoice.currency, status: 'failed', stripe_invoice_id: invoice.id }); // Update subscription status await supabase .from('subscriptions') .update({ status: 'past_due' }) .eq('id', subscription.id); } ``` ## 5. Pricing Page Component **File: `src/app/pricing/page.tsx`** ```typescript 'use client'; import { useState } from 'react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; import { Check } from 'lucide-react'; import { toast } from 'sonner'; const plans = [ { id: 'free', name: 'Free', price: { monthly: 0, yearly: 0 }, description: 'Perfect for trying out', features: [ '3 projects', '50,000 AI tokens/month', 'Basic components', 'GitHub integration', 'Export code (ZIP)' ] }, { id: 'pro', name: 'Pro', price: { monthly: 29, yearly: 290 }, description: 'For professional developers', features: [ 'Unlimited projects', '500,000 AI tokens/month', 'Advanced components', 'Priority support', '1 custom domain', 'Auto-deploy', 'Code history (30 days)' ], popular: true }, { id: 'team', name: 'Team', price: { monthly: 99, yearly: 990 }, description: 'For small teams', features: [ 'Everything in Pro', '2M AI tokens/month', '5 team members', 'Team collaboration', 'Unlimited domains', 'API access', 'Code history (90 days)' ] }, { id: 'enterprise', name: 'Enterprise', price: { monthly: 499, yearly: 4990 }, description: 'For large organizations', features: [ 'Everything in Team', 'Unlimited AI tokens', 'Unlimited team members', 'SSO (SAML, OAuth)', 'SLA (99.9%)', 'Dedicated support', 'On-premise option' ] } ]; export default function PricingPage() { const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>('monthly'); const [loading, setLoading] = useState(null); async function handleSubscribe(planId: string) { if (planId === 'free') { window.location.href = '/signup'; return; } setLoading(planId); try { const response = await fetch('/api/stripe/checkout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ planId, billingCycle }) }); const data = await response.json(); if (!response.ok) { throw new Error(data.error); } // Redirect to Stripe checkout window.location.href = data.url; } catch (error: any) { toast.error(error.message || 'Failed to start checkout'); setLoading(null); } } return (

Choose Your Plan

Build amazing projects with AI-powered code generation

{/* Billing cycle toggle */}
{plans.map((plan) => ( {plan.popular && (
Most Popular
)} {plan.name} {plan.description}
${plan.price[billingCycle]} /{billingCycle === 'monthly' ? 'mo' : 'yr'}
    {plan.features.map((feature, index) => (
  • {feature}
  • ))}
))}
); } ``` --- # πŸ”’ VIII. FEATURE GATING ## 1. Feature Gate Middleware **File: `src/lib/subscription/feature-gate.ts`** ```typescript import { createClient } from '@/lib/supabase/server'; export interface PlanLimits { projects: number; // -1 = unlimited tokens_per_month: number; team_members: number; custom_domains: number; } export interface FeatureAccess { can_access: boolean; reason?: string; upgrade_required?: string; // plan_id to upgrade to } export class FeatureGate { /** * Check if user can create a new project */ async canCreateProject(userId: string): Promise { const supabase = createClient(); // Get user's plan const { data: profile } = await supabase .from('profiles') .select('subscription_plan') .eq('id', userId) .single(); if (!profile) { return { can_access: false, reason: 'User not found' }; } // Get plan limits const { data: plan } = await supabase .from('subscription_plans') .select('limits') .eq('id', profile.subscription_plan) .single(); if (!plan) { return { can_access: false, reason: 'Plan not found' }; } const limits = plan.limits as PlanLimits; // Unlimited projects if (limits.projects === -1) { return { can_access: true }; } // Check current project count const { count } = await supabase .from('projects') .select('id', { count: 'exact', head: true }) .eq('user_id', userId); if (count === null) { return { can_access: false, reason: 'Failed to count projects' }; } if (count >= limits.projects) { return { can_access: false, reason: `You've reached the limit of ${limits.projects} projects`, upgrade_required: 'pro' }; } return { can_access: true }; } /** * Check if user has tokens available */ async hasTokensAvailable( userId: string, tokensNeeded: number ): Promise { const supabase = createClient(); // Get user's plan and usage const { data: profile } = await supabase .from('profiles') .select('subscription_plan, tokens_used_this_month, monthly_tokens') .eq('id', userId) .single(); if (!profile) { return { can_access: false, reason: 'User not found' }; } // Unlimited tokens if (profile.monthly_tokens === -1) { return { can_access: true }; } const tokensRemaining = profile.monthly_tokens - profile.tokens_used_this_month; if (tokensRemaining < tokensNeeded) { return { can_access: false, reason: `Not enough tokens. Need ${tokensNeeded}, have ${tokensRemaining}`, upgrade_required: this.suggestPlanForTokens(tokensNeeded) }; } return { can_access: true }; } /** * Check if user can add team member */ async canAddTeamMember(userId: string): Promise { const supabase = createClient(); // Get user's plan const { data: profile } = await supabase .from('profiles') .select('subscription_plan') .eq('id', userId) .single(); if (!profile) { return { can_access: false, reason: 'User not found' }; } // Check if plan supports team if (profile.subscription_plan === 'free' || profile.subscription_plan === 'pro') { return { can_access: false, reason: 'Team features require Team or Enterprise plan', upgrade_required: 'team' }; } // Get plan limits const { data: plan } = await supabase .from('subscription_plans') .select('limits') .eq('id', profile.subscription_plan) .single(); if (!plan) { return { can_access: false, reason: 'Plan not found' }; } const limits = plan.limits as PlanLimits; // Unlimited team members if (limits.team_members === -1) { return { can_access: true }; } // TODO: Count current team members // For now, just check if limit exists return { can_access: true }; } /** * Check feature availability */ async checkFeature( userId: string, feature: string ): Promise { const supabase = createClient(); const { data: profile } = await supabase .from('profiles') .select('subscription_plan') .eq('id', userId) .single(); if (!profile) { return { can_access: false, reason: 'User not found' }; } const plan = profile.subscription_plan; // Feature matrix const features: Record = { 'auto-deploy': ['pro', 'team', 'enterprise'], 'custom-domain': ['pro', 'team', 'enterprise'], 'team-collaboration': ['team', 'enterprise'], 'api-access': ['team', 'enterprise'], 'sso': ['enterprise'], 'priority-support': ['pro', 'team', 'enterprise'] }; const allowedPlans = features[feature]; if (!allowedPlans) { return { can_access: true }; // Feature doesn't exist, allow by default } if (!allowedPlans.includes(plan)) { // Find the cheapest plan that supports this feature const planOrder = ['pro', 'team', 'enterprise']; const suggestedPlan = planOrder.find(p => allowedPlans.includes(p)); return { can_access: false, reason: `Feature "${feature}" requires ${allowedPlans.join(' or ')} plan`, upgrade_required: suggestedPlan }; } return { can_access: true }; } private suggestPlanForTokens(tokensNeeded: number): string { if (tokensNeeded <= 500000) return 'pro'; if (tokensNeeded <= 2000000) return 'team'; return 'enterprise'; } } ``` ## 2. Feature Gate Hook **File: `src/hooks/use-feature-gate.ts`** ```typescript import { useState, useEffect } from 'react'; import { FeatureAccess } from '@/lib/subscription/feature-gate'; export function useFeatureGate(feature: string) { const [access, setAccess] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { checkFeature(); }, [feature]); async function checkFeature() { setLoading(true); try { const response = await fetch(`/api/features/check?feature=${feature}`); const data = await response.json(); setAccess(data); } catch (error) { setAccess({ can_access: false, reason: 'Failed to check feature' }); } finally { setLoading(false); } } return { access, loading, refetch: checkFeature }; } ``` ## 3. Feature Gate API **File: `src/app/api/features/check/route.ts`** ```typescript import { NextRequest, NextResponse } from 'next/server'; import { createClient } from '@/lib/supabase/server'; import { FeatureGate } from '@/lib/subscription/feature-gate'; export async function GET(request: NextRequest) { const supabase = createClient(); const { data: { user } } = await supabase.auth.getUser(); if (!user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const feature = request.nextUrl.searchParams.get('feature'); if (!feature) { return NextResponse.json( { error: 'Feature parameter required' }, { status: 400 } ); } const gate = new FeatureGate(); const access = await gate.checkFeature(user.id, feature); return NextResponse.json(access); } ``` ## 4. Upgrade Modal Component **File: `src/components/subscription/upgrade-modal.tsx`** ```typescript 'use client'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Zap } from 'lucide-react'; interface UpgradeModalProps { open: boolean; onClose: () => void; reason: string; suggestedPlan?: string; } export function UpgradeModal({ open, onClose, reason, suggestedPlan }: UpgradeModalProps) { function handleUpgrade() { window.location.href = `/pricing?plan=${suggestedPlan || 'pro'}`; } return (
Upgrade Required {reason}
); } ``` --- # πŸ“Š IX. USAGE TRACKING & QUOTAS ## 1. Quota Checker Middleware **File: `src/middleware/quota-check.ts`** ```typescript import { createClient } from '@/lib/supabase/server'; import { NextResponse } from 'next/server'; export async function checkQuota(userId: string, tokensToUse: number) { const supabase = createClient(); // Get user's profile const { data: profile } = await supabase .from('profiles') .select('monthly_tokens, tokens_used_this_month, subscription_plan') .eq('id', userId) .single(); if (!profile) { return { allowed: false, reason: 'User not found' }; } // Unlimited tokens if (profile.monthly_tokens === -1) { return { allowed: true }; } // Check if quota exceeded const remainingTokens = profile.monthly_tokens - profile.tokens_used_this_month; if (remainingTokens < tokensToUse) { return { allowed: false, reason: 'Monthly token quota exceeded', remaining: remainingTokens, quota: profile.monthly_tokens }; } return { allowed: true, remaining: remainingTokens, quota: profile.monthly_tokens }; } ``` ## 2. Quota Display Component **File: `src/components/dashboard/quota-display.tsx`** ```typescript 'use client'; import { useEffect, useState } from 'react'; import { Progress } from '@/components/ui/progress'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { AlertTriangle } from 'lucide-react'; interface QuotaInfo { used: number; total: number; percentage: number; } export function QuotaDisplay() { const [quota, setQuota] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { fetchQuota(); }, []); async function fetchQuota() { try { const response = await fetch('/api/usage/quota'); const data = await response.json(); setQuota(data); } catch (error) { console.error('Failed to fetch quota:', error); } finally { setLoading(false); } } if (loading) return
Loading...
; if (!quota) return null; const isLow = quota.percentage >= 80; const isVeryLow = quota.percentage >= 95; return (
{/* Quota bar */}
AI Tokens Used {quota.used.toLocaleString()} / {quota.total.toLocaleString()}

{quota.percentage.toFixed(1)}% used this month

{/* Warning */} {isLow && ( {isVeryLow ? 'You\'re running out of tokens! Upgrade to continue using AI features.' : 'You\'ve used most of your monthly tokens. Consider upgrading your plan.'} )}
); } ``` ## 3. Quota API **File: `src/app/api/usage/quota/route.ts`** ```typescript import { NextResponse } from 'next/server'; import { createClient } from '@/lib/supabase/server'; export async function GET() { const supabase = createClient(); const { data: { user } } = await supabase.auth.getUser(); if (!user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const { data: profile } = await supabase .from('profiles') .select('monthly_tokens, tokens_used_this_month') .eq('id', user.id) .single(); if (!profile) { return NextResponse.json({ error: 'Profile not found' }, { status: 404 }); } const percentage = (profile.tokens_used_this_month / profile.monthly_tokens) * 100; return NextResponse.json({ used: profile.tokens_used_this_month, total: profile.monthly_tokens, percentage, remaining: profile.monthly_tokens - profile.tokens_used_this_month }); } ``` --- # πŸ‘‘ X. ADMIN DASHBOARD ## 1. Admin Stats Overview **File: `src/app/admin/page.tsx`** ```typescript import { createClient } from '@/lib/supabase/server'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Users, CreditCard, DollarSign, TrendingUp } from 'lucide-react'; export default async function AdminDashboard() { const supabase = createClient(); // Get statistics const [ { count: totalUsers }, { count: activeSubscriptions }, { data: revenue }, { data: recentUsers } ] = await Promise.all([ supabase.from('profiles').select('*', { count: 'exact', head: true }), supabase.from('subscriptions').select('*', { count: 'exact', head: true }).eq('status', 'active'), supabase.from('payment_history').select('amount').eq('status', 'succeeded'), supabase.from('profiles').select('*, subscriptions(*)').order('created_at', { ascending: false }).limit(10) ]); const totalRevenue = revenue?.reduce((sum, p) => sum + p.amount, 0) || 0; const mrr = (totalRevenue / 100).toFixed(2); // Convert cents to dollars return (

Admin Dashboard

{/* Stats Grid */}
Total Users
{totalUsers || 0}
Active Subscriptions
{activeSubscriptions || 0}
Total Revenue
${mrr}
MRR
${(parseFloat(mrr) / 12).toFixed(2)}
{/* Recent Users Table */} Recent Users {recentUsers?.map((user: any) => ( ))}
Email Plan Status Joined
{user.email} {user.subscription_plan} {user.subscriptions?.[0]?.status || 'free'} {new Date(user.created_at).toLocaleDateString()}
); } ``` --- # 🎯 XI. SUMMARY & IMPLEMENTATION GUIDE ## What We've Built ### 1. **GitHub Integration** βœ… - OAuth authentication flow - Repository management (list, create) - Push code to GitHub - Webhook for sync - Commit history tracking ### 2. **Pricing Plans** βœ… - 4 tiers: Free, Pro, Team, Enterprise - Feature comparison matrix - Monthly/Yearly billing options - Clear value propositions ### 3. **Stripe Integration** βœ… - Checkout flow - Subscription management - Webhook handlers - Payment history - Billing portal ### 4. **Feature Gating** βœ… - Project limits - Token quotas - Team features - Feature availability checks - Upgrade prompts ### 5. **Usage Tracking** βœ… - Token usage monitoring - Quota display - Alerts for limits - Monthly reset ### 6. **Admin Dashboard** βœ… - User statistics - Revenue tracking - Subscription overview - User management --- ## Implementation Steps ### Week 1: GitHub Integration ```bash # 1. Setup GitHub OAuth app # 2. Implement OAuth flow # 3. Add repository management # 4. Test push functionality ``` ### Week 2: Pricing & Stripe ```bash # 1. Setup Stripe account # 2. Create products and prices # 3. Implement checkout flow # 4. Test webhooks locally with Stripe CLI stripe listen --forward-to localhost:3000/api/webhooks/stripe ``` ### Week 3: Feature Gating ```bash # 1. Implement FeatureGate class # 2. Add quota checks # 3. Create upgrade modals # 4. Test all limits ``` ### Week 4: Polish & Deploy ```bash # 1. Build admin dashboard # 2. Add analytics # 3. Test end-to-end # 4. Deploy to production ``` --- ## Environment Variables Checklist ```env # GitHub GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET= GITHUB_REDIRECT_URI= GITHUB_WEBHOOK_SECRET= # Stripe STRIPE_SECRET_KEY= STRIPE_WEBHOOK_SECRET= NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= # App NEXT_PUBLIC_APP_URL= # Supabase (already configured) NEXT_PUBLIC_SUPABASE_URL= NEXT_PUBLIC_SUPABASE_ANON_KEY= SUPABASE_SERVICE_ROLE_KEY= ``` --- ## Testing Checklist ### GitHub Integration - [ ] Connect GitHub account - [ ] List repositories - [ ] Create new repository - [ ] Push code successfully - [ ] Webhook receives events - [ ] Sync history recorded ### Pricing & Payments - [ ] View pricing page - [ ] Subscribe to Pro (test mode) - [ ] Webhook updates subscription - [ ] Payment recorded - [ ] Can cancel subscription - [ ] Downgrade to Free works ### Feature Gating - [ ] Free user blocked at 3 projects - [ ] Token limit enforced - [ ] Upgrade modal shows - [ ] Features locked correctly - [ ] Pro user has access ### Admin Dashboard - [ ] Stats display correctly - [ ] Revenue calculation accurate - [ ] User list shows - [ ] Can view subscription details --- ## Revenue Projections ### Conservative (100 users) - Free: 60 users β†’ $0 - Pro: 30 users β†’ $870/mo - Team: 8 users β†’ $792/mo - Enterprise: 2 users β†’ $998/mo - **Total MRR: $2,660** ### Growth (500 users) - Free: 250 users β†’ $0 - Pro: 180 users β†’ $5,220/mo - Team: 50 users β†’ $4,950/mo - Enterprise: 20 users β†’ $9,980/mo - **Total MRR: $20,150** ### Scale (2000 users) - Free: 800 users β†’ $0 - Pro: 900 users β†’ $26,100/mo - Team: 250 users β†’ $24,750/mo - Enterprise: 50 users β†’ $24,950/mo - **Total MRR: $75,800** --- **πŸŽ‰ Congratulations! You now have a complete monetization strategy with GitHub integration!** ## Next Steps 1. Set up GitHub OAuth app 2. Create Stripe account and products 3. Implement feature gating 4. Add analytics tracking 5. Build admin dashboard 6. Launch beta program 7. Gather user feedback 8. Iterate and improve --- ## Resources - **GitHub OAuth**: https://docs.github.com/en/developers/apps/building-oauth-apps - **Stripe Docs**: https://stripe.com/docs - **Octokit**: https://github.com/octokit/octokit.js - **Supabase Auth**: https://supabase.com/docs/guides/auth ## Need Help? - GitHub Issues: https://github.com/your-repo/issues - Discord: https://discord.gg/your-server - Email: support@yourdomain.com **Happy Building! πŸš€πŸ’°**