GraceSoft Story, Day 2: Wiring Billing Into the Story (and Bringing the UI Back)
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.