Introduction — The Weekend Lie
If you’ve ever searched “How to add payments to your SaaS,” you’ll find dozens of cheerful tutorials.
“Just integrate Stripe.”
“Hook up a webhook.”
“Done in a weekend.”
That advice works — if you’re in the US.
If you’re an Indian founder trying to sell globally in USD, while building on Supabase, hosting on Render, and targeting creators across time zones — it’s not a weekend project.
It’s a systems problem.
And systems don’t forgive assumptions.
When I started building Vocalis, I assumed billing would be the easy part. I had already designed the content engine. The persona layer worked. The template renderer was stable. The AI outputs were structured.
Payments? That’s just plumbing.
I was wrong.
After being rejected by Lemon Squeezy.
After being rejected by Paddle.
After realizing Stripe Atlas wasn’t practical for me right now.
I had to build a production-ready billing system using Razorpay — one that could:
Handle international USD payments
Avoid currency misreporting
Support trial and non-trial flows
Allow launch offers without breaking renewals
Prevent duplicate subscriptions
Survive webhook chaos
This is the architecture I ended up building.
And why I’m glad I didn’t rush it.
The First Shock: The Metadata Mismatch
The first time I tested a USD payment, I was excited.
I ran a $19 Creator plan test.
Payment successful.
Webhook fired.
Database updated.
Everything looked clean.
Until I checked the logs.
Razorpay, in certain events, reported my home currency (INR) in the webhook payload — even though the plan was configured in USD.
Let that sink in.
If my backend trusted that metadata blindly:
A $19 subscription
Would appear as ₹19
That’s not a rounding error.
That’s a financial reporting disaster.
Now imagine:
Sending invoices with wrong currency symbols
Calculating revenue incorrectly
Emailing users the wrong plan amount
Reconciling payouts manually
This wasn’t cosmetic. It was structural.
The Fix: Amount-Based Currency Inference
I stopped trusting currency metadata.
Instead, I asked:
What’s invariant?
The unit amount.
My pricing structure was simple:
USD plans: under $100
INR plans: above ₹1000
That gap gave me leverage.
Inside my webhook handler, I built a logic gate:
Now instead of trusting metadata, I derive currency from economic context.
It’s not pretty.
But it’s robust.
Invoices show the right symbol.
Emails show the right symbol.
Reports show the right currency.
The system adapts — not assumes.
That was the first architectural pivot.
The Bigger Problem: Plan Keys Are a Trap
Originally, I linked subscriptions using plan keys:
creator
agency
starter
Simple.
Readable.
Clean.
Until I introduced a Launch Offer.
Let’s say:
Creator Plan → $29/month
Launch Creator Plan → $19/month
Now what?
If both use the key creator, you can’t distinguish pricing lineage.
If you overwrite the plan, existing renewals break.
If you delete the launch plan, grandfathered customers lose mapping.
I realized something important:
Plan keys are human-friendly.
They are not database-friendly.
Architectural Pivot: UUID-Based Plan Versioning
I rebuilt the schema.
Instead of linking subscriptions to a plan_key, I linked them to a plan_id (UUID).
The subscription_plans table now looks like:
id (UUID) | key | price | currency | is_active |
|---|
Key difference:
The frontend only shows plans where:
But the database keeps all historical plans.
To enforce safety, I added a Partial Unique Index:
This ensures:
Only one active Creator plan at a time
Old plans remain in DB forever
Existing subscriptions remain valid
Grandfathered users never break
Now I can:
Launch discounted plans
Retire them later
Keep historical pricing intact
Scale without fear
This was not a tutorial solution.
This was a founder-who-got-burned solution.
The “None” Date Crash
Billing systems have a personality.
They behave differently based on configuration.
Initially, I used trial-based subscriptions.
Razorpay sent a start_at timestamp.
My DB parsed it.
Everything fine.
Then for launch, I switched to Immediate Charge (no trial).
Suddenly:
start_at = null
And PostgreSQL doesn’t politely accept "None".
It throws:
On launch day, that would mean:
Payment succeeds
Webhook fires
DB crashes
User stuck in limbo
That’s the kind of bug that kills trust permanently.
The Surgical Date Fallback
Instead of assuming the timestamp would exist, I built defensive logic.
Inside webhook processing:
Then, in Supabase upsert logic, I used conditional filters.
No nulls propagate.
No invalid date inserts.
If metadata is missing, the system derives.
That’s a pattern you’ll notice:
Every time an assumption broke, I replaced it with inference + fallback.
Because production systems are not polite.
Idempotency: The Composite Shield
Webhooks are unreliable.
They retry.
They duplicate.
They arrive out of order.
If you naively insert rows on every webhook call, you end up with:
5 active subscriptions for one user
Broken dashboards
Corrupt analytics
So I designed what I call the Composite Shield.
Inside PostgreSQL:
This ensures the same state cannot exist twice.
Then in Python, I implemented a strict state machine:
Try update existing row
If no row updated → insert
If constraint violation → ignore duplicate
The database becomes the final arbiter of truth.
Not the webhook.
Not Razorpay.
Not my API.
The database.
This small mindset shift changes everything.
Why I Didn’t “Just Launch”
It would have been easy to:
Ignore currency mismatch
Ignore plan versioning
Ignore idempotency
Ignore null dates
Launch fast
Fix later
But billing bugs don’t just break features.
They break trust.
And trust is the only currency that matters for a solo founder.
I’ve rushed launches before.
I’ve shipped brittle foundations.
I’ve watched systems collapse under edge cases.
This time I didn’t.
The Emotional Layer
Building Vocalis is not my first attempt at building something meaningful.
I’ve built startups since 2005.
Most didn’t work.
Some survived.
None scaled globally.
Each time, I learned something.
This time I applied those lessons before launch.
Because the difference between a side project and a SaaS company is not UI polish.
It’s foundation integrity.
What This Architecture Now Enables
Because of these pivots, Vocalis now supports:
USD and INR seamlessly
Launch offers without renewal crashes
Grandfathered pricing models
Trial and immediate charge modes
Idempotent webhook handling
Reliable state transitions
That’s not glamorous.
But it’s scalable.
And scalable systems attract serious users — and serious investors.
Conclusion: Foundation Before Fame
The internet glorifies speed.
“Ship fast.”
“Launch messy.”
“Figure it out later.”
Sometimes that works.
But payments are not the place for chaos.
By facing these hurdles early, I’ve built a billing engine that:
Respects user timelines
Handles multi-currency cleanly
Avoids metadata traps
Survives webhook retries
Scales without schema fear
Vocalis launches tomorrow on Product Hunt.
But more importantly — Vocalis now rests on a foundation that won’t collapse under growth.
And for a solo founder rebuilding again —
That matters more than any upvote.
Experience it yourself:
👉 https://vocalis.so/launch
Related Post
Latest Post
Subscribe Us
Subscribe To My Latest Posts & Product Launches











