DARABOTH
All posts

Jul 5, 202611 min read4 reads

How I turned my portfolio into a 3D solar system (and how you can too)

The full build log of Daraboth Universe: Next.js + React Three Fiber + Supabase + MCP, every architecture trick, every bug scar, and a one-shot prompt you can paste into an AI agent to build your own.

This website is not a website. It's a solar system. Every part of my life is a planet: Origin (my story), Forge (projects), Signal (blog), Chronicle (journal), and Nexus (my AI agents). You scroll to travel between worlds, click a planet to warp through a gate tunnel, and land standing on its 3D surface under its own aurora.

This post is the full build log — the architecture, the exact tools, the mistakes, and at the end a one-shot prompt you can paste into an AI coding agent to build your own universe. No secrets included; everything here is safe to copy.

The stack

  • Next.js (App Router) + TypeScript — server components for content, client islands for 3D
  • Tailwind CSS v4 — design tokens for the dark cosmic theme
  • React Three Fiber + drei + Three.js — the solar system, planet surfaces, auroras
  • Motion (framer-motion's successor) — scroll-driven voyage, useScroll + useTransform
  • Supabase — Postgres + Storage + Auth for posts, journal, projects, and API tokens
  • pgvector — embeddings so AI agents can semantically search my content
  • MCP (Model Context Protocol) — my site is an MCP server; AI agents write posts here
  • pnpm — package manager
  • Vercel — hosting

The architecture that makes it work

1. One planet registry rules everything

A single file, src/lib/planets.ts, defines every world: name, realm, route, orbit radius, size, speed, and three colors — surface color, atmosphere glow, and aurora (every planet's aurora is different). The 3D scene, the warp transition, the nav, and the per-page atmosphere all read from this one array. Add a planet to the array and it exists everywhere.

2. Scrolling is traveling

The home page is one sticky full-viewport canvas with the scroll track flowing over it. Scroll progress maps to a camera flight path: the first 10% is the hero overview, then each fifth of the remaining scroll is one "leg" from planet to planet. The trick that keeps it smooth: pass the scroll progress into the 3D scene as a getter function (voyage={() => scrollYProgress.get()}) and read it inside useFrame — never React state, which would re-render the tree 60 times a second.

Each leg spends 70% of its scroll flying to the world and the last 30% diving into its atmosphere — the camera closes in until the planet fills the sky while a full-screen tint in that planet's colors washes over the viewport. Content panels ("stations") float over the canvas with pointer-events: none on the layer and pointer-events: auto only on the cards, so clicks pass through to the planets.

3. Landing pages are 3D surfaces

Every world's page opens with a PlanetArrival scene: a huge sphere below the camera forms the curved horizon, tinted sky and fog from the registry colors, floating crystal shards, and swaying aurora curtains in that planet's unique aurora color. One component, five different worlds, because everything derives from the registry.

4. The nav is a spaceship console

No header. A floating glass dock (top on desktop, bottom on mobile — thumb reach) shows Sol plus each planet as a glowing orb in orbit order. Clicking one plays the warp and routes you there.

5. The stargate transition

Traveling between worlds drives you through a corridor of glowing screen-shaped frames that rush at the camera, each growing until its edges pass beyond the viewport, alternating the destination's glow and aurora colors. It's pure CSS keyframes — six divs and two animations — no WebGL needed. Navigation fires at 950ms, and a watchdog hard-navigates at 2.8s if the soft route ever stalls, so travel can never freeze.

6. The site is an MCP server

/api/mcp is a stateless Streamable HTTP MCP server. AI agents authenticate with a bearer token (minted in my admin dashboard, stored as a SHA-256 hash — the plaintext is shown exactly once) and get tools: create_post, update_post, create_journal_entry, create_project, upload_image, search_content, and more. Posts default to draft so an agent can't accidentally publish. This very post was created through that gateway.

Embeddings sync fire-and-forget on every content save, so search_content does semantic search over everything I've written.

Step by step, in build order

  1. Scaffold: pnpm create next-app with TypeScript + Tailwind. Define the dark cosmic tokens (deep space background #05060e, star/fog text colors, one accent).
  2. Content backend: Supabase project, tables for posts, journal_entries, projects, api_tokens. Add a mock-data fallback so the site runs with zero env vars.
  3. Planet registry: the single source of truth file. Do this before any 3D.
  4. Solar system scene: R3F canvas — star shell, glowing sun, orbit rings, planets with hover glow and HTML labels. All motion via refs + useFrame; positions computed deterministically from time (angle = phase + t * speed), never integrated.
  5. Scroll voyage: sticky canvas + scroll track, camera legs between vantage points, atmosphere dive, station panels.
  6. Planet surfaces: the arrival scene component, mounted at the top of each world's page.
  7. Auroras: per-planet aurora color in the registry; torus light-rings in the system view, sprite curtains on the surfaces.
  8. Spaceship console nav + stargate warp transition.
  9. Admin dashboard: auth-gated CRUD for all content, markdown editor with image/video/YouTube embeds, browser-side image compression before upload (huge phone photos will time out Next's image optimizer otherwise).
  10. MCP gateway: token minting UI, the JSON-RPC route, tool catalog with zod schemas.
  11. Vector search: pgvector migration, embedding sync on save, search_content tool.
  12. Performance passes: dpr={1} and fewer particles on mobile, reduced-motion fallbacks for every scene (2D star map, gradient skies).

The tools I actually used to build it

I built this almost entirely with Claude Code (Anthropic's agentic CLI) driving the whole codebase. The specific setup, all installable:

  • Claude Codenpm install -g @anthropic-ai/claude-code, then run claude in the repo. It reads, edits, runs the type checker, and commits.
  • pnpmnpm install -g pnpm (I switched from yarn mid-project; corepack was broken on my Node).
  • Playwright screenshots as the agent's eyes — the agent verified every 3D change visually against the dev server: pnpm dlx playwright screenshot --viewport-size="1440,900" --wait-for-timeout=12000 http://localhost:3000 shot.png (run pnpm dlx playwright install chromium-headless-shell once first). For a WebGL site this is non-negotiable: types passing means nothing if the planets render behind the headline.
  • A design-taste Skill — this is the secret ingredient for the visual quality. Claude Code supports Skills: markdown instruction files in ~/.claude/skills/<name>/SKILL.md that steer how the agent works. I use an "anti-slop frontend" skill (mine is called design-taste-frontend) that bans the generic AI look — no default purple gradients, no three-equal-feature-cards, no Inter + centered hero — and forces deliberate choices through three dials: DESIGN_VARIANCE / MOTION_INTENSITY / VISUAL_DENSITY. This site runs at variance 8, motion 8, density 3: asymmetric layouts, cinematic motion, airy spacing. It also enforces the practical stuff — Motion over useState for continuous values, min-h-[100dvh] over h-screen, one icon family (@phosphor-icons/react), full loading/empty/error states, WCAG-contrast CTAs. Search GitHub for "claude code design skill" or "anti-slop frontend skill" for public versions, or write your own: any SKILL.md describing your taste rules works.
  • The site's own MCP server — connected back into Claude Code so the agent can publish content: claude mcp add --transport http daraboth-universe https://www.vongpichdaraboth.com/api/mcp --header "Authorization: Bearer <your-token>". Swap in your own domain and token.
  • Supabase CLI / SQL editor — for running the migrations in supabase/.

Secrets live in .env only — NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, NEXT_PUBLIC_SITE_URL, and an embeddings key (GEMINI_API_KEY or OPENAI_API_KEY). Never hardcode them, never commit them, and never let an agent print them. A .env.example with names only keeps the setup documented.

Scars: the bugs worth knowing about

  • Never run next build while next dev is running. They share .next and will corrupt each other's cache (mine took 98 seconds to "compact" before I learned this). Verify with tsc --noEmit + screenshots instead.
  • www vs apex domain broke MCP auth. The apex 308-redirects to www, and HTTP clients drop the Authorization header on cross-host redirects — every authenticated call became a 401. Point clients at the exact canonical host.
  • Full-screen overlays eat clicks. The scroll track sits above the canvas; without pointer-events-none, no planet was clickable.
  • Transparent canvases show what's behind them. An opaque <color attach="background"> fixed ghost geometry bleeding through the solar system.
  • Compress images in the browser before upload (canvas → WebP, max 1920px). Phone photos are 10MB+ and will time out your image optimizer.

The one-shot prompt

Paste this into Claude Code (or any capable AI coding agent) in an empty folder. It contains no secrets — you supply your own services.

Build me a personal "universe" portfolio website where every section of my life is a planet in an interactive 3D solar system.

STACK: Next.js App Router + TypeScript, Tailwind CSS v4, React Three Fiber + drei + three, Motion (motion/react) for scroll animation, Supabase for content (with a mock-data fallback so the site runs without env vars), pnpm.

DESIGN: dark cosmic theme (near-black blue background #05060e, soft starlight text, one aurora accent). Cinematic, immersive, world-class polish — high layout variance (asymmetric, no centered-hero-plus-three-cards template), high motion intensity, low visual density (airy, gallery-like spacing). Avoid the generic AI look: no default purple gradients, no Inter-by-default, one icon family only, design real loading/empty/error states, WCAG-readable buttons. Respect prefers-reduced-motion everywhere with 2D fallbacks.

CORE ARCHITECTURE — build these in order:

1. A planet registry file (src/lib/planets.ts): one array defining each world — id, name, realm, route, orbit radius, planet size, orbit speed, phase angle, and three colors: surface color, atmosphere glow, and a unique aurora color per planet. My worlds: Origin (/about, my story), Forge (/projects), Signal (/blog), Chronicle (/journal), Nexus (/agents). Everything else must derive from this file.

2. Home page = the solar system. R3F canvas: star shell, glowing sun with pulsing sprite, orbit rings, planets orbiting deterministically (angle = phase + time * speed, computed in useFrame via refs — never React state per frame). Hover wakes a planet's glow; each planet has an HTML label. Every planet also gets two slowly rotating aurora torus rings in its aurora color.

3. Scroll voyage: the canvas is sticky and full-viewport; the page scroll flies the camera from planet to planet. Reserve the first 10% of scroll for a hero overview, then one leg per planet. Each leg: 70% flying to the world, 30% diving into its atmosphere (camera closes in, planet fills the sky, and a full-screen CSS tint in the planet's glow + aurora colors fades over the viewport). Pass scroll progress into the scene as a getter function read inside useFrame. Per-planet content panels ("stations") float over the canvas — pointer-events-none on the layer, pointer-events-auto on the cards, so planets stay clickable.

4. Planet arrival pages: each world's page opens with a full-viewport 3D "standing on the surface" scene — a huge sphere below the camera as the curved horizon in the planet's color, tinted sky + fog, floating crystal shards, and tall swaying aurora curtains in the planet's aurora color. One shared component driven by the registry. Page headline floats in the sky with a "Touchdown · Planet X" kicker.

5. Navigation = spaceship console: no header. A floating glass pill dock (top on desktop, bottom-docked on mobile) with one glowing orb per world in orbit order plus a home "Sol" orb and a contact button. Active world highlighted in its colors.

6. Warp transition: clicking a planet (in the scene or the dock) plays a stargate tunnel — ~6 screen-shaped glowing frames born tiny at screen center that rush at the viewer, each scaling until its edges pass beyond the viewport, alternating the destination's glow and aurora colors, ending with the destination's light filling the screen. Pure CSS keyframes. Route at ~950ms; add a watchdog that hard-navigates via window.location.assign after 2.8s if the route didn't change, so travel can never freeze.

7. Content: Supabase tables for posts, journal entries, and projects, each with status (draft/published) and slug. Blog supports markdown with images, YouTube, and video embeds. Auth-gated /admin dashboard with mobile-first forms for all content. Compress images in the browser before upload (canvas → WebP, max 1920px, quality 0.82).

8. MCP gateway: a stateless Streamable HTTP MCP server at /api/mcp (JSON-RPC over POST). Bearer token auth: tokens minted in the admin dashboard, stored as SHA-256 hashes, shown once. Tools: create_post, update_post, list_posts, create_journal_entry, create_project, upload_image — all validated with zod, posts defaulting to draft. Return proper 401 with WWW-Authenticate, handle initialize/tools/list/tools/call, and add CORS headers + OPTIONS.

9. Performance: dpr 1 and reduced particle counts on mobile, opaque canvas backgrounds, all env vars read from .env (create .env.example with names only, never values).

VERIFY AS YOU GO: run the type checker after every change, and take Playwright screenshots of the running dev server (desktop and mobile viewports) to visually confirm every 3D scene — do not trust types alone. Never run a production build while the dev server is running.

Set up your own Supabase project, fill .env from .env.example, and let the agent run. Mine took a few days of iterating — the prompt above front-loads everything I learned so yours should take less.

What's next

Per-project 3D tours inside each planet (fly past your work as floating monuments), environmental storytelling on Origin, and sound design per world. The universe keeps expanding.

If you build your own universe with this, I'd genuinely love to see it — my contact channel is on the About page.

  • nextjs
  • react-three-fiber
  • threejs
  • mcp
  • claude-code
  • supabase
  • portfolio
  • ai-agents