Tier Configuration
```typescript
// lib/tiers.ts
export const TIER_LIMITS = {
free: {
requestsPerDay: 10,
storageGB: 1,
historyDays: 7,
exportEnabled: false,
apiAccess: false,
prioritySupport: false,
},
pro: {
requestsPerDay: Infinity,
storageGB: 100,
historyDays: 365,
exportEnabled: true,
apiAccess: true,
prioritySupport: false,
},
enterprise: {
requestsPerDay: Infinity,
storageGB: Infinity,
historyDays: Infinity,
exportEnabled: true,
apiAccess: true,
prioritySupport: true,
},
} as const;
export type UserTier = keyof typeof TIER_LIMITS;
export type TierLimits = typeof TIER_LIMITS[UserTier];
export function getTierLimits(tier: UserTier): TierLimits {
return TIER_LIMITS[tier] || TIER_LIMITS.free;
}
```
Database Schema
```sql
-- migrations/001_user_profiles.sql
CREATE TABLE user_profiles (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
subscription_tier VARCHAR(20) NOT NULL DEFAULT 'free',
-- Usage tracking
requests_today INTEGER DEFAULT 0,
requests_date DATE DEFAULT CURRENT_DATE,
storage_used_bytes BIGINT DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Atomic increment function (prevents race conditions)
CREATE OR REPLACE FUNCTION increment_daily_usage(
p_user_id UUID,
p_today DATE
)
RETURNS TABLE(new_count INTEGER, was_reset BOOLEAN) AS $$
DECLARE
v_new_count INTEGER;
v_was_reset BOOLEAN := FALSE;
BEGIN
UPDATE user_profiles
SET
requests_today = CASE
WHEN requests_date = p_today THEN requests_today + 1
ELSE 1
END,
requests_date = p_today,
updated_at = NOW()
WHERE id = p_user_id
RETURNING requests_today INTO v_new_count;
-- Check if we reset (new day)
v_was_reset := (SELECT requests_date != p_today FROM user_profiles WHERE id = p_user_id);
RETURN QUERY SELECT v_new_count, v_was_reset;
END;
$$ LANGUAGE plpgsql;
-- Index for fast lookups
CREATE INDEX idx_user_profiles_tier ON user_profiles(subscription_tier);
```
Rate Limit Functions
```typescript
// lib/rate-limits.ts
import type { SupabaseClient } from '@supabase/supabase-js';
import { TIER_LIMITS, type UserTier } from './tiers';
export interface RateLimitResult {
allowed: boolean;
limit: number;
remaining: number;
reset: number; // Unix timestamp
tier: UserTier;
}
export async function checkRateLimit(
supabase: SupabaseClient,
userId: string
): Promise {
const today = new Date().toISOString().split('T')[0];
const { data: profile } = await supabase
.from('user_profiles')
.select('subscription_tier, requests_today, requests_date')
.eq('id', userId)
.single();
if (!profile) {
return {
allowed: true,
limit: TIER_LIMITS.free.requestsPerDay,
remaining: TIER_LIMITS.free.requestsPerDay,
reset: getResetTimestamp(),
tier: 'free',
};
}
const tier = (profile.subscription_tier as UserTier) || 'free';
const limit = TIER_LIMITS[tier].requestsPerDay;
// Unlimited tier
if (limit === Infinity) {
return {
allowed: true,
limit: -1,
remaining: -1,
reset: getResetTimestamp(),
tier,
};
}
// Check usage from today
const used = profile.requests_date === today ? profile.requests_today : 0;
const remaining = Math.max(0, limit - used);
return {
allowed: remaining > 0,
limit,
remaining,
reset: getResetTimestamp(),
tier,
};
}
export async function incrementUsage(
supabase: SupabaseClient,
userId: string
): Promise {
const today = new Date().toISOString().split('T')[0];
const { error } = await supabase.rpc('increment_daily_usage', {
p_user_id: userId,
p_today: today,
});
return !error;
}
function getResetTimestamp(): number {
const tomorrow = new Date();
tomorrow.setUTCDate(tomorrow.getUTCDate() + 1);
tomorrow.setUTCHours(0, 0, 0, 0);
return Math.floor(tomorrow.getTime() / 1000);
}
export function getRateLimitHeaders(result: RateLimitResult): Record {
const headers: Record = {
'X-RateLimit-Limit': result.limit === -1 ? 'unlimited' : String(result.limit),
'X-RateLimit-Remaining': result.remaining === -1 ? 'unlimited' : String(result.remaining),
'X-RateLimit-Reset': String(result.reset),
};
if (!result.allowed) {
const retryAfter = result.reset - Math.floor(Date.now() / 1000);
headers['Retry-After'] = String(Math.max(0, retryAfter));
}
return headers;
}
export function createRateLimitResponse(result: RateLimitResult): Response {
return new Response(
JSON.stringify({
error: 'Rate limit exceeded',
code: 'RATE_LIMIT_EXCEEDED',
message: You've used all ${result.limit} requests for today. Upgrade for unlimited access.,
reset: result.reset,
upgradeUrl: '/pricing',
}),
{
status: 429,
headers: {
'Content-Type': 'application/json',
...getRateLimitHeaders(result),
},
}
);
}
```
Using in API Routes
```typescript
// app/api/generate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createServerSupabaseClient } from '@/lib/supabase-server';
import {
checkRateLimit,
incrementUsage,
createRateLimitResponse,
getRateLimitHeaders
} from '@/lib/rate-limits';
export async function POST(request: NextRequest) {
const userId = request.headers.get('x-user-id');
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const supabase = await createServerSupabaseClient();
// Check rate limit BEFORE expensive operation
const rateLimit = await checkRateLimit(supabase, userId);
if (!rateLimit.allowed) {
return createRateLimitResponse(rateLimit);
}
// Process the request
const body = await request.json();
const result = await processRequest(body);
// Increment usage AFTER successful operation
await incrementUsage(supabase, userId);
return NextResponse.json(
{ result },
{ headers: getRateLimitHeaders(rateLimit) }
);
}
```
Feature Gate Component
```typescript
// components/FeatureGate.tsx
'use client';
import { useUser } from '@/hooks/useUser';
import { TIER_LIMITS, type UserTier } from '@/lib/tiers';
interface FeatureGateProps {
feature: keyof typeof TIER_LIMITS.free;
children: React.ReactNode;
fallback?: React.ReactNode;
}
export function FeatureGate({ feature, children, fallback }: FeatureGateProps) {
const { tier, isLoading } = useUser();
if (isLoading) return null;
const limits = TIER_LIMITS[tier as UserTier];
const hasAccess = limits[feature] === true || limits[feature] === Infinity;
if (!hasAccess) {
return fallback ?? (
This feature requires Pro
Upgrade now
);
}
return <>{children}>;
}
// Usage
feature="apiAccess"
fallback={}
>
```
Usage Display Component
```typescript
// components/UsageDisplay.tsx
'use client';
import { useUser } from '@/hooks/useUser';
import { TIER_LIMITS } from '@/lib/tiers';
export function UsageDisplay() {
const { tier, profile } = useUser();
const limits = TIER_LIMITS[tier];
if (limits.requestsPerDay === Infinity) {
return Unlimited;
}
const used = profile?.requests_today ?? 0;
const remaining = Math.max(0, limits.requestsPerDay - used);
const percentage = (used / limits.requestsPerDay) * 100;
return (
{remaining} remaining today
{used} / {limits.requestsPerDay}
className={h-full ${percentage > 80 ? 'bg-red-500' : 'bg-blue-500'}}
style={{ width: ${Math.min(100, percentage)}% }}
/>
{remaining === 0 && (
Upgrade for unlimited
)}
);
}
```