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.
Freelance
Need help with this?
I help startups and businesses ship web projects: migrations, new products, and performance fixes.
Get in touch →