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) |
| 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 .env.local
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 # Biome linting
bun run fix # Biome auto-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
# Quality
bun run ci # type-check + lint + 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: Optional Slack/Discord/email 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)
See SECURITY.md for detailed security policy, rate limiting details, and vulnerability reporting.
Contributions welcome! Please read CONTRIBUTING.md for guidelines.
- 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.