← Back to Blog

Jenn's Ultimate Decades Tool: Replacing a Sims 4 Challenge Spreadsheet

The Sims 4 Ultimate Decades Challenge is played with a spreadsheet open next to the game: which day is it, what year is that, who ages up next, who's dead. I turned that spreadsheet into a mobile-first web app with generated timelines, auto age-ups, death rolls, a family tree, and a $2.99/month premium tier with paying subscribers.

reacttypescriptviteazureazure-functionsblob-storagestripegoogle-oauthreactflow

Live at sims-tracker.com · React, TypeScript, Azure Static Web Apps, Stripe subscriptions, and one very long spreadsheet that no longer exists.

The idea

The Ultimate Decades Challenge is a Sims 4 playthrough where a family lives through history, decade by decade, with house rules for how fast time moves and how long Sims live. The game doesn't track any of that for you. Players keep a spreadsheet next to the game: a row for every Sim day, a column for the challenge year, columns for each life stage so you know who ages up when, and a separate sheet listing every Sim who has ever existed.

Jenn plays this challenge, and that spreadsheet was the part of the game that wasn't fun. It was big, fragile, and miserable on a phone. So the brief was simple: replace the spreadsheet, keep the spreadsheet feel. It shouldn't be a generic dashboard with cards and gradients. It should be a fast, typography-led, spreadsheet-shaped tool that works one-handed while the game runs on the other screen.

It started as a scaffold on April 11, hit 1.0 on May 13, and is now past 700 commits and on v1.4. It's also a real product: players pay a monthly subscription for the premium tier.


What it does

  • Setup wizard. Pick a start year, how many Sim days make a year, and the starting weekday. Define human life stages and their lengths in Sim days, then add pets (dogs, cats, horses) and occults (vampires, werewolves, and so on) with their own aging tables. The wizard generates the whole timeline from that.
  • Timeline. The spreadsheet view: day of week, day number, year, events, births, deaths, and a column per life stage. Tap a day number to mark progress, or tap an earlier day to rewind.
  • Sims Info Sheet. The roster, with names, parents, partners, birth and death days, birthplace, heirs, and traits. Life stage is computed rather than picked: birth day plus current day plus the aging table.
  • Auto-timeline. Add a Sim with a birth date and they appear in the right life-stage column on their birthday. Their next age-up is filled in and moves forward as you play. If they die, their future age-ups disappear and their past ones stay.
  • Death rolls. Elders roll dice at each age-up to see whether they make it. The tracker handles the roll, marks the death or reveals the next age-up, and keeps the timeline consistent.
  • Family tree, stats, and photos. A generated family tree with lineage highlighting, a statistics tab (average age at death, gender ratio, and more), a marriage and pregnancy tracker, and a photo album for storytelling shots.
  • Demo mode. A read-only tracker for a medieval dynasty so new visitors can poke around without signing in.

Architecture

It's a static React app with a thin serverless API:

  • Frontend: React 19 + TypeScript on Vite, deployed to Azure Static Web Apps.
  • API: Azure Functions (Node) that SWA builds and hosts next to the site: get/put/list/delete saves, avatar and photo uploads, and the Stripe endpoints.
  • Storage: Azure Blob Storage. Each tracker is one JSON document (config, timeline, sims, currentDay) under the user's ID, and images are separate blobs.
  • Auth: Google Identity Services. No passwords, no user table. The Google account's stable sub ID is the user.
  • Payments: Stripe Checkout and the Customer Portal for a premium tier (unlimited trackers, life-stage photos, unlimited album photos).
  • Environments: staging and main each deploy to their own Static Web App through GitHub Actions, and each points at its own blob container.

One JSON blob per tracker is unfashionable, but it fits the domain. A tracker is edited by one person, loaded whole, and saved whole. There are no cross-user queries, so a database would add cost and nothing else.


Tech choices, and why

The timeline is derived, not stored

A challenge date is pure arithmetic: with a start year, days per year, and a starting weekday, any day number maps to a year and a weekday. The timeline is generated from config, and the only things stored per row are what the player adds.

Age-ups work the same way. Earlier versions stored each Sim's next age-up, and that went stale whenever anything changed: a renamed Sim, a mid-game rule change, a death. Now age-ups are computed from the Sim's birth day and the aging table every time, and auto-entries are keyed to the Sim's ID rather than their name. That one change removed a whole category of "why is Grandma aging up twice" bugs.

Editable rules mid-game

