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
- Open commands.db in any SQLite client.
- Update payload, add aliases, or flip a command to remote.
- 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:
- Live: https://portfolio.tuliocunha.dev
- Repo: https://github.com/tuliopc23/Hackerfolio-tulio
If you adapt it, I’d love to see your variations.