Next.js 14 app for analyzing and improving documents (CV, legal, academic, business) with AI. Includes upload, analysis, Stripe billing, export (PDF/DOCX), and transactional emails.
- Next.js 14 (App Router), TypeScript, Tailwind CSS
- Clerk — auth (Google + email)
- Supabase — Postgres + Storage (server uses service role key only on the server; never expose it client-side)
- Anthropic — Claude for document analysis
- Stripe — subscriptions (Pro $9/mo, Business $29/mo)
- Resend — welcome, usage limit, upgrade emails
- Upstash Redis — rate limiting (10 req/min per user for
/api/analyzeand/api/upload)
npm installCopy the example env file and fill in values:
cp .env.local.example .env.localSee .env.local.example for every variable. Required:
- Clerk:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,CLERK_SECRET_KEY,CLERK_WEBHOOK_SECRET - Supabase:
NEXT_PUBLIC_SUPABASE_URL,NEXT_PUBLIC_SUPABASE_ANON_KEY,SUPABASE_SERVICE_ROLE_KEY(server-only) - Anthropic:
ANTHROPIC_API_KEY - Stripe:
STRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET,STRIPE_PRO_PRICE_ID,STRIPE_BUSINESS_PRICE_ID - Resend:
RESEND_API_KEY, optionalRESEND_FROM_EMAIL - Upstash (optional but recommended):
UPSTASH_REDIS_REST_URL,UPSTASH_REDIS_REST_TOKEN - App:
NEXT_PUBLIC_APP_URL(e.g.http://localhost:3000)
Run the following SQL in the Supabase SQL editor (or migrations).
Users (sync with Clerk via webhook; service role used only in API routes):
create table if not exists public.users (
id text primary key,
email text,
plan text not null default 'free' check (plan in ('free', 'pro', 'business')),
usage_count int not null default 0,
usage_reset_at timestamptz,
stripe_customer_id text,
created_at timestamptz not null default now()
);Documents:
create table if not exists public.documents (
id uuid primary key default gen_random_uuid(),
user_id text not null references public.users(id),
filename text not null,
doc_type text not null,
score numeric,
created_at timestamptz not null default now()
);Rewrites:
create table if not exists public.rewrites (
id uuid primary key default gen_random_uuid(),
document_id uuid not null references public.documents(id),
original_text text not null,
fixed_text text not null,
created_at timestamptz not null default now()
);Storage: Create a storage bucket named documents (private or with RLS as needed). API uploads files under {user_id}/{timestamp}-{filename}.
RLS: Enable RLS on tables if desired; all server-side access uses the service role client in lib/supabase/server.ts, which bypasses RLS. The anon key is only used in lib/supabase/client.ts for client-side usage and must never be given the service role key.
- Create an application and add Google + Email (or email/password) sign-in.
- In Dashboard → Webhooks, add endpoint:
https://your-domain.com/api/webhooks/clerk(or use ngrok for local:https://xxx.ngrok.io/api/webhooks/clerk). Subscribe touser.created. Copy the signing secret intoCLERK_WEBHOOK_SECRET.
- Create Products: Pro ($9/month) and Business ($29/month). Copy each Price ID to
STRIPE_PRO_PRICE_IDandSTRIPE_BUSINESS_PRICE_ID. - Webhooks: add endpoint
https://your-domain.com/api/stripe/webhook. Events:checkout.session.completed,customer.subscription.deleted. Copy signing secret toSTRIPE_WEBHOOK_SECRET. Use the raw body for verification (this app usesreq.text()in the webhook handler).
- Create an API key and set
RESEND_API_KEY. Optionally setRESEND_FROM_EMAILto a verified domain.
npm run devOpen http://localhost:3000. Sign in (Clerk), then use the dashboard. Protect /dashboard via middleware (already configured).
POST /api/upload— multipart file (PDF/DOCX, max 5MB) +docType; returnsdocumentId,extractedText. Rate limited.POST /api/analyze— body:{ documentId, extractedText, docType }; returns analysis andrewriteId. Free plan: 1 run/day; then 403 + usage_limit email. Rate limited.POST /api/stripe/checkout— body:{ priceId }; returns Stripe Checkout URL.POST /api/stripe/webhook— Stripe webhooks (raw body).POST /api/stripe/portal— returns Stripe Customer Portal URL.POST /api/export— body:{ rewriteId, format }(pdf|docx|txt). Free users get 403 with{ error: "upgrade_required" }for PDF/DOCX.POST /api/webhooks/clerk— Clerkuser.created→ create user row, send welcome email.
- Supabase: The service role key is used only in server-side code (
lib/supabase/server.ts, API routes). It is never exposed to the client. The client uses onlyNEXT_PUBLIC_SUPABASE_ANON_KEYvialib/supabase/client.ts.