GitRead leverages best-in-class third-party services to provide authentication, payments, database storage, and AI capabilities.
Clerk authentication
User authentication and session management powered by Clerk.
Setup
Clerk is configured at the application root:
import { ClerkProvider } from '@clerk/nextjs'
export default function RootLayout({ children }) {
return (
<ClerkProvider>
<html lang="en">
<body>{children}</body>
</html>
</ClerkProvider>
)
}
Client-side authentication
Access user state and authentication methods:
import { useAuth, UserButton } from '@clerk/nextjs'
const { isSignedIn, userId } = useAuth()
if (!isSignedIn) {
return <SignInButton />
}
Key hooks:
useAuth() - Get authentication state and user ID
UserButton - Pre-built profile dropdown component
SignInButton - Trigger sign-in flow
Server-side authentication
Protect API routes with Clerk middleware:
import { getAuth } from '@clerk/nextjs/server'
import { NextRequest } from 'next/server'
export async function GET(req: NextRequest) {
const { userId } = getAuth(req)
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Protected route logic
}
Middleware configuration
Clerk middleware handles authentication globally:
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
const isPublicRoute = createRouteMatcher([
'/',
'/terms',
'/privacy',
'/support',
'/api/health'
])
export default clerkMiddleware(async (auth, req) => {
if (isPublicRoute(req)) {
return NextResponse.next()
}
// Protected routes require authentication
})
Public routes:
- Home page (
/)
- Legal pages (
/terms, /privacy)
- Support page (
/support)
- Health check endpoint (
/api/health)
All other routes require authentication by default, providing secure access to user-specific features.
User identification
Clerk provides a unique user ID for each account:
const { userId } = useAuth()
// Use userId to fetch user-specific data
const { data } = await supabase
.from('user_credits')
.select('*')
.eq('user_id', userId)
This ID is consistent across sessions and is used as the primary key for user data.
Stripe payments
Secure payment processing for credit purchases.
Stripe initialization
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2025-01-27.acacia',
})
Environment variables:
STRIPE_SECRET_KEY - Server-side API key (never expose to client)
NEXT_PUBLIC_APP_URL - Base URL for redirect URLs
Creating checkout sessions
Generate a Stripe Checkout session for credit purchases:
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [
{
price_data: {
currency: 'usd',
product_data: {
name: `${credits} Credits`,
description: `Purchase ${credits} credits for GitRead`,
},
unit_amount: Math.round(price * 100), // Convert dollars to cents
},
quantity: 1,
},
],
mode: 'payment',
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/`,
metadata: {
userId,
credits: credits.toString(),
},
})
Session parameters:
payment_method_types - Accept card payments
mode: 'payment' - One-time payment (not subscription)
metadata - Store user ID and credit amount for verification
Payment flow
- User selects credit amount
- API creates Stripe checkout session
- User redirects to Stripe-hosted checkout page
- After payment, redirect to success page with session ID
- Success page verifies payment and adds credits
const handleBuyCredits = async (creditAmount: number) => {
const response = await fetch('/api/create-checkout-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credits: creditAmount }),
})
const data = await response.json()
if (data.url) {
window.location.href = data.url // Redirect to Stripe
}
}
Payment verification
Verify completed payments on the success page:
const session = await stripe.checkout.sessions.retrieve(sessionId)
if (session.payment_status === 'paid') {
const credits = parseInt(session.metadata?.credits || '0')
// Validate session belongs to current user
if (session.metadata?.userId !== userId) {
return NextResponse.json({ error: 'Invalid session' }, { status: 400 })
}
// Add credits to user account
}
Preventing duplicate credits
Track processed sessions to prevent double-crediting:
const { data: processedSession } = await supabaseAdmin
.from('processed_stripe_events')
.select('*')
.eq('event_id', `session_${sessionId}`)
.single()
if (processedSession) {
return NextResponse.json({ success: true }) // Already processed
}
// Process payment and mark as complete
await supabaseAdmin
.from('processed_stripe_events')
.insert({
event_id: `session_${sessionId}`,
user_id: userId,
credits: credits,
processed_at: new Date().toISOString(),
})
Always verify payments server-side. Never trust client-provided payment status.
Supabase database
PostgreSQL database for user credits and README history.
Client initialization
Two clients for different permission levels:
import { createClient } from '@supabase/supabase-js'
// Anon key for client-side operations (read-only)
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
// Service role key for server-side operations (admin access)
const supabaseAdmin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
)
Use anon key for:
- Client-side read operations
- Public data access
Use service role for:
- API route operations
- Credit modifications
- Administrative tasks
Database schema
user_credits table
CREATE TABLE user_credits (
user_id TEXT PRIMARY KEY,
credits INTEGER NOT NULL DEFAULT 1,
updated_at TIMESTAMP DEFAULT NOW()
)
generated_readmes table
CREATE TABLE generated_readmes (
id SERIAL PRIMARY KEY,
user_id TEXT NOT NULL,
repo_url TEXT NOT NULL,
readme_content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
)
processed_stripe_events table
CREATE TABLE processed_stripe_events (
event_id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
credits INTEGER NOT NULL,
processed_at TIMESTAMP DEFAULT NOW()
)
CRUD operations
Read credits
const { data, error } = await supabaseAdmin
.from('user_credits')
.select('credits')
.eq('user_id', userId)
.single()
Update credits
await supabaseAdmin
.from('user_credits')
.upsert({
user_id: userId,
credits: newCredits,
updated_at: new Date().toISOString()
})
Save README
await supabaseAdmin
.from('generated_readmes')
.insert({
user_id: userId,
repo_url: repoUrl,
readme_content: readmeContent
})
Fetch README history
const { data } = await supabaseAdmin
.from('generated_readmes')
.select('*')
.eq('user_id', userId)
.order('created_at', { ascending: false })
Error handling
Handle common database errors:
if (error) {
if (error.code === 'PGRST116') {
// No rows found - create default record
} else if (error.code === '42501') {
// Permission denied - retry with exponential backoff
} else {
// Other errors - log and return error response
}
}
Retry logic
Implement retries for transient failures:
export async function getUserCredits(userId: string, retries = 3): Promise<number> {
try {
const { data, error } = await supabase
.from('user_credits')
.select('credits')
.eq('user_id', userId)
.single()
if (error && retries > 0) {
await sleep(1000)
return getUserCredits(userId, retries - 1)
}
return data?.credits ?? 1
} catch (error) {
if (retries > 0) {
await sleep(1000)
return getUserCredits(userId, retries - 1)
}
return 1
}
}
Retry logic helps handle temporary network issues and database contention.
OpenRouter AI gateway
Access Google Gemini 2.5 Pro through OpenRouter’s unified API.
Client setup
import { OpenAI } from 'openai'
const client = new OpenAI({
baseURL: "https://openrouter.ai/api/v1",
apiKey: process.env.OPENROUTER_API_KEY,
})
Why OpenRouter?
- Unified API for multiple AI models
- Simplified billing and rate limiting
- Automatic fallback to alternative models
- Built-in caching for cost optimization
Making AI requests
const response = await client.chat.completions.create({
model: "google/gemini-2.5-pro-preview-03-25",
messages: [
{ role: "system", content: "You are an expert technical writer." },
{ role: "user", content: prompt }
]
})
const readme = response.choices[0].message.content
Error handling
Handle rate limits and API errors:
try {
response = await client.chat.completions.create({...})
} catch (error: any) {
if (error.message?.includes("rate") || error.status === 429) {
throw new Error(`API rate limit exceeded. Please try again later.`)
}
throw error
}
Environment variables
Required configuration:
# Clerk Authentication
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_...
CLERK_SECRET_KEY=sk_...
# Stripe Payments
STRIPE_SECRET_KEY=sk_...
NEXT_PUBLIC_APP_URL=https://gitread.dev
# Supabase Database
NEXT_PUBLIC_SUPABASE_URL=https://...supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
SUPABASE_SERVICE_ROLE_KEY=eyJ...
# OpenRouter AI
OPENROUTER_API_KEY=sk_...
# GitIngest API
PYTHON_API_KEY=...
Never commit environment variables to version control. Use .env.local for local development and secure secret management in production.
Security best practices
API keys
- Store all secrets in environment variables
- Use
NEXT_PUBLIC_ prefix only for client-safe values
- Rotate keys regularly
- Monitor API usage for anomalies
Authentication
- Verify user identity on every API request
- Use Clerk’s server-side
getAuth() for protection
- Implement rate limiting per user
- Log authentication failures
Database access
- Use service role key only on server
- Implement row-level security policies
- Validate all user input before queries
- Log all credit modifications
Payment processing
- Never trust client-side payment status
- Verify all payments with Stripe server-side
- Track processed payments to prevent duplicates
- Log all payment events for auditing