# Codefolio: I shipped a GitHub-sync portfolio SaaS with 4k lines of specs — and claimed a domain that isn't mine

Source: https://www.michaelchurley.com/blog/codefolio-spec-first-github-portfolio-saas  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: codefolio, developer-portfolio, github-sync, nextjs, convex, clerk, stripe, saas, proxy-ts, resume, analytics, michaelmonetized, spec-first

![Codefolio marketing hero: Public Beta + Get started free](/blog/codefolio-spec-first-github-portfolio-saas/screenshots/marketing-hero.png)

## Who

I wanted a **developer portfolio platform** (multi-tenant, GitHub sync, pin six projects on free, custom domain on Pro, analytics that tell you whether recruiters came from Twitter or a blog referral) instead of another personal homepage.

Who this is for: operators who will read `DESIGN.md` and `CONTRACTS.md` before they trust a launch tweet, and people who have watched a SaaS invent 10K+ developers before the Vercel project exists.

If you have ever claimed a `.dev` domain in `openGraph.url` and then discovered the hostname already belongs to someone else's portfolio, this brief is for you.

## What

I built **Codefolio**, package `codefolio` **0.1.0**, **private** under **michaelmonetized/codefolio**. Layout title: **Codefolio - Developer Portfolio Platform**. Description: Showcase your code. Build your reputation. The modern portfolio platform for developers.

Stack from package.json: **Next.js 16.2.6**, **React 19.2.6**, Tailwind **4.3**, Bun, **Clerk** (`@clerk/nextjs` ^7.3.3), **Convex** ^1.38.0, `stripe` + `@stripe/stripe-js`, `resend`, `posthog-js`, `@sentry/nextjs`, radix-ui, next-themes (dark default). Three commits. HEAD `c499a72`.

![Free / Pro $9 / Team $29 pricing grid](/blog/codefolio-spec-first-github-portfolio-saas/screenshots/pricing-tiers.png)

Surfaces that exist in the tree:

- Marketing: `/`, `/features`, `/pricing`, `/about`, `/blog`, `/careers`, `/examples`, `/privacy`, `/terms`, `/login`, `/signup`
- Public portfolio: `/:username`, `/:username/:project`, `/:username/resume`
- Dashboard (Clerk-protected via `proxy.ts`): `/dashboard`, `/dashboard/projects`, `/dashboard/projects/[id]`, `/dashboard/analytics`, `/dashboard/settings`

Convex schema is five tables: `profiles`, `projects`, `analytics`, `subscriptions`, `githubSyncs`. GitHub sync action pulls `api.github.com/users/{username}/repos?per_page=100&sort=updated&type=owner` and upserts. Plan limits in `lib/types.ts`: Free 6 pins / 7-day analytics; Pro unlimited + custom domain + 90 days + case studies + remove branding; Team 5 members + 365-day analytics.

Pricing cards match: **Free**, **Pro $9/mo**, **Team $29/mo**.

What does **not** exist: `app/api/**` (no Stripe webhook route handlers), a `/demo` page (Hero still links there), a live deploy on the probed Vercel hostnames, or ownership of **codefolio.dev**.

Docs are not an afterthought. `DESIGN.md` (~1083 lines), `CONTRACTS.md` (~466), `COMPLIANCE.md` (~1171 WCAG 2.1 AA), `TECH-REQ.md` (~429), plus Fallow `REVIEW.md` (~9.2k LOC, 6 unused deps including stripe/resend/posthog, dashboard pages marked critical complexity).

Hero badge: **Now in Public Beta**. Social proof strip: **10K+ Developers / 50K+ Projects Showcased / 1M+ Portfolio Views**. Those numbers are marketing copy with no telemetry backing in the private tree.

## Where

Code: [github.com/michaelmonetized/codefolio](https://github.com/michaelmonetized/codefolio), **private**. Live app URL: **none** at pack time (`codefolio.vercel.app` returns 404).

![Five Convex tables vs Stripe/Resend unused deps](/blog/codefolio-spec-first-github-portfolio-saas/screenshots/schema-five-tables.png)

Layout `openGraph.url` and feature copy talk about **codefolio.dev**. Probe on 2026-09-08: `https://www.codefolio.dev` returns **HTTP 200** for Abdel Ahzab, Full-Stack Engineer shipping Applied AI: unrelated personal site on Cloudflare/Vercel. That is a **name collision**, not my deploy.

![codefolio.dev claimed in OG vs live third-party portfolio](/blog/codefolio-spec-first-github-portfolio-saas/screenshots/domain-collision.png)

Audience sits next to every portfolio SaaS that ships specs + dashboard chrome before billing webhooks and a domain you actually control.

## When

**2026-02-07.** `531f712` Create Next App. Bootstrap wrap-up in `.work/` claims Convex project `codefolio`, schema complete (5 tables / 16 indexes), CONTRACTS.md, shadcn button/card/input, build passes.

**2026-06-22 ~5:44 PM ET.** `01aa6cc` **nightly**. The product and the essay-length docs land together: marketing, dashboard, public portfolio/resume, Fallow gate hooks, DESIGN/COMPLIANCE/TECH-REQ/REVIEW.

**2026-06-22 ~6:19 PM ET.** `c499a72` **nightly** HEAD.

GitHub `created_at` / `pushed_at` both sit on **2026-06-22** even though the first commit is February (private repo timing vs local history).

![Three-commit arc Feb to June nightlies](/blog/codefolio-spec-first-github-portfolio-saas/screenshots/commit-arc.png)

## Why

A portfolio platform is a different product from a personal site, and I wanted the contracts written before the launch thread. Clerk + Convex + a real `/:username` surface is useful even when Stripe checkout is still schema fiction. Claiming `codefolio.dev` in metadata while the hostname serves another engineer is the kind of fact you put in the brief before you buy ads. Three commits can still carry nine thousand lines, and Fallow will still tell you stripe and resend never got imported.

**Engagement Q:** When your OG URL names a `.dev` you do not control and your Hero invents 10K users, do you fix the domain story first, or the fake social proof?
