Wonsuk Choi
← blog

How I Built a Personal Ops Dashboard with Next.js, Supabase, and AI

I built a private ops dashboard to run a dozen web projects, then cut it to a third of its size. What I built, what I removed, and why smaller works better.

Why I Built It#

When you run several web projects at once, each with its own domain, database, analytics and users, managing them becomes its own part-time job. I kept switching between hosting dashboards, Supabase, Google Analytics, email and three different todo apps, and nothing was connected.

So in early 2026 I built a private ops dashboard into wonsukchoi.com: one Next.js app meant to hold everything I needed to run my projects. This post covers what was in it, the technical decisions behind it, and the part I did not expect to write about: how much of it I later deleted, and why the smaller version works better.

What It Started As#

The first version grew into five tab groups.

  • System: a site monitor polling every live site every 30 seconds (an HTTP check plus a check of each Supabase project, deduplicated because several sites share one database), and domain-expiry warnings.
  • Tools: a private AI chat connected to several models through OpenRouter, with model switching mid-conversation.
  • Content: a journal and a notes system with nested categories, per-note passwords, image uploads and an offline sync queue built on IndexedDB.
  • Work: todos, a calendar with Google Calendar sync, a subscription cost tracker, plus habits, income, goals and a reading list.
  • Site: contact messages, newsletter subscribers, analytics and the blog editor.

The main component passed 4,000 lines.

What I Removed, and Why#

Over the summer I took most of it out again.

  • The newsletter (June). It had almost no subscribers and a signup form on every page. I removed it end to end, including the table.
  • The whole personal half (June). Journal, notes, the AI chat, subscriptions, habits, income, goals, reading and the calendar sync went in one commit: about 9,000 lines and five API routes. They had been replaced by Hermes, an AI agent I run on my own server that handles that side of my day (I wrote about it here). A second home for the same data was just more to maintain.
  • Site and database monitoring (July). I watch servers with a separate system, so the dashboard tabs were duplicates.
  • Leftovers (July and August). Tabs I had already hidden still had their components and data plumbing in the code. I deleted them and split what remained into one file per tab.

What It Is Now#

Three tabs:

  • Overview: the day at a glance. Unread messages, new inquiries, page views this week, weather, and a short preview of each section.
  • Messages: contact-form submissions with status and archiving.
  • Blog: the editor and publishing flow I am using for this post: drafts, auto-save, scheduling.

Site status did not disappear; it moved to where it is useful. The project cards on my public homepage show a live up or down state for 16 sites, and a scheduled job checks them and alerts me when one goes down.

Technical Decisions That Held Up#

Next.js App Router#

The dashboard page is a server component that loads its data before rendering, so there are no loading spinners on first view and no client-side request waterfalls. Mutations go through server actions (publishing a post, archiving a message), which removes a layer of API-route boilerplate and keeps data code next to the components that use it.

Supabase for Auth, Database and Storage#

Supabase handles sign-in, the Postgres database and file storage for blog images. Types are generated from the database schema, so a schema change shows up as a TypeScript error in the code that uses it instead of a runtime crash.

Row-level security is on for every table, but I no longer treat it as the whole security model. The dashboard reads data with the service-role key, which bypasses those policies, so every server action and API route also checks who is calling. Database policies are the backstop; the explicit check in each route is what actually keeps data private.

The Same Patterns, at a Larger Scale#

The patterns from this project carried into ExpertSapiens, a multi-tenant platform that now runs more than a dozen client and partner sites from one Next.js codebase: the tenant is resolved from the request hostname, configuration is a typed object, and each tenant's data is isolated in a shared database. Adding a tenant is a configuration entry, not a new deployment.

What I Learned#

  • Build for the workflow you have, not the one you imagine. Half of the original features were things I thought I would use daily. Once an agent did the same jobs from chat, the screens went unused.
  • Delete in full. Hiding a tab is not removing it. The dead components and data plumbing stayed until I deleted them in a separate pass.
  • Small files from the start. One component per tab would have kept the 4,000-line file from ever existing.

A private dashboard needs the same skills as a client-facing product: authentication, background jobs, file uploads and a clean interface. The difference is that the only user is me, so I can be ruthless about cutting what I do not use.

If you need someone to take a project from an empty repository to production, with Next.js, Supabase, TypeScript and AI integrations, I would be glad to talk.

Get in touch →

Related posts

How I Built a Personal AI That Knows My Entire Business

I built an AI that can represent me accurately on my personal site — not with RAG or vector search, but with a simpler pattern that works better at this scale. Here's the architecture, the tradeoffs, and what building a real knowledge base about yourself actually involves.

Freelance

Need help with this?

I help startups and businesses ship web projects: migrations, new products, and performance fixes.

Get in touch →