GraceSoftGraceSoft

GraceSoft Story, Day 2: Wiring Billing Into the Story (and Bringing the UI Back)

Davina LeongApril 7, 20263 min read

Today started deep in the backend and ended with the UI finally catching up.

The problem

GraceSoft Story started as a way to turn commits into timelines. But timelines alone aren’t a product. To make it real, I needed billing, plans and feature access control, without turning the codebase into a mess of if (plan === 'pro').

Step 1: Plans shouldn’t live in code

refactor(plans): remove implicit plan seeding

Plans don’t belong in migrations or seeders. They belong in Stripe. So instead of defining plans internally, I made Stripe the source of truth.

Step 2: Sync Stripe → app

feat(stripe): store product details when syncing catalog

Products and pricing now live locally after a sync. That means fast queries, mapping features to plans, and no dependency on the Stripe API at runtime.

Step 3: Subscription modelling

feat(billing): integrate account-plan-subscription schemas

I avoided the typical shortcut (user.plan) and used three separate models instead: accounts, plans and subscriptions. This keeps things flexible for teams, shared billing and multiple repos per account.

Step 4: The Stripe mapping problem

fix(stripe): persist subscriptions using metadata tier mapping

Stripe doesn’t know your app’s logic, so I used metadata to map Stripe products to internal tiers. That avoids brittle assumptions like matching on names.

Step 5: Feature gating as data

feat(plans): add pricing-gated feature schema
feat(stripe): refresh plan gates from catalog tier updates

Instead of hardcoding:

if ($user->plan === 'pro')

I now have feature definitions tied to plans and synced automatically. So the system can answer:

“Does this account have access to this feature?”

Step 6: Event-driven billing

feat(api): add stripe and postmark webhook endpoints

Billing state is now asynchronous, event-driven and consistent. Updates no longer depend on something the user does.

Step 7: Document before it gets messy

docs(readme): replace template with project documentation

The README now covers the architecture, the billing flow and the schema decisions.

Step 8: Design direction first

chore(ui): add design inspiration

Before jumping into the UI, I set the tone: a pre-glass macOS feel, structured and calm interfaces, and a layout that puts the narrative first.

Step 9: The UI catches up

feat(ui): implement app shell and redesign core story pages

The app shell is in place, the story pages are redesigned, and the layout matches the “timeline as narrative” idea.

What changed today

  • Billing is no longer an afterthought
  • Plans live outside the code
  • Features are structured data
  • The UI is catching up to the system

More importantly:

The system now understands who gets to see what part of the story.

Closing thought

Today wasn’t about flashy features. It was about removing shortcuts. The moment you introduce billing, you’re forced to answer:

  • What is valuable?
  • What is restricted?
  • What is part of the story?

Next up: enforcing feature gates across the UI and API, connecting usage to pricing, and refining the UX around locked features.

If you’re building something similar, how are you structuring feature gating without polluting your domain logic?


Part of the GraceSoft Story build-in-public series. Originally published on DEV.

Share this post

← Back to Blog