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 this real, I needed:
- billing
- plans
- feature access control
Without turning the codebase into a mess of if (plan === 'pro').
Step 1 — Plans Shouldn’t Live in Code
First thing I removed:
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
Now products and pricing live locally after sync.
This unlocks:
- fast queries
- feature mapping
- no runtime dependency on Stripe API
Step 3 — Subscription Modeling
feat(billing): integrate account-plan-subscription schemas
I avoided the typical shortcut (user.plan).
Instead:
- accounts
- plans
- subscriptions
This keeps things flexible for:
- teams
- shared billing
- 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 → internal tiers
This avoids brittle assumptions like name matching.
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
- dynamically synced
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:
- async
- event-driven
- consistent
No more relying on user-triggered updates.
Step 7 — Document Before It Gets Messy
docs(readme): replace template with project documentation
Captured:
- architecture
- billing flow
- schema decisions
Step 8 — Design Direction First
Before jumping into UI, I set the tone:
chore(ui): add design inspiration
Leaning towards:
- pre-glass macOS feel
- structured, calm interfaces
- narrative-first layout
Step 9 — UI Catches Up
feat(ui): implement app shell and redesign core story pages
Finally:
- app shell in place
- story pages redesigned
- layout aligns with the “timeline as narrative” idea
What Changed Today
- Billing is no longer an afterthought
- Plans are externalized
- Features are structured data
- 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.
Because the moment you introduce billing,
you’re forced to answer:
- What is valuable?
- What is restricted?
- What is part of the story?
Tomorrow:
- enforce feature gates across UI + API
- connect usage → pricing
- refine the UX around locked features
If you're building something similar,
how are you structuring feature gating without polluting your domain logic?
Top comments (0)