Client · anon key
The browser talks to the database directly
- Todos and reminders read and write in place.
- Layout writes fire on every drag and resize.
- Public anon key only, nothing privileged.
DB-06Case study
A drag-and-drop widget dashboard with exactly one user: me. 25 live widgets, 18 API routes, and a real hosted database from day one, because the plan is for that user count to be temporary.
01Survey brief
This is my personal dashboard: weather, todos, reminders, GitHub activity, Spotify, site health, and the rest of a 25-widget grid I can drag into any shape I want. It has one user, no login, and no public URL.
The cheap way to build it is a static page and localStorage. I went the other way on purpose: a hosted database from the first commit, every widget built as a self-contained component, and a written roadmap that names the multi-user steps before they're needed. The bet is simple. If the personal version is engineered like a product, the product version becomes an upgrade instead of a rewrite.
The repo already talks like a product. The idea backlog tags certain widgets premium, and the TODO sketches accounts, saved dashboards, a marketplace, and a desktop wrapper. None of that is built yet, and this page won't pretend it is. What's built is everything below.
02The instruments
Every widget is one file in components/widgets/. At module scope it calls registerWidget() and drops its metadata into a registry Map: default size, minimum size, accent color, whether it needs a location, whether it has settings. The dashboard shell never imports a widget by name. It reads the registry.
That keeps the cost of a new widget honest: one new file, plus one import line in the index. The import line matters more than it looks. It triggers the module-scope registration, and its position controls the widget's column when the grid rebuilds from defaults.
The registered 25 pull from wherever their data actually lives: OpenWeatherMap for the weather cluster, an RSS proxy for news feeds, GitHub's GraphQL API for the contributions graph, Spotify for now-playing with playback control, and plain client-side math for the moon phase.
// components/widgets/WeatherWidget.tsx
registerWidget({
type: 'weather',
defaultSize: { ... },
minSize: { ... },
accentColor: '...',
locationBased: true,
hasSettings: true
});
// + one import line in index.ts, and it exists
Survey noteEach of the 25 widgets is one self-contained file that registers itself and owns its own live data, defaults, and settings. The dashboard grows one file at a time, not through a tangle of shared wiring.
03Grid mechanics
react-grid-layout v2 draws the grid. The decisions around it are where the engineering lives.
12 columns, 14px margins, 20px padding. Row height isn't a constant; it's computed from the window width so cells stay roughly square at any size. The whole rule is one line of math.
Adding a widget calls findOpenPosition(), a small first-fit scan across rows and columns for the first open slot. No modal asking where to put it.
onLayoutChange persists each widget's position and size to the database as it happens. There's no save button. The layout you leave is the layout you get back, on any machine.
// square-ish cells at any window width
// COLS 12 · margin 14 · padding 20
const rowHeight = Math.round(
(containerWidth - 40 - 14 * 11) / 12
);
04Base layers
Supabase holds the state in a handful of small tables: widget layout and config, settings, todos, reminders, quotes, and the statuses that tie the idea backlog to GitHub issues. Nothing clever. The point is that it's hosted and shared, not trapped in one browser profile.
The other route handlers do the outward-facing work: OAuth flows for Google and Spotify written as plain route handlers, an RSS proxy so feeds fetch server-side, and GitHub's GraphQL and REST APIs called with plain fetch. Data flow is client components plus route handlers, end to end. There isn't a server action in the repo.
Client · anon key
Server · service role
05Fine grain
Each widget declares an accent color in its metadata. It flows into the DOM as CSS custom properties that the theme CSS picks up for glows, so a new theme restyles every widget without touching one.
Space, minimal dark, and minimal light, each a CSS file scoped to a class on the html element, with SSR fallback variables so the first paint never flashes the wrong theme.
Around 500 widget ideas catalogued across 15 categories on a dedicated ideas page, tagged high-value, popular, gap, or premium, with each idea's status synced to GitHub issues through the app's own API.
Computed client-side from the date. No key, no request, no rate limit.
Live US AQI from Open-Meteo, with PM2.5, PM10, ozone, and nitrogen dioxide mapped across the six EPA bands from Good to Hazardous, each carrying its own color and health note. No key, no rate limit.
Search Console, Web Vitals, and Lighthouse widgets fed through the Google OAuth flow, putting search and performance data on the same grid as everything else.
06Margin of error
A case study about a work in progress should read like one. The margins are marked on the sheet.
There's no login and row-level security is off, and the project docs say so in plain words. The upgrade path is one sentence: add auth, add a user_id column, turn on RLS. That's an addition, not a rewrite.
Verification today is an ad-hoc Playwright script that captures screenshots for a visual check. CI/CD sits on the infrastructure list: planned, not pretended.
The TODO reads like a pricing page in waiting: accounts, multiple saved dashboards, a widget marketplace, a Tauri desktop wrapper, and premium notes down to removing the logo and custom domains. All of it is still ink on the roadmap.
08Next traverse
This dashboard is how I build when the only stakeholder is me: a real data layer, docs that admit what's missing, and components that keep the next feature cheap. If your team has an internal tool inching toward becoming a real product, I help teams in Chicago and everywhere make that jump on purpose.