Ominvo beta launches July 23, 2026 — only 10 spots remain Join the waitlist →
All posts
Building OminvoDay 39

Day 39: MRR math fix, two new pages, and one engineering dead-end

June 10, 20265 min read

Today shipped three real things and produced one dead-end. The dead-end taught more than the wins. Honest recap of all four.

The MRR math fix

The /api/admin/revenue endpoint had two silent bugs that I caught while planning Day 39. Neither would have surfaced as an error — they would have just shown wrong numbers in the Revenue tab forever.

Bug one: Stripe price IDs were hardcoded as string literals in the route file. Not env vars — actual strings in the code. That means at live-mode cutover (~July 23), swapping to live-mode prices would have required a code change and a deploy. Not how env vars are supposed to work. Fixed: all four price IDs now read from STRIPE_CHAD_PRICE_ID, STRIPE_GIGACHAD_PRICE_ID, STRIPE_CHAD_ANNUAL_PRICE_ID, and STRIPE_GIGACHAD_ANNUAL_PRICE_ID. The same vars already in Vercel for the checkout API.

Bug two: annual subscriptions were being counted at their full annual amount toward MRR. A Chad annual sub ($288/yr) was contributing $288 to MRR instead of $24. GigaChad annual ($948/yr) was contributing $948 instead of $79. Test data with one annual sub would have made the dashboard look like there were twelve paying customers. Post-launch with real annual subs this would have been a serious data hygiene problem. Fixed: amounts now read directly from Stripe's price object (unit_amount / 100 to convert from cents), and annual amounts are divided by 12.

Reading from Stripe's price object instead of hardcoding the amounts is the better long-term call. If pricing ever changes, the API self-corrects without a code change.

The analytics dashboard feature page

Third Product dropdown item now lives at /features/analytics-dashboard. Ten sections covering rating trend, response rate, review velocity, star distribution, competitor tracking, and the tier comparison. Every section has an HTML/CSS mock of the actual UI — the dashboard top nav, stat cards, review cards in their real styling, an SVG trend chart, a velocity bar chart. Pure server component, no JavaScript.

The tier comparison section answers the question most visitors ask first: what do Noob, Chad, and GigaChad actually unlock? Noob gets a basic review count. Chad adds the full analytics dashboard. GigaChad adds competitor tracking. Makes the pricing page math make sense.

The MDX dead-end

This is the part worth writing up honestly. The plan after shipping the analytics page was to write a 5000-word comparison post — Birdeye vs Podium vs Ominvo — with embedded charts, pricing tables, and visual breakdowns inside the blog post itself.

The current blog system uses next-mdx-remote/rsc to render MDX. Plain markdown works fine but custom React components do not — there is no components registry wired into the MDXRemote call. To embed charts inside posts I would need to add one.

I added a BlogCharts.tsx file with five reusable server components: PriceCompareBar, PieChart, LineChart, CompareTable, StatCallout. Wired them into the slug page's MDXRemote components prop. TypeScript passed. Local dev preview rendered correctly. Pushed.

Vercel build failed: TypeError: Cannot read properties of undefined (reading 'map') during prerender of the new blog post. Added defensive default array values to all five components. Built locally — passed. Pushed. Build succeeded this time. Visited the post. The components rendered as empty containers. Title text showed. Grid lines on charts showed. But the actual data — the bars, slices, points — none of it appeared.

The cause: next-mdx-remote/rsc passes multiline JSX array props through MDX in a way that does not consistently evaluate them as JavaScript arrays. The components rendered with their default empty arrays in production, even though they had concrete data in the MDX source. Locally this worked because of dev-mode hot reload behaving differently than the prerender pipeline.

The honest call at that point was to stop. Two paths forward: either rewrite the chart system to accept data via something other than JSX array props (a JSON file, a dedicated data layer), or step back and put the content somewhere it would work without the MDX gymnastics. I picked the second.

Reverted the BlogCharts.tsx component, the slug page changes, and the blog post. Restored the blog system to its Day 39 morning state. Two hours of work erased. The work was not wasted — the diagnosis is in this post, and the next blog post that needs visuals will not start by trying to embed JSX charts in MDX.

The /why-us page instead

The actual deliverable for the comparison content became a standalone marketing page at /why-us. Same content I had planned for the blog post, but as a pure TSX page where I have full control over the rendering — no MDX intermediary.

The page covers nine sections: a hero positioning Ominvo GigaChad ($79/mo annual) against Birdeye Growth ($349/mo per location), three pricing reality cards showing year-one costs, a full HTML/CSS dashboard mock with stat cards and review cards in the real product styling, a four-step how-it-works explainer, an SVG rating trend line chart, a donut chart for star distribution, the feature-by-feature comparison table, year-one cost bar chart, three reasons, three competitor verdicts, and a final CTA.

The pricing data is real and current. Birdeye Starter is $299/month per location plus a $500-$1,500 setup fee and an 8% renewal increase. Podium Core is $399/month with AI replies as a $99/month add-on. Both require annual contracts. Ominvo GigaChad at $79/month annual delivers the same competitor tracking and analytics that Birdeye charges $349/month per location for.

The page took less time to build as a standalone TSX route than the MDX chart system did to fail. That is worth remembering.

What is next

Day 40 priorities: annual billing end-to-end test in Stripe test mode, the /features/smart-filters page as the next Product dropdown destination, and a decision on whether to revisit the MDX chart system properly or just commit to TSX pages for content that needs visuals.

Internal links: /why-us, /features/analytics-dashboard, /pricing, /changelog.

Day 39 of building in public. Full changelog at /changelog.

Written by

The founder of Ominvo

Building review management for single-location small businesses. Join the waitlist →