Players change their house rules halfway through a playthrough, so life-stage lengths, pets, and occults can all be edited after setup. When a rule changes, every Sim, living and dead, is recalculated against the new timings. That only works because the timeline is derived: there's nothing stored that needs migrating.

Debounced saves

Edits hit the UI immediately, and the blob write waits until the player pauses (about 30 seconds). Saves also flush on sign-out and page close. Tapping through a dozen days no longer means a dozen uploads.

Family tree with React Flow + dagre

The tree comes from parent and partner links on the Sims sheet, laid out top-down by dagre and rendered with React Flow. Hover a Sim and their ancestors light up in one color and their descendants in another, and everyone else dims. Partnerships carry a type (spouse, engaged, divorced, affair, widowed), each with its own marker.

Themes as tokens

The UI is built on global CSS variables. A theme is a data-theme attribute on <html> plus a block of tokens, and there are about a dozen, from default and dark to palettes like Berry Jungle and Petal Pop. The rule is that components never hard-code a color, and that rule caught a real bug: the photo album shipped with a few literal colors and looked broken in half the themes until they were swapped for tokens.


Things that fought back

  • 57,400 rows. A tracker starting in year 1 runs to 2050. At 28 Sim days a year, that's 57,400 timeline rows and roughly 2.7 million DOM nodes. It froze the browser, and because the save reloaded on every refresh, the player could never get back in. The timeline now renders a window of years around the current day, with "load earlier/later" buttons and a jump-to-year box. Most trackers fit in one window and look exactly as before.
  • Saves too big to save. Before photos moved into their own blobs, they were embedded in the tracker JSON, and trackers grew until the save failed silently. Photos are now separate uploads, and if a save does fail, the header says "Not saved," a banner shows the size, and there's a one-click backup download. Edits made during a failed save are kept.
  • Trusting the client. A security pass found the API taking the user ID from the request body and checking it against a header that was never sent. Every endpoint now verifies the Google ID token on the server (signature against Google's published keys, plus issuer, audience, and expiry) using node:crypto, and user IDs are validated before they touch a blob path. It's a good reminder that "the frontend only sends its own ID" is not an auth model.
  • Staging writing to production. Both environments shared one blob container, so testing on staging changed real trackers. Every handler now reads its container name from an app setting, which made isolation a config change instead of a code change.
  • Death rolls that didn't stick. A Sim who survived a death roll would sometimes lose their next age-up and have to roll again: if someone else died that day, after a reload, or after an edit. It took keeping the survival as its own fact that timeline rebuilds preserve.

Paying users

The free tier is the whole tracker. Premium is $2.99 a month and adds what costs me money or matters most to long-running playthroughs: unlimited trackers, life-stage photos, and an unlimited photo album (free trackers hold 50 photos).

Having people pay monthly changed how I build it:

  • Stripe does the billing. Checkout creates the subscription, the Customer Portal handles card changes and cancellations, and a webhook writes the subscription status to storage, where the API checks it. There are no card details in my code and no billing screens to maintain.
  • Premium status has to be trustworthy. Once people are paying, "is this user premium?" and "whose billing portal is this?" are security questions. They're answered on the server from a verified Google identity, never from anything the browser sends.
  • Losing data is not an option. Paying users have months of playthrough in their trackers, so save failures are loud, backups are one click, and staging runs against its own storage so testing never touches real trackers.
  • Changelogs are written for players. Every release gets plain-English notes and a "What's New" button, because subscribers should be able to see what they're paying for.

The pricing is deliberately small. It's meant to cover hosting and storage and reward the work, not gate the core challenge behind a paywall.

Building for one real user

The best thing about this project is that it has a user with opinions. Jenn's requests go into a running list (traits by life stage, name bubbles on the timeline, hiding tabs they don't use), and every release has a plain-English changelog written for players rather than developers. Having a real person playing a real game every night is what turned a spreadsheet clone into a tool other challenge players can pick up too.

Stack Summary

Layer Tool
Frontend React 19, TypeScript, Vite
Hosting Azure Static Web Apps (staging + production)
API Azure Functions (Node)
Storage Azure Blob Storage: one JSON per tracker, images as separate blobs
Auth Google Identity Services, ID tokens verified server-side
Payments Stripe subscriptions ($2.99/mo): Checkout, Customer Portal, webhooks
Family tree React Flow + dagre
Charts Recharts
Drag and drop dnd-kit
CI/CD GitHub Actions