Skip to content

Latest commit

Β 

History

843 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

clickfolio.me

clickfolio.me home page: headline, upload button and a live preview of a portfolio design

Turn your PDF resume into a hosted web portfolio in under 60 seconds.

Upload a PDF. AI parses it. Get a shareable link.

License: MIT Cloudflare Workers vinext


Features

  • Instant PDF Parsing - AI extracts your information automatically
  • Clean Public URLs - Get yoursite.com/yourname immediately
  • 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

Tech Stack

Layer Technology
Framework vinext (Vite-based Next.js)
Runtime Cloudflare Workers
Database PlanetScale Postgres via Cloudflare Hyperdrive + Drizzle ORM (node-postgres pg >= 8.16.3)
Auth Clerk (Google OAuth + credentials; prebuilt <SignIn>/<SignUp> UI, JWKS-verified session JWTs)
Storage Cloudflare R2 (S3-compatible)
AI Parsing OpenRouter via Cloudflare AI Gateway (openai/gpt-oss models)
Styling shadcn/ui + Tailwind CSS 4

Why Cloudflare Workers?

We chose Cloudflare Workers over traditional hosting for several reasons:

Performance

  • 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

Cost Efficiency

  • Free Tier: 100,000 requests/day free
  • Hyperdrive: Free connection pooling and query caching for Postgres at the edge
  • R2 Storage: 10GB free, no egress fees
  • Total: A production app can run free for most use cases (plus PlanetScale/Clerk free tiers)

Developer Experience

  • No Container Management: Just deploy code
  • Automatic Scaling: From 0 to millions of requests
  • Integrated Stack: Hyperdrive, R2, Workflows, and Durable Objects work seamlessly together

Trade-offs

  • No fs Module: Must use R2 for file operations
  • No Next.js <Image /> Component: Use <img> with CSS instead
  • No DB in Middleware: The edge proxy only checks cookie presence
  • Bundle Size: Keep dependencies minimal

Quick Start

Prerequisites

Installation

# Clone the repository
git clone https://github.com/divkix/clickfolio.me.git
cd clickfolio.me

# Install dependencies
pnpm install

# Copy environment template and fill in your values
cp .env.example .dev.vars

# Apply database migrations (needs DATABASE_URL from PlanetScale)
DATABASE_URL="postgres://…" pnpm run db:migrate

# Start development server
pnpm run dev

Open http://localhost:3000


Self-Hosting Guide

Beginner-Friendly Deployment (copy/paste)

If 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 PlanetScale account (Postgres database, free tier is fine)
  • A Clerk account (authentication, free tier is fine)
  • An OpenRouter account (for AI parsing)
  • pnpm installed (copy/paste this in Terminal):
    npm install -g pnpm

Step 0: Get the code

  1. Download the repo ZIP from GitHub and unzip it, or use:
    git clone https://github.com/divkix/clickfolio.me.git
    cd clickfolio.me
  2. Install dependencies:
    pnpm install

Step 1: Create the PlanetScale Postgres database

  1. Create a Postgres database in the PlanetScale console (e.g. clickfolio).
  2. Copy the direct connection string (PlanetScale console β†’ your database β†’ Connect β†’ Postgres URL).
  3. You will use it as DATABASE_URL below. Note: drizzle-kit uses this DIRECT URL; the deployed Worker connects through Cloudflare Hyperdrive instead.

Step 2: Create the Cloudflare Hyperdrive binding

  1. In Terminal:
    pnpm exec wrangler hyperdrive create clickfolio-pg --connection-string="postgres://user:password@host/db"
  2. Copy the printed Hyperdrive id.
  3. Open wrangler.jsonc and put that id under hyperdrive[0].id.

Step 3: Create Cloudflare R2 bucket

  1. Go to Cloudflare Dashboard β†’ R2 β†’ Create bucket.
  2. Name it clickfolio-bucket.
  3. The bucket is accessed via binding in wrangler.jsonc - no API tokens needed.

