Land more interviews. Upload your resume and a job description; get an ATS-style score, missing keywords, improved bullet suggestions, and a concise cover‑letter draft.
- AI tailoring (OpenAI, JSON mode): improved bullets + cover‑letter draft
- ATS insights: match score and missing keywords (lightweight local analysis)
- Auth & history: email/password login and per-user run history (Firestore)
- Free vs Pro: daily free limit vs. unlimited (Stripe subscriptions)
- Billing UX: Upgrade, Manage Billing, and Resume Subscription flows
- Production-ready Next.js app: App Router, TypeScript, Tailwind v4
- Frontend/Server: Next.js (App Router), TypeScript, Tailwind CSS v4
- Auth & Data: Firebase Auth + Firestore
- Billing: Stripe (Checkout + Customer Portal + Webhooks, test mode initially)
- AI: OpenAI Chat Completions (JSON output)
- Hosting: Vercel (Node runtime for API routes)
src/
app/
api/
tailor/route.ts # main generation route
history/route.ts # user runs
me/route.ts # plan state for billing buttons
billing/
checkout/route.ts # Stripe Checkout
portal/route.ts # Stripe Customer Portal
stripe/
webhook/route.ts # Stripe webhook (raw body, Node runtime)
(pages + layout)
components/
AIForm.tsx # editor + results
billing/BillingButtons.tsx # plan-aware billing UI
Section.tsx # simple panel
(auth/status, footer, etc.)
lib/
validators.ts # zod schemas & TailorResponse type
ats.ts # lightweight ATS keyword coverage
firebase/ # client & admin init
stripe.ts # stripe client/server helpers
Body:
{ "resumeText": "string (>=50)", "jobDescription": "string (>=50)" }Response:
{
"atsScore": 0,
"missingKeywords": ["string"],
"improvedBullets": ["string"],
"coverLetterDraft": "string"
}Returns the user’s recent runs from users/{uid}/runs.
Plan state used by the header:
{
"isPro": true,
"hasCustomer": true,
"subscriptionStatus": "active",
"cancelAtPeriodEnd": false,
"scheduledCancel": false,
"currentPeriodEnd": "2025-10-31T...Z",
"cancelAt": null
}Starts a Stripe subscription Checkout session.
Opens the Customer Portal. If the user has no Stripe customer yet, returns 400 { "error": "NO_CUSTOMER" } and the client falls back to Checkout.
Raw-body verified webhook (Node runtime). Updates Firestore plan fields on subscription create/update/delete. Supports flexible billing (may fall back to item period end / cancel_at).
-
users/{uid}:
isPro, stripeCustomerId, stripeSubscriptionId, subscriptionStatus, cancelAtPeriodEnd, scheduledCancel, currentPeriodEnd, cancelAt, canceledAt, stripeUpdatedAt -
users/{uid}/runs/{runId}:
resumePreview, jdPreview, tailoredResume, coverLetter, bullets, resumeLen, jdLen, atsScore, missingKeywordsCount, improvedBulletsCount, coverLetterLen, model, plan, createdAt -
usage_daily/{uid_YYYY-MM-DD}:
userId, date, count, createdAt, updatedAt(used for free daily limit; avoids composite index).
- Signed out → “Sign in to upgrade”
- Free or Pro with no Stripe customer → “Upgrade to Pro”
- Pro + customer →
- If
scheduledCancelorcancelAtPeriodEnd→ show Resume subscription + Manage Billing + “Access until {date}” (currentPeriodEnd ?? cancelAt) - Else just Manage Billing (+ “Access until” when available)
- If
- Richer ATS metrics (skills taxonomy, seniority verbs, readability)
- Shareable public links for runs
- Observability: latency, token usage, failure analytics

