This file is the single source of truth for AI agents working on this repository. Read it before making any changes.
BettaResume is a professional resume builder SPA (Single Page Application). Users can create, edit, and manage multiple resumes with real-time formatting, multiple templates, section management, and PDF export.
- Frontend: Next.js 16 static export, deployed to GitHub Pages
- Backend: Cloudflare Worker exposing a tRPC API
- Database: Cloudflare D1 (SQLite) via Drizzle ORM
- Auth: Clerk (hosted sign-in, JWT verification)
bettaresume/
├── src/ # Next.js frontend (root workspace)
├── api/ # Cloudflare Worker backend (npm workspace: bettaresume-api)
├── packages/
│ └── types/ # Shared Zod schemas + TypeScript types (npm workspace: @bettaresume/types)
├── docs/ # Architecture documentation (BACKEND.md, FRONTEND.md, etc.)
├── bruno/ # Bruno API test collection
├── scripts/ # Build scripts (postbuild.js)
├── public/ # Static assets
├── package.json # Root workspace config + frontend deps
├── biome.jsonc # Linter/formatter config (Biome)
├── tsconfig.json # Root TypeScript config (frontend + types)
├── next.config.js # Next.js config (static export)
└── .env.example # Frontend env var template
| Tool | Purpose |
|---|---|
| Next.js 16 (App Router) | Framework, static export (output: "export") |
| React 19 | UI library |
| TypeScript 5 | Type safety |
| Tailwind CSS v4 | Styling |
| shadcn/ui + Radix UI | Accessible UI primitives |
| tRPC v11 + React Query v5 | Type-safe API calls + data fetching |
Clerk (@clerk/nextjs, @clerk/react) |
Authentication |
| Zustand v5 | Local UI state management |
| TipTap v3 | Rich text editor |
React PDF (@react-pdf/renderer) |
PDF export |
| dnd-kit | Drag-and-drop section reordering |
| Biome | Linting + formatting |
| Tool | Purpose |
|---|---|
| Cloudflare Workers | Serverless runtime |
| tRPC v11 | Type-safe API |
| Drizzle ORM | Database ORM |
| Cloudflare D1 (SQLite) | Database |
Clerk (@clerk/backend) |
Auth token verification |
| Wrangler | Dev server + deployment |
| Zod v3 | Input validation |
| Tool | Purpose |
|---|---|
| Zod v4 | Schema definitions |
| TypeScript | Type generation from schemas |
- Node.js 20+
- npm 11 (see
packageManagerinpackage.json) - Wrangler CLI (installed locally in
api/devDeps — no global install needed) - Clerk account — get free API keys at https://dashboard.clerk.com
Frontend — create .env.local in the repository root:
NEXT_PUBLIC_API_URL=http://localhost:4000
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_your_key_here
NEXT_PUBLIC_DEV_MODE=trueBackend — create api/.dev.vars:
CLERK_SECRET_KEY=sk_test_your_key_here
CLERK_PUBLISHABLE_KEY=pk_test_your_key_here# 1. Install all workspace dependencies
npm install
# 2. Apply DB migrations and seed local D1 database
npm run db:reset-seed
# 3. Start both servers (frontend + API) concurrently
npm run devnpm run dev runs build:types → db:reset-seed → dev:all (frontend on :3000, API on :4000).
# Development
npm run dev # Start everything (types + DB + frontend + API)
npm run dev:frontend # Next.js only (port 3000, Turbopack)
npm run dev:api # Wrangler Worker only (port 4000)
# Code quality
npm run check # Biome lint + format check
npm run check:write # Biome auto-fix (safe)
npm run check:unsafe # Biome auto-fix (unsafe)
npm run typecheck # TypeScript type check (no emit)
# Build
npm run build # build:types + next build + postbuild
npm run build:types # Build @bettaresume/types package only
# Database (delegates to api/ workspace)
npm run db:migrate # Apply migrations to local D1
npm run db:reset # Re-apply all migrations
npm run db:seed # Seed local D1 with dummy data
npm run db:reset-seed # Reset + migrate + seed (combined)
npm run db:studio # Open Drizzle Studio (visual DB editor)
# Clean
npm run clean # Remove .next, api/.wrangler, packages/types/distCustom hash-based router (src/lib/hash-router.tsx) for GitHub Pages compatibility. All routes use #/path fragments.
Routes defined in src/app/router.tsx:
#/→ redirects to#/dashboardif signed in, else Clerk sign-in#/login→ Clerk redirect#/dashboard→ Dashboard (protected)#/resume-editor/:id→ Resume editor (protected)
src/
├── app/
│ ├── layout.tsx # Root providers (Theme, Clerk, tRPC, Toasts)
│ ├── page.tsx # Single entry: mounts HashRouterProvider + AppRouter
│ ├── router.tsx # Route matching + auth guard
│ ├── protected-route.tsx # Auth guard component
│ ├── provider.tsx # tRPC provider
│ └── splash-screen.tsx # Loading screen
├── features/
│ ├── auth/ # Auth store + login view
│ ├── dashboard/ # Dashboard view + components
│ └── resume-editor/ # Resume editor view, store, types, utils
├── components/
│ ├── ui/ # shadcn/ui primitives
│ ├── export/ # PDF export (pdf-document.tsx, export-buttons.tsx)
│ ├── providers/ # App-level React providers
│ ├── rich-text-editor/ # TipTap rich text editor
│ └── sections-forms/ # Per-section form components
├── hooks/ # Custom React hooks
├── lib/
│ ├── hash-router.tsx # Hash router implementation
│ ├── api.ts # Sync manager + backend status
│ ├── trpc/ # tRPC client setup
│ ├── fonts.ts # Font definitions for PDF
│ └── utils.ts # Utility functions (cn, etc.)
└── styles/ # Global CSS
- tRPC + React Query: All server state (resumes, sections, user). Use
trpc.<router>.<procedure>.useQuery()/.useMutation(). - Zustand (
features/*/**.store.ts): Local UI state —auth.store.ts(auth),resume.store.ts(current resume being edited).
| Alias | Resolves to |
|---|---|
@/* |
src/* |
@bettaresume/types |
packages/types/src/index.ts |
Component pattern (section forms):
function ExperienceForm({ data, onChange }) {
const [localData, setLocalData] = useState(data);
const hasChanges = JSON.stringify(localData) !== JSON.stringify(data);
return (
<>
<FormSaveBar hasChanges={hasChanges} onSave={() => onChange(localData)} />
{/* form fields */}
</>
);
}No nested buttons in Accordion (causes React hydration error):
// ❌ Wrong
<AccordionTrigger><Button>Click</Button></AccordionTrigger>
// ✅ Correct
<div className="flex"><AccordionTrigger>Title</AccordionTrigger><Button>Action</Button></div>Navigation:
import { useHashNavigate } from '@/lib/hash-router';
const navigate = useHashNavigate();
navigate('/resume-editor/123');Cloudflare Worker fetch handler:
OPTIONS *→ CORS preflightGET/POST /trpc/*→ tRPC handler (adds CORS headers to response)GET /health→ JSON health check- Everything else → 404
export const appRouter = router({
user: userRouter,
resume: resumeRouter,
section: sectionRouter,
auth: authRouter,
});
export type AppRouter = typeof appRouter; // imported by frontend for type safetyCreated for every request. Contains:
db— Drizzle DB instanceuser— Clerk user object (ornull)userId— Clerk user ID string (ornull)env— Cloudflare bindings (bettaresume_d1,CLERK_SECRET_KEY, etc.)clerkClient— Clerk backend SDK
publicProcedure— No auth requiredprotectedProcedure— ThrowsUNAUTHORIZEDif no user in context
api/src/trpc/procedures/
├── auth.ts # auth.getUser
├── user.ts # user.get, user.upsert
├── resume.ts # resume.list, resume.getById, resume.create, resume.update, resume.delete
├── resume-section.ts # section operations on resumes
└── section.ts # section.upsert, section.reorder, section.delete
- Schema (
schema.ts):users,resumes,sections,accounts,sessions,verificationTokens - Migrations (
api/drizzle/): SQL files applied by Wrangler - Seed (
api/src/db/seed.sql): Dummy data for local dev
- Worker name:
bettaresume-api-server - Custom domain:
api.bettaresume.com - D1 binding:
bettaresume_d1→ databasebettaresume - Dev port:
4000
- Create or edit a file in
api/src/trpc/procedures/ - Define the procedure with
publicProcedureorprotectedProcedure - Add it to the router in
api/src/root.ts - TypeScript inference automatically propagates to the frontend
All types and Zod schemas shared between frontend and backend.
Key exports (packages/types/src/schemas.ts):
sectionTypeSchema→"personal-info" | "summary" | "experience" | "education" | "skills" | "projects" | "certifications" | "awards" | "languages" | "publications" | "volunteer" | "references" | "custom"templateTypeSchema→"minimal" | "modern" | "classic" | "professional" | "creative" | "executive" | "tech"- Per-section content schemas:
personalInfoSchema,experienceSchema,educationSchema,skillCategorySchema,projectSchema,certificationSchema,awardSchema,languageSchema,publicationSchema,volunteerSchema,referenceSchema - Resume schemas:
createResumeSchema,updateResumeSchema,resumeSettingsSchema
Key exports (packages/types/src/types.ts):
SectionType,TemplateType,Resume,ResumeSection,ResumeWithSections,User,SyncStatus,SyncState
Always add new section types or templates to this package first — both API and frontend depend on it.
- Add the new type string to
sectionTypeSchemainpackages/types/src/schemas.ts - Add a content schema for it in the same file
- Export the new type from
packages/types/src/types.ts - Add a
SECTION_CONFIGSentry insrc/features/resume-editor/types.ts(default title, icon, etc.) - Create a form component in
src/components/sections-forms/ - Register it in
src/features/resume-editor/resume-editor.tsx(section renderer) - Update PDF preview in
src/components/export/pdf-document.tsx
| Table | Purpose |
|---|---|
User |
User accounts (Clerk user IDs) |
Resume |
Resume documents (name, template, metadata JSON, archived flag) |
Section |
Resume sections (type, order, visible, content JSON) |
Account |
OAuth accounts (NextAuth legacy) |
Session |
Auth sessions (NextAuth legacy) |
VerificationToken |
Email verification (NextAuth legacy) |
Section content is stored as serialized JSON in the content column. The schema matches the Zod schemas in @bettaresume/types.
Migrations: api/drizzle/*.sql — generated by drizzle-kit generate, applied by wrangler d1 migrations apply.
To change the schema:
# 1. Edit api/src/db/schema.ts
# 2. Generate migration
npm run db:generate # (runs drizzle-kit generate in api/)
# 3. Apply locally
npm run db:migrate- User visits app → redirected to Clerk hosted sign-in (
<RedirectToSignIn />) - After Clerk sign-in → JWT token stored by Clerk SDK in browser
- Frontend attaches
Authorization: Bearer <token>header to all tRPC requests - Backend (
api/src/trpc/context.ts) verifies JWT using@clerk/backend - On first sign-in,
user.upsertprocedure creates the user record in D1
Dev bypass: In NODE_ENV=development, isDevBypass = true in router.tsx skips Clerk auth checks. The frontend still needs valid Clerk keys for auth-dependent features, but routing works without signing in.
Linter/Formatter: Biome (configured in biome.jsonc).
npm run check # Check all files
npm run check:write # Auto-fix safe issuesRules:
- Sorted imports (Biome
organizeImports) - Sorted Tailwind classes (
useSortedClassesforclsx,cva,cn) - TypeScript strict mode +
noUncheckedIndexedAccess verbatimModuleSyntax— useimport typefor type-only imports
Indentation: Tabs (Biome default).
npm run buildproduces./out/(static export)- CI (
.github/workflows/cd.yml) deploys./out/to GitHub Pages onmainpush - Required GitHub Actions vars:
CLERK_PUBLISHABLE_KEY,API_URL
cd api && npx wrangler deploy- CI deploys on
mainpush alongside frontend - Required Cloudflare secrets:
CLERK_SECRET_KEY,CLERK_PUBLISHABLE_KEY - Required GitHub Actions secrets:
CLOUDFLARE_API_TOKEN,CLERK_SECRET_KEY
Bruno collection at ./bruno/ — open with Bruno:
# Health check
curl http://localhost:4000/health
# tRPC (needs Bearer token from Clerk)
curl http://localhost:4000/trpc/resume.list \
-H "Authorization: Bearer <clerk-token>"| Problem | Solution |
|---|---|
| CORS errors | Backend adds Access-Control-Allow-* headers to all responses, including x-dev-mode header |
| "Publishable key not valid" | Check api/.dev.vars — pk_test_... → CLERK_PUBLISHABLE_KEY, sk_test_... → CLERK_SECRET_KEY |
| Foreign key constraint on resume create | user.upsert must be called first (happens on login). Run npm run db:reset-seed locally |
| Nested button hydration error | Never put <Button> inside <AccordionTrigger> — place action buttons outside the trigger |
| Port mismatch | NEXT_PUBLIC_API_URL must match Wrangler dev port (default 4000) |
| Type errors in build | Build uses ignoreBuildErrors: true; run npm run typecheck separately |
| Stale D1 state | npm run db:reset or npm run clean && npm run db:reset-seed |
All architecture docs are in docs/:
docs/DEVELOPMENT.md— full setup walkthroughdocs/FRONTEND.md— detailed frontend architecturedocs/BACKEND.md— detailed backend architecturedocs/DATABASE.md— schema + migration detailsdocs/API.md— tRPC procedure referencedocs/TYPES.md— shared types reference