Step 4: 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 5: Set up Clerk

  1. Create an application at clerk.com β†’ copy the Publishable key (pk_…) and Secret key (sk_…).
  2. Enable the Google social connection in Clerk's dashboard (Clerk hosts the OAuth app β€” no separate Google Cloud project needed).
  3. Create a webhook in Clerk β†’ Webhooks pointing to https://your-domain.com/api/webhooks/clerk, subscribing to user.created, user.updated, user.deleted. Copy the signing secret (whsec_…).

Step 6: Set up OpenRouter

  1. Create OpenRouter account β†’ API Keys.
  2. Copy your API key.

Step 7: Add secrets to Cloudflare (production) Run each command and paste the value when prompted:

pnpm exec wrangler secret put CLERK_SECRET_KEY                 # sk_… from Clerk
pnpm exec wrangler secret put CLERK_WEBHOOK_SECRET             # whsec_… from Clerk webhook
pnpm exec wrangler secret put NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY # pk_… from Clerk
pnpm exec wrangler secret put APP_URL                          # https://your-domain.com
openssl rand -base64 32                                        # then:
pnpm exec wrangler secret put PENDING_UPLOAD_SECRET            # random value from openssl above
pnpm exec wrangler secret put CF_AI_GATEWAY_ACCOUNT_ID
pnpm exec wrangler secret put CF_AI_GATEWAY_ID
pnpm exec wrangler secret put CF_AIG_AUTH_TOKEN

Step 8: Deploy

DATABASE_URL="postgres://…" pnpm run deploy   # build β†’ db:migrate β†’ R2 lifecycle β†’ wrangler deploy

Step 9: Add your domain Cloudflare Dashboard β†’ Workers & Pages β†’ your worker β†’ Settings β†’ Domains & Routes.

Important: After domain is connected, update these secrets and redeploy:

  • APP_URL = https://your-domain.com
  • Point the Clerk webhook endpoint URL at https://your-domain.com/api/webhooks/clerk

Then redeploy:

pnpm run deploy

If you followed the steps above, the site should be live at your domain.

Step 1: Cloudflare Setup

  1. Create a Cloudflare account at cloudflare.com

  2. Create the Hyperdrive binding over your PlanetScale Postgres:

    pnpm exec wrangler hyperdrive create clickfolio-pg --connection-string="postgres://…"

    Copy the id to wrangler.jsonc

  3. 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
  4. 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
      }
    ]

Step 2: Clerk Setup

  1. Create an application at clerk.com
  2. Copy the Publishable key (pk_…) and Secret key (sk_…)
  3. Enable Google sign-in in Clerk's dashboard (Clerk manages the OAuth app)
  4. Add a webhook endpoint https://your-domain.com/api/webhooks/clerk subscribed to user.created, user.updated, user.deleted; copy its signing secret (whsec_…)

Step 3: OpenRouter + Cloudflare AI Gateway (required)

  1. Create account at openrouter.ai
  2. Go to API Keys
  3. Create new API key and copy it
  4. Get your OpenRouter HTTP Referer and App Title from the dashboard

Cloudflare AI Gateway This project uses Cloudflare AI Gateway for AI calls.

  1. Go to Cloudflare Dashboard > AI > AI Gateway
  2. Create a gateway
  3. Store your OpenRouter token in Cloudflare Secrets Store
  4. You will use CF_AI_GATEWAY_* environment variables

Step 4: Environment Variables

Create .dev.vars for development:

# Generate a secure secret with: openssl rand -base64 32

APP_URL=http://localhost:3000
PENDING_UPLOAD_SECRET=your-generated-secret

NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_…
CLERK_SECRET_KEY=sk_test_…
CLERK_WEBHOOK_SECRET=whsec_…

# Direct PlanetScale URL β€” used ONLY by drizzle-kit locally (db:migrate/push/studio).
# The dev Worker itself connects through the local Hyperdrive simulation.
DATABASE_URL=postgres://user:password@host/clickfolio

# 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.

Step 5: Deploy to Cloudflare

  1. Apply database migrations

    DATABASE_URL="postgres://…" pnpm run db:migrate
  2. Set production secrets (see Step 7 of the beginner guide above)

  3. Deploy

    pnpm run deploy
  4. Configure custom domain (optional)

    • In Cloudflare Dashboard > Workers & Pages > Your Worker
    • Add custom domain in Settings > Domains & Routes

Development

Available Scripts

# Development
pnpm run dev              # Start dev server at localhost:3000
pnpm run lint             # Oxlint linting (via vp lint)
pnpm run fix              # Oxlint + Oxfmt auto-fix (via vp check --fix)
pnpm run type-check       # TypeScript check

# Build & Deploy
pnpm run build            # Vite production build (vinext)
pnpm run preview          # Local Cloudflare preview
pnpm run deploy           # scripts/deploy.ts: build, db:migrate, R2 lifecycle, wrangler deploy, IndexNow (needs DATABASE_URL)

# Database (PlanetScale Postgres via drizzle-kit; needs DATABASE_URL except generate)
pnpm run db:generate      # Generate migration files into migrations_pg/
pnpm run db:migrate       # Apply migrations_pg/ to DATABASE_URL
pnpm run db:push          # Sync schema without migration files (prototyping only)
pnpm run db:studio        # Drizzle Studio UI (port 4984)

# Testing
pnpm run test             # All tests
pnpm run test:unit        # Unit tests (fast, no retries)
pnpm run test:integration # Integration tests
pnpm run test:security    # Security tests
pnpm run test:coverage    # All tests + coverage
pnpm run test:ci          # CI mode (JSON reporter)
pnpm run test:ui          # Interactive UI mode

# Quality
pnpm run verify           # check + type-check + knip
pnpm run format           # Format with Oxfmt
pnpm run seo:lastmod      # Update SEO last-modified timestamps
pnpm run ci               # type-check + lint + test + build

Project Structure

app/
β”œβ”€β”€ api/                 # API routes (webhooks/clerk, upload, resume, etc.)
β”œβ”€β”€ (admin)/             # Admin dashboard pages
β”‚   β”œβ”€β”€ users/       # User management
β”‚   β”œβ”€β”€ resumes/     # Resume management
β”‚   └── analytics/   # Site analytics
β”œβ”€β”€ (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
β”œβ”€β”€ [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 (ClerkProvider wrapper)
└── globals.css          # Global styles

components/
β”œβ”€β”€ templates/           # 14 resume template components
β”œβ”€β”€ ui/                  # shadcn/ui components
β”œβ”€β”€ auth/                # LoginButton using Clerk's native sign-in modal
β”œβ”€β”€ dashboard/           # Dashboard-specific components
β”œβ”€β”€ icons/               # Custom icon components
β”œβ”€β”€ analytics/           # Analytics components
└── *.tsx                # Shared components (Footer, Logo, etc.)

lib/
β”œβ”€β”€ auth/                # Clerk integration (server JWKS verification, session, client seam)
β”œβ”€β”€ ai/                  # AI parsing (OpenRouter via CF AI Gateway)
β”œβ”€β”€ cron/                # Daily DB cleanup cron
β”œβ”€β”€ db/                  # Drizzle PG schema + getDb(env.HYPERDRIVE)
β”œβ”€β”€ durable-objects/     # WebSocket Durable Object
β”œβ”€β”€ parse/               # Parse step bodies, error classification, alerts, DO notify
β”œβ”€β”€ workflows/           # ResumeParseWorkflow + R2DeleteWorkflow and their triggers
β”œβ”€β”€ 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 + Workflows + Cron + WebSocket auth)

migrations_pg/
└── *.sql                # Postgres migrations (drizzle-kit)

tests/
β”œβ”€β”€ unit/                # Unit tests
β”œβ”€β”€ integration/         # Integration tests
β”œβ”€β”€ security/            # Security tests (IDOR, rate limits)
└── setup.ts             # Test configuration

Architecture

The Claim Check Pattern

Allows anonymous users to upload before authenticating:

1. POST /api/upload         β†’ Upload file directly to Worker
2. Worker stores in R2      β†’ Signed pending_upload cookie (HMAC'd with PENDING_UPLOAD_SECRET)
3. User authenticates       β†’ Clerk (Google OAuth or credentials)
4. POST /api/resume/claim   β†’ Link upload to user, trigger parsing
5. Poll /api/resume/status  β†’ Wait for AI parsing (~30-40s)

Privacy Filtering

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

Real-time Updates (WebSocket)

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: Clerk session JWT verified against JWKS before upgrade
  • Use case: Waiting room shows live parsing progress instead of polling

Parse Pipeline (Cloudflare Workflows)

Durable resume parsing with per-step retries:

  • Workflow: ResumeParseWorkflow (lib/workflows/resume-parse-workflow.ts) runs claim β†’ parse β†’ complete
  • Trigger: /api/resume/claim and /api/resume/retry start an instance keyed by the resume id, so a repeated start is a no-op
  • Retries: the parse step retries transient failures 3 times with exponential backoff; permanent errors throw NonRetryableError
  • Failure: once retries are spent the workflow marks the row failed, notifies the Durable Object, and sends an alert
  • Duplicate uploads: an await-cache instance sleeps 10 minutes, then expires a waiting_for_cache row that never resolved
  • Alerting: Cloudflare Logpush by default, optional Slack/Discord webhook on permanent failures

R2 Cleanup

  • Anonymous uploads: an R2 lifecycle rule (r2-lifecycle.json, applied by pnpm run deploy) deletes temp/ objects after 1 day
  • Deletions: R2DeleteWorkflow retries failed deletes (account deletion, admin dismiss) with backoff

Scheduled Tasks (Cron)

Cron Time (UTC) Task
0 3 * * * 3:00 AM Database cleanup (expired rate limits, handle history)

Runs via worker/index.ts without self-fetch (avoids double billing).

Referral Program

Removed β€” all 14 templates are now free for every user. No referral gating.


Resume Templates

14 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 Free
Spotlight Creative Warm creative portfolio with animated sections Free
Midnight Modern Dark minimal with serif headings and gold accents Free
Boardroom Professional Dark executive ledger with a pinned identity column, brass accents Free
Bold Corporate Professional Executive typography with bold numbered sections Free
Broadsheet Professional Newspaper front page: masthead name, ruled columns, roles as stories Free
Case File Professional Typed dossier in a manila folder, sections filed as exhibits Free
Retro OS Creative Late-90s desktop, every section in its own bevelled window Free

All templates receive content (ResumeContent) and profile props, respect privacy settings, and are mobile-responsive.


Security

  • 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; Clerk session JWTs verified against JWKS on every server request
  • Webhook Signatures: Clerk webhooks are Svix-signature verified before processing
  • 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.


Contributing

Contributions welcome! See AGENTS.md for branch conventions, commit style, and the pnpm run ci quality gate.

Quick Contribution Guide

  1. Fork the repository
  2. Create a feature branch (git checkout -b feat/amazing-feature)
  3. Use conventional commits (feat:, fix:, docs:)
  4. Run quality checks (pnpm run ci)
  5. Submit a pull request

Troubleshooting

Build Fails with TypeScript Errors

pnpm run type-check  # See all errors
pnpm run build       # Fix errors and rebuild

Auth Redirect Issues / Users Missing After Sign-Up

  1. Verify CLERK_SECRET_KEY / NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY match the same Clerk application
  2. Confirm the Clerk webhook points at /api/webhooks/clerk and CLERK_WEBHOOK_SECRET matches β€” a signed-in user with no synced row gets 404s until the webhook lands
  3. Clear browser cookies

Database Connection Failures

  1. Verify the Hyperdrive binding id in wrangler.jsonc matches wrangler hyperdrive create output
  2. For db:* scripts, confirm DATABASE_URL is set to the DIRECT PlanetScale connection string
  3. Check PlanetScale console β†’ your database is awake and credentials are valid

R2 Upload Fails

  1. Check R2 CORS includes your domain
  2. Verify R2 bucket binding is configured in wrangler.jsonc
  3. Confirm bucket name in binding matches actual bucket

Parsing Stuck in "Processing"

  1. Verify CF AI Gateway config and OpenRouter BYOK setup
  2. Check PDF isn't corrupted
  3. Use retry button (max 2 retries)

"Cannot find module 'fs'"

You're on Cloudflare Workers. Use R2 bindings for file operations.


License

MIT License - see LICENSE for details.


Acknowledgments


Built with TypeScript. Deployed on the edge. Designed for speed.

About

Your resume, reimagined πŸ“

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages