Skip to main content
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

  1. User selects credit amount
  2. API creates Stripe checkout session
  3. User redirects to Stripe-hosted checkout page
  4. After payment, redirect to success page with session ID
  5. 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

Build docs developers (and LLMs) love