Turn your PDF resume into a hosted web portfolio in under 60 seconds.
Upload a PDF. AI parses it. Get a shareable link.
- Instant PDF Parsing - AI extracts your information automatically
- Clean Public URLs - Get
yoursite.com/yournameimmediately - Privacy Controls - Show/hide phone numbers and addresses
- Multiple Templates - Professional, modern designs
- Mobile Responsive - Looks great on all devices
- SEO Optimized - Proper metadata, Open Graph tags
| Layer | Technology |
|---|---|
| Framework | vinext (Vite-based Next.js) |
| Runtime | Cloudflare Workers |
| Database | Cloudflare D1 (SQLite) + Drizzle ORM |
| Auth | Better Auth (Google OAuth + email/password) |
| Storage | Cloudflare R2 (S3-compatible) |
| AI Parsing | OpenRouter via Cloudflare AI Gateway (openai/gpt-oss models) |
| Styling | shadcn/ui + Tailwind CSS 4 |
We chose Cloudflare Workers over traditional hosting for several reasons:
- Edge Computing: Code runs in 300+ data centers worldwide, closest to your users
- Cold Start: ~0ms cold starts vs. 200-500ms on traditional serverless
- Latency: Sub-50ms response times globally
- Free Tier: 100,000 requests/day free
- D1 Database: 5GB free, built-in SQLite
- R2 Storage: 10GB free, no egress fees
- Total: A production app can run free for most use cases
- No Container Management: Just deploy code
- Automatic Scaling: From 0 to millions of requests
- Integrated Stack: D1, R2, and Workers work seamlessly together
- No
fsModule: Must use R2 for file operations - No Next.js
<Image />Component: Use<img>with CSS instead - Edge Middleware Limits: No D1 access in middleware
- Bundle Size: Keep dependencies minimal
- Bun v1.0+ (package manager)
- Cloudflare Account with R2 and D1 enabled
- Google Cloud Console project for OAuth
- OpenRouter account for AI parsing
# Clone the repository
git clone https://github.com/divkix/clickfolio.me.git
cd clickfolio.me
# Install dependencies
bun install
# Copy environment template
cp .env.example .dev.vars
# Set up local database
bun run db:migrate
# Start development server
bun run devIf you are not technical, follow this exact checklist. You only need a terminal and browser.
What you need
- A Cloudflare account (free is fine)
- A Google Cloud account (for Google Sign-In)
- An OpenRouter account (for AI parsing)
- Bun installed (copy/paste this in Terminal):
curl -fsSL https://bun.sh/install | bash
Step 0: Get the code
- Download the repo ZIP from GitHub and unzip it, or use:
git clone https://github.com/divkix/clickfolio.me.git cd clickfolio.me - Install dependencies:
bun install
Step 1: Create Cloudflare D1 database
- In Terminal:
bunx wrangler d1 create clickfolio-db
- Copy the
database_idprinted in the terminal. - Open
wrangler.jsoncand replace thedatabase_idvalue.
Step 2: Create Cloudflare R2 bucket
- Go to Cloudflare Dashboard β R2 β Create bucket.
- Name it
clickfolio-bucket. - The bucket is accessed via binding in wrangler.jsonc - no API tokens needed.
Step 3: Configure R2 CORS In Cloudflare R2 bucket settings β CORS, paste:
[
{
"AllowedOrigins": ["http://localhost:3000", "https://your-domain.com"],
"AllowedMethods": ["GET", "PUT", "POST"],
"AllowedHeaders": ["*"],
"MaxAgeSeconds": 3000
}
]Step 4: Set up Google OAuth
- Go to Google Cloud Console.
- Create project β APIs & Services β Credentials.
- Create OAuth Client ID (Web app).
- Add redirect URIs:
http://localhost:3000/api/auth/callback/googlehttps://your-domain.com/api/auth/callback/google
- Copy Client ID and Client Secret.
Step 5: Set up OpenRouter
- Create OpenRouter account β API Keys.
- Copy your API key.
Step 6: Add secrets to Cloudflare (production) Run each command and paste the value when prompted:
bunx wrangler secret put BETTER_AUTH_SECRET
bunx wrangler secret put BETTER_AUTH_URL # Also used as app URL
bunx wrangler secret put GOOGLE_CLIENT_ID
bunx wrangler secret put GOOGLE_CLIENT_SECRET
bunx wrangler secret put CF_AI_GATEWAY_ACCOUNT_ID
bunx wrangler secret put CF_AI_GATEWAY_ID
bunx wrangler secret put CF_AIG_AUTH_TOKENStep 7: Deploy
bun run db:migrate:prod
bun run deployStep 8: Add your domain Cloudflare Dashboard β Workers & Pages β your worker β Settings β Domains & Routes.
Important: After domain is connected, update this secret:
BETTER_AUTH_URL=https://your-domain.com
Then redeploy:
bun run deployIf you followed the steps above, the site should be live at your domain.
-
Create a Cloudflare account at cloudflare.com
-
Create D1 Database
bunx wrangler d1 create clickfolio-db
Copy the
database_idtowrangler.jsonc -
Create R2 Bucket
- Go to Cloudflare Dashboard > R2
- Create bucket named
clickfolio-bucket - The bucket is accessed via binding in
wrangler.jsonc- no API tokens needed
-
Configure R2 CORS Add CORS policy in R2 bucket settings:
[ { "AllowedOrigins": ["http://localhost:3000", "https://your-domain.com"], "AllowedMethods": ["GET", "PUT", "POST"], "AllowedHeaders": ["*"], "MaxAgeSeconds": 3000 } ]
- Go to Google Cloud Console
- Create a new project (or select existing)
- Go to APIs & Services > Credentials
- Create OAuth 2.0 Client ID (Web application type)
- Add authorized redirect URIs:
- Development:
http://localhost:3000/api/auth/callback/google - Production:
https://your-domain.com/api/auth/callback/google
- Development:
- Copy Client ID and Client Secret
- Create account at openrouter.ai
- Go to API Keys
- Create new API key and copy it
- Get your OpenRouter HTTP Referer and App Title from the dashboard
Cloudflare AI Gateway This project uses Cloudflare AI Gateway for AI calls.
- Go to Cloudflare Dashboard > AI > AI Gateway
- Create a gateway
- Store your OpenRouter token in Cloudflare Secrets Store
- You will use
CF_AI_GATEWAY_*environment variables
Create .dev.vars for development:
# Generate a secure secret
openssl rand -base64 32
# Copy to .dev.vars
BETTER_AUTH_SECRET=your-generated-secret
BETTER_AUTH_URL=http://localhost:3000
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-client-secret
# Cloudflare AI Gateway (BYOK - OpenRouter key stored in CF Secrets Store)
CF_AI_GATEWAY_ACCOUNT_ID=your-account-id
CF_AI_GATEWAY_ID=your-gateway-id
CF_AIG_AUTH_TOKEN=your-gateway-auth-token
See .env.example for complete template with all options.
-
Apply database migrations
bun run db:migrate:prod
-
Set production secrets
bunx wrangler secret put BETTER_AUTH_SECRET bunx wrangler secret put BETTER_AUTH_URL # Also used as app URL bunx wrangler secret put GOOGLE_CLIENT_ID bunx wrangler secret put GOOGLE_CLIENT_SECRET bunx wrangler secret put CF_AI_GATEWAY_ACCOUNT_ID bunx wrangler secret put CF_AI_GATEWAY_ID bunx wrangler secret put CF_AIG_AUTH_TOKEN -
Deploy
bun run deploy
-
Configure custom domain (optional)
- In Cloudflare Dashboard > Workers & Pages > Your Worker
- Add custom domain in Settings > Domains & Routes
# Development
bun run dev # Start dev server at localhost:3000
bun run lint # Oxlint linting (via vp lint)
bun run fix # Oxlint + Oxfmt auto-fix (via vp check --fix)
bun run type-check # TypeScript check
# Build & Deploy
bun run build # Vite production build (vinext)
bun run build:worker # Alias for build
bun run preview # Local Cloudflare preview
bun run deploy # Build and deploy to Cloudflare Workers
# Database (D1 + Drizzle)
bun run db:generate # Generate migrations from schema
bun run db:migrate # Apply migrations locally
bun run db:migrate:prod # Apply migrations to production
bun run db:studio # Drizzle Studio UI (port 4984)
bun run db:reset # Wipe local D1 and re-migrate
# Testing
bun run test # All tests
bun run test:unit # Unit tests (fast, no retries)
bun run test:integration # Integration tests
bun run test:security # Security tests
bun run test:coverage # All tests + coverage
bun run test:ci # CI mode (JSON reporter)
bun run test:ui # Interactive UI mode
# Quality
bun run ci # type-check + lint + test + buildapp/
βββ api/ # API routes (auth, upload, resume, etc.)
βββ (admin)/ # Admin dashboard pages
β βββ admin/
β β βββ users/ # User management
β β βββ referrals/ # Referral analytics
β β βββ resumes/ # Resume management
β β βββ analytics/ # Site analytics
β βββ layout-client.tsx # Admin layout wrapper
βββ (protected)/ # Auth-gated pages
β βββ dashboard/ # User dashboard with analytics
β βββ edit/ # Resume content editor
β βββ settings/ # Privacy & theme settings
β βββ themes/ # Theme gallery
β βββ waiting/ # AI parsing status (WebSocket)
β βββ wizard/ # Onboarding wizard
βββ (public)/ # Public pages requiring no auth
β βββ verify-email/ # Email verification page
βββ [handle]/ # Public resume viewer /@handle
βββ for/ # Landing pages by profession
β βββ student/
β βββ software-engineer/
β βββ designer/
β βββ product-manager/
β βββ marketer/
β βββ consultant/
βββ blog/ # Blog posts & content marketing
βββ preview/[id]/ # Template preview (before claiming)
βββ page.tsx # Homepage
βββ layout.tsx # Root layout
βββ globals.css # Global styles
components/
βββ templates/ # 10 resume template components
βββ ui/ # shadcn/ui components
βββ dashboard/ # Dashboard-specific components
βββ icons/ # Custom icon components
βββ analytics/ # Analytics components
βββ *.tsx # Shared components (Footer, Logo, etc.)
lib/
βββ auth/ # Better Auth configuration
βββ ai/ # AI parsing (OpenRouter via CF AI Gateway)
βββ cron/ # Scheduled task implementations
βββ db/ # Drizzle schema, client, migrations
βββ durable-objects/ # WebSocket Durable Object
βββ email/ # Email service (CF Email)
βββ queue/ # Queue consumer, types, DLQ
βββ schemas/ # Zod validation schemas
βββ templates/ # Theme registry & metadata
βββ types/ # TypeScript type definitions
βββ utils/ # Utility functions
βββ blog/ # Blog post data
βββ config/ # Site config, FAQ, retry policies
worker/
βββ index.ts # Custom worker entry (vinext + Queue + Cron)
__tests__/
βββ unit/ # Unit tests
βββ integration/ # Integration tests
βββ security/ # Security tests (IDOR, rate limits)
βββ setup.ts # Test configuration
Allows anonymous users to upload before authenticating:
1. POST /api/upload β Upload file directly to Worker
2. Worker stores in R2 β Store temp key in localStorage
3. User authenticates β Google OAuth
4. POST /api/resume/claim β Link upload to user, trigger parsing
5. Poll /api/resume/status β Wait for AI parsing (~30-40s)
Before rendering public profiles:
- Phone numbers: Hidden by default
- Addresses: City/State only (full address hidden)
- Email: Public (for contact)
- User controls visibility in settings
Live status updates during AI parsing:
- Endpoint:
wss://your-domain.com/ws/resume-status?resume_id={id} - Technology: Cloudflare Durable Objects (
ClickfolioStatusDO) - Flow: WebSocket connection β DO tracks parsing progress β Real-time status pushed to client
- Authentication: Session token validated before upgrade
- Use case: Waiting room shows live parsing progress instead of polling
Asynchronous resume parsing pipeline:
- Queue:
clickfolio-parse-queue(Cloudflare Queues) - DLQ:
clickfolio-parse-dlqfor failed messages - Producer:
/api/resume/claimenqueues after upload - Consumer:
worker/index.tsprocesses in background - Retry: 3 automatic retries with exponential backoff
- Alerting: Cloudflare Logpush by default, optional Slack/Discord webhook on permanent failures
Four cron triggers run automatically:
| Cron | Time (UTC) | Task |
|---|---|---|
0 2 * * * |
2:00 AM | R2 temp file cleanup (old uploads) |
0 3 * * * |
3:00 AM | Database cleanup (expired sessions) |
0 4 * * * |
4:00 AM | Sync disposable email domain blocklist |
*/15 * * * * |
Every 15 min | Recover orphaned resumes (stuck in parsing) |
All run via worker/index.ts without self-fetch (avoids double billing).
Unlock premium templates by sharing:
- Mechanism: Share your unique referral link from dashboard
- Tracking: Friend signs up β your referral count increases
- Unlocks:
- 3 referrals: DesignFolio, Spotlight templates
- 5 referrals: Midnight template
- 10 referrals: Bold Corporate template
- View: Dashboard shows current count and progress to next unlock
10 built-in templates in components/templates/:
| Template | Category | Description | Unlock Requirement |
|---|---|---|---|
| Minimalist Editorial | Professional | Clean magazine-style layout with serif typography | Free (default) |
| Neo Brutalist | Creative | Bold design with thick borders and loud colors | Free |
| Glass Morphic | Modern | Dark theme with frosted glass effects | Free |
| Bento Grid | Modern | Modern mosaic layout with colorful cards | Free |
| Classic ATS | Professional | Legal brief typography, ATS-optimized single-column layout | Free |
| DevTerminal | Developer | GitHub-inspired dark terminal aesthetic for developers | Free |
| DesignFolio | Creative | Digital brutalism meets Swiss typography with acid lime accents | 3 referrals |
| Spotlight | Creative | Warm creative portfolio with animated sections | 3 referrals |
| Midnight | Modern | Dark minimal with serif headings and gold accents | 5 referrals |
| Bold Corporate | Professional | Executive typography with bold numbered sections | 10 referrals |
All templates receive content (ResumeContent) and user props, respect privacy settings, and are mobile-responsive. Premium templates unlock through the referral program.
- Application-Level Authorization: All data access controlled in code
- Rate Limiting: 5 resume uploads/day per user, plus IP-based limits (10/hour, 50/day) for anonymous uploads
- Input Validation: Zod schemas on all endpoints
- XSS Protection: React's default sanitization
- Encrypted Secrets: All secrets encrypted in Cloudflare
- Privacy Controls: Users control visibility of phone numbers and addresses
- IP Privacy: IP addresses SHA-256 hashed before storage (GDPR-friendly)
To report a vulnerability, open a GitHub issue with the "security" label or contact the maintainers directly via the repository's GitHub page.
Contributions welcome! See AGENTS.md for branch conventions, commit style, and the bun run ci quality gate.
- Fork the repository
- Create a feature branch (
git checkout -b feat/amazing-feature) - Use conventional commits (
feat:,fix:,docs:) - Run quality checks (
bun run ci) - Submit a pull request
bun run type-check # See all errors
bun run build # Fix errors and rebuild- Verify
BETTER_AUTH_URLincludeshttps://for production - Check redirect URIs match in Google Cloud Console
- Clear browser cookies
- Check R2 CORS includes your domain
- Verify R2 bucket binding is configured in
wrangler.jsonc - Confirm bucket name in binding matches actual bucket
- Verify CF AI Gateway config and OpenRouter BYOK setup
- Check PDF isn't corrupted
- Use retry button (max 2 retries)
You're on Cloudflare Workers. Use R2 bindings for file operations.
MIT License - see LICENSE for details.
- vinext - Vite-based Next.js for Cloudflare Workers
- Better Auth - Authentication
- Drizzle ORM - Type-safe database
- Cloudflare - Edge infrastructure
- OpenRouter - AI API gateway
- OpenAI - AI inference
- shadcn/ui - UI components (built on Radix UI + Tailwind CSS)
Built with TypeScript. Deployed on the edge. Designed for speed.