Full-stack portfolio project built with React, TypeScript, Cloudflare Pages, Cloudflare Workers, D1, R2, and Drizzle ORM.
This is not only a static personal site. It includes a public portfolio, a protected backstage admin, database-backed content editing, R2 image management, and Cloudflare-hosted analytics integration in one codebase.
- Full-stack web delivery with React, TypeScript, Cloudflare Workers, D1, R2, and Drizzle ORM
- Database-backed portfolio content with static fallback data for API outages and local development
- Protected BackStage administration for works, skills, self content, users, messages, and settings
- Practical API integration, reusable UI patterns, image upload flow, and Cloudflare deployment workflow
- Build-time SSG for public routes (
/and/works):renderToString+ prerender script injects full HTML and JSON-LD before deploy
Production content is stored in Cloudflare D1 and served through /api/*. Static fallback and local seed data are intentionally kept in sync through:
frontend/src/content/portfolio.ts— bilingual static sections and experience fallbackfrontend/src/content/apiFallback.ts— works and skills fallbackdata-migration/db.json— migration source data for works, skills, self content, and social linksworkers/migrations/0001_seed.sqlandworkers/migrations/0004_experience_rich_content.sql— fresh local database seed state
- Portfolio: https://portfolio.jackhellowin.win/
- API origin in production: same origin under
/api/*
- Public portfolio with dark/light mode and bilingual content
- Build-time prerendered
/and/works— Googlebot Phase-1 sees complete HTML and JSON-LD without executing JavaScript - FrontStage sections for hero, about, experience, works, skills, and contact
- BackStage admin for overview, content, works, skills, users, messages, and settings
- Works image editing flow backed by Cloudflare R2 rather than local asset URLs
- Cloudflare Worker API built with Hono
- D1-backed data for works, skills, self content, users, content blocks, experiences, and messages
- JWT auth with optional environment-admin credentials
- R2 upload/delete API for work images
- Cloudflare Analytics read endpoint for the admin dashboard
portfolio/
├── frontend/ # React + Vite application deployed to Cloudflare Pages
│ ├── src/ # FrontStage, BackStage, hooks, styles, API clients
│ ├── src/entry-server.tsx # Build-time SSR render function
│ ├── scripts/prerender.mjs # Prerender script (runs after vite build)
│ └── public/ # Static assets and Pages routing config
├── workers/ # Cloudflare Worker API
│ ├── src/routes/ # Auth, works, skills, upload, analytics, content, messages
│ ├── src/drizzle/schema/ # D1 schemas
│ └── migrations/ # D1 migrations applied by Wrangler
├── data-migration/ # Historical migration helpers / source data
├── openspec/ # Spectra / OpenSpec artifacts
└── .github/workflows/ # Frontend and Worker deployment workflows
- React 18
- TypeScript
- Vite
- Redux Toolkit
- Tailwind CSS
react-helmet-async(per-page SSR head management)react-router-domStaticRouter (SSR render path inentry-server.tsx)cobeglobe accent
- Cloudflare Workers
- Hono
- Cloudflare D1
- Cloudflare R2
- Drizzle ORM
- JWT-based authentication
- pnpm
- GitHub Actions
- Spectra / OpenSpec
- Node.js 18+
- pnpm 9+
- Cloudflare Wrangler CLI
cd frontend
pnpm install
cd ../workers
pnpm installTerminal 1:
cd workers
pnpm run devTerminal 2:
cd frontend
pnpm run dev --host 127.0.0.1 --port 4173The frontend dev server proxies /api/* to http://127.0.0.1:8787, so local login, works, skills, and admin features only work when the Worker is also running.
Useful local URLs:
- Frontend:
http://127.0.0.1:4173/ - Worker:
http://127.0.0.1:8787/ - Health check:
http://127.0.0.1:8787/health
cd workers
pnpm run db:migrate:localIf you add a new D1 migration later, apply it locally before testing the related UI or API change.
Defined in workers/wrangler.toml:
DB- D1 database bindingIMAGES_BUCKET- R2 bucket binding
Configured as Worker secrets / variables in Cloudflare:
ADMIN_USERNAMEADMIN_PASSWORDCF_ANALYTICS_API_TOKENCF_ZONE_ID
Notes:
ADMIN_USERNAMEandADMIN_PASSWORDprovide the environment-admin login fallback used by/api/auth/login.CF_ANALYTICS_API_TOKENshould be an Analytics read token only.- Do not commit secret values into the repo or README.
CLOUDFLARE_API_TOKENCLOUDFLARE_ACCOUNT_IDVITE_API_PREFIXonly when intentionally overriding the default same-origin API behavior
GET /api/worksGET /api/skillsGET /api/social-mediaGET /api/self-contentGET /api/content-blocksGET /api/experiences
POST /api/auth/loginPOST /api/auth/refreshPOST /api/auth/logoutGET /api/auth/mePOST /api/uploadDELETE /api/upload/:keyGET /api/messagesPATCH /api/messages/:id/readDELETE /api/messages/:idGET /api/admin/analytics
Use this when the change is limited to frontend/**.
Automatic:
- Push to
main .github/workflows/deploy-frontend.ymlbuilds and deploys Cloudflare Pages
The build is a three-step pipeline: client bundle → SSR bundle → prerender script. The SSR bundle (dist/server/) is created and deleted within the same build; only the final prerendered HTML is deployed.
Manual:
cd frontend
pnpm run build
wrangler pages deploy dist --project-name portfolio --branch mainUse this when the change touches workers/**, backend secrets, routes, or D1 schema.
Automatic:
- Push to
main .github/workflows/deploy-workers.ymldeploys the Worker and then applies remote D1 migrations
Manual:
cd workers
wrangler deploy --config wrangler.toml
wrangler d1 migrations apply bruno-portfolio-db --remoteImportant:
- Frontend deployment does not redeploy the Worker.
- Worker deployment does not rebuild Pages.
- If you change schema and code together, deploy the Worker and apply the migration in the same release window.
- If the Cloudflare token can deploy Pages but cannot run D1 migrations, update the token permissions before relying on the Worker workflow.
- Use BackStage
Contentfor bilingual hero/about text. - Content blocks are stored in D1 through
/api/content-blocks.
- Use BackStage content editing for structured experience records.
- Experience rows support:
- date range
- bilingual role and organization
- location
- tags
- bilingual bullet lists
- Works images should live in R2 and be served from
https://images.jackhellowin.win/... - Use the BackStage work editor image preview flow to replace or delete images.
- Do not reintroduce local image URLs for editable work cards unless intentionally changing the storage model.
Before merging a future change:
- Run
pnpm run buildinfrontend/. - Run
pnpm exec tsc --noEmitinworkers/. - If a route or schema changed, verify the matching Worker migration exists.
- If works images changed, confirm the R2 URL returns
200. - If admin auth changed, test both login and protected route behavior.
- If deployment behavior changed, review both GitHub workflows separately.
- Keep Spectra / OpenSpec artifacts in sync with shipped behavior.
- If a new public route is added, add it to
scripts/prerender.mjsroutes array and updatepublic/_redirects.
After deployment:
- Verify
/api/works,/api/skills,/api/content-blocks, and/api/experiences. - Verify BackStage login and one protected endpoint.
- Verify at least one R2 work image loads from the public site.
- Confirm the live site is serving the newest built asset bundle.
- Run
curl https://portfolio.jackhellowin.win/ | grep 'application/ld+json'and confirm non-empty output.
This project uses a spec-driven workflow:
- Discuss or propose the change when requirements are still moving
- Implement against the accepted Spectra / OpenSpec artifacts
- Ingest requirement changes back into the artifacts when scope shifts
- Verify the result
- Archive completed changes after the implementation and docs agree
MIT