Development Personal Projects

startpage-app: Taking My New Tab Page From localStorage to the Cloud

How I rebuilt my personal startpage as an account-synced Next.js app — and why localStorage alone stopped being good enough.

The Problem With My Old New Tab Page

A while back I built myself a personal “start page” — a custom new tab page with a clock, a greeting, quick links to the sites I use daily, a switchable search engine, and a wallpaper I actually liked looking at. It was a single ~500-line Nuxt/Vue page, and everything — my link groups, my chosen wallpaper, my preferred search engine — lived in localStorage.

That worked fine right up until it didn’t. Switch to a different browser, get a new laptop, or accidentally clear site data, and the whole setup was gone. I’d rebuild it, again, from scratch. For a page I look at dozens of times a day, that was more annoying than it had any right to be.

So I rebuilt it — this time as startpage-app, a Next.js app where all of that state lives in an actual account instead of a browser tab.

What It Does Now

Sign in with Google and you get a dashboard with:

  • A live clock and date, plus a time-of-day greeting (“Good Morning/Afternoon/Evening/Night”) with a text color you can pick from a small curated palette
  • A search bar with a switchable search engine (Google, DuckDuckGo, or your own)
  • Custom link groups — bookmark folders, each with its own icon, fully CRUD-able, with a couple of sensible defaults (“AI Assistant,” “Social Media”) auto-created on sign-up
  • A wallpaper gallery — save several, switch between them, delete the ones you don’t want anymore
  • A floating, draggable YouTube Music mini-player, because a new tab page is a good place for that
  • Dark/light theme toggle

None of it is tied to a single browser anymore. Sign in anywhere and it’s all there.

The Stack, and a Couple of Real Gotchas

Under the hood it’s Next.js 15 (App Router) with React 19, NextAuth for Google OAuth, and Prisma talking to a Neon-hosted Postgres database. A couple of decisions worth calling out:

JWT sessions instead of database sessions. NextAuth’s Prisma adapter still persists accounts and users to the database, but I deliberately kept sessions as JWTs rather than DB-backed — it avoids a database round trip on every single request once a token’s been issued.

Server-side validation of “curated” choices. Things like the greeting color and the link-group icons are picked from a fixed list defined in code, not stored as arbitrary strings the client sends up. The server validates against that enum rather than trusting whatever the client claims it picked.

A subtle App Router trap. At one point layout.tsx had been turned into a Client Component just to host SessionProvider and ThemeProvider — which silently disables the file’s metadata export. My tab title and favicon quietly broke and I didn’t notice for a while. Fix was straightforward once I found it: pull those providers into their own client component and let layout.tsx stay a Server Component.

The July 1 Rewrite

Most of this came together in one intensive day of work on July 1st, where I brought the app up to full feature parity with the old Nuxt version — link groups, the music player, wallpaper management, search engine sync — and along the way patched two real CVEs, including a critical (CVSS 10.0) RCE in Next.js’s React Flight protocol that had landed in a recent release. Worth saying plainly: a good chunk of that day’s work was done pair-programming with Claude Sonnet 5, which made moving that fast on a rewrite like this a lot more feasible.

Where It Stands

It’s feature-complete relative to the old app, plus the account-sync piece that was the whole point of rebuilding it. But it’s not polished — there’s no test suite yet, a bit of mock fallback data still wired into the dashboard from earlier development, and the database schema already has displayOrder columns sitting there unused, waiting on a drag-to-reorder UI I haven’t built yet.

Next up: cleaning out that leftover mock data, and finally wiring up reordering for links and groups so the last bit of the old app’s polish makes it into this one too.

Related Posts