Skip to content
Tulio Cunha Developer notes · reading view
Back to all posts

Building a Terminal Portfolio Web App

Why I built a keyboard-first, terminal-style portfolio; how the React + Elysia (Bun) + TanStack + SQLite stack fits the problem.

Screenshot of the live app at portfolio.tuliocunha.dev

Context

I spend most of my working day inside a terminal. That environment shapes how I think: short commands, clear responses, minimal visual noise. I wanted my portfolio to follow the same logic — simple verbs, predictable output, keyboard-first navigation.

A secondary goal was visual continuity. I use Ghostty with a very specific palette and spacing, so I mirrored that aesthetic: high-contrast prompts, a crisp cursor, tight line height, and restrained animations.

Goals and Constraints

  • Discoverable interaction — A small command set with readable output and sensible aliases.
  • Keyboard-first — Immediate focus, history, and quick clearing (clear or Ctrl+L).
  • Low-overhead content changes — I should be able to edit command content without touching React.
  • Straightforward deploys — Single container image, health checks, quick rollouts.

What You Can Type

The app exposes a compact set of commands. Aliases keep it approachable:

contact

whoami or info — About me and what I do

Work

  • ls or projects — Browse recent projects
  • grep or skills — View technical skills

Connect

  • gh or github — Open my GitHub
  • contact — Email & booking link
  • open — Visit my website

Terminal

  • clear — Clear the terminal
  • Ctrl+L — Keyboard shortcut to clear

If you never type a command, the page still reads like a portfolio: the right pane includes a Fastfetch-style system block, a world clock (Rio, San Francisco, Tokyo), and a Recent Projects list pulled from GitHub.

Architecture Overview

[Browser / React]
├─ Terminal UI (prompt, history, renderer)
├─ Side panes (Fastfetch, world clock, projects)
├─ TanStack Router (routes)
└─ TanStack Query (data fetching)

[Elysia (Bun) API]
├─ /api/commands → SQLite-backed content
├─ /api/projects → GitHub fetch + caching
├─ /api/worldclock → Timezones
└─ /healthz → Health check

[SQLite (bundled)]
└─ commands.db (name, aliases, type, payload/endpoint)

Why This Stack Fits

  • React — The input → parse → render cycle maps naturally to React. Rendering semantic blocks (text, lists, links) is ergonomic, and accessibility (aria-live) is straightforward.
  • TanStack Router/Query — Keeps the app minimal. Query handles caching, retries, and loading states for clocks and projects.
  • Elysia (Bun) — A fast, compact server. Endpoints stay terse.
  • SQLite (bundled) — Commands are treated as content, not code. I can change copy or aliases without a React deploy.
  • Cloudflare Containers — One image, health-checked, quick to roll out. Ideal for a portfolio-scale app.

Commands as Content (SQLite)

Commands live in a tiny SQLite file included in the container. Each row defines a command:

  • name — primary keyword
  • aliases — comma-separated list
  • type — static (renders markdown) or remote (API call)
  • payload — markdown for static output
  • endpoint — path for remote commands
  • updated_at — housekeeping

Editing Workflow

  1. Open commands.db in any SQLite client.
  2. Update payload, add aliases, or flip a command to remote.
  3. Build and deploy a new image.

I considered a headless CMS, but at this scale, it added more friction than value. A local DB keeps content close to the code while staying decoupled.

Data Endpoints

The API surface is intentionally small:

  • GET /api/commands — List all command definitions
  • GET /api/commands/:name — Resolve one command
  • GET /api/projects — GitHub repos, trimmed and cached
  • GET /api/worldclock — Formatted timezone data
  • GET /healthz — Health check

On the client, TanStack Query handles caching and retries, keeping the UI responsive even on spotty networks.

UI Details

  • Terminal — Input focus, command history, semantic output renderer. Uses aria-live="polite" for accessibility.
  • Fastfetch Mock — Personality via fake system specs (host, OS, CPU, RAM).
  • World Clock — Time zones I actively collaborate across.
  • Recent Projects — GitHub repos with name, description, language.

Styling mirrors my Ghostty palette for consistency.

Deployment

The app ships as a single container image:

  • Build — Includes commands.db and Elysia server
  • Health checks — /healthz endpoint
  • Rollouts — Push image, update service, rely on health gates

For a personal site, it’s fast, cheap, and predictable.

Trade-offs & Lessons

  • SQLite vs CMS — SQLite is faster to iterate but requires rebuilds. Acceptable for my cadence.
  • Keyboard-first UX — Great for technical users; still readable for others. Command set must stay small.
  • Single image — Easy to reason about. If needed, UI/API split is simple later.

Roadmap

  • Command help overlay + fuzzy search
  • Theme customization for non-Ghostty users
  • Optional sound/visual feedback (accessible)

Open Source & Live Site

The project is live and open source:

If you adapt it, I’d love to see your variations.