Every Tool and System Behind My $944 MRR iOS App

GainFrame is 4 months old, at $944 MRR and growing 45% a month, and it's just me. Every department a normal app company hires for is a service or a Claude Code skill. This is the whole list, with the actual workflows and numbers.

By ·

Line-art illustration of a single figure at the center of a loop of six stations: a wrench, a paper plane, a megaphone, a magnifying glass, a coin, and a bar chart

Quick build-in-public update on my iOS app. GainFrame is 4 months old, sitting at $944 MRR, up 45% in the last month, 203 active subs. It's just me. People keep asking what tools I use, so this is the whole list, organized the way I think about it: like departments. Every department a normal app company would hire for is either a service I pay for or a Claude Code skill I wrote. I'll go through each one with the actual workflows and numbers, because the tool names alone are the least useful part.

RevenueCat overview dashboard showing 30 active trials, 203 active subscriptions, $944 MRR, $2,917 revenue and 1,103 new customers in the last 28 days
The RevenueCat overview as of this morning. $944 MRR, 203 subs, 30 trials in flight, 1,103 new customers in 28 days.

Fair warning that some of this is over-engineered for an app making $944 a month. I build automation because I like building automation. But most of it pays for itself in hours saved, and a few pieces are the only reason the app grows at all.


The Engineering Team

The stack: SwiftUI + SwiftData, iOS 17+, Swift strict concurrency on. The Xcode project is generated by XcodeGen from a project.yml — the .xcodeproj isn't even in git, so project-file merge conflicts don't exist. Dependencies split across CocoaPods and SPM, builds run through fastlane via Bundler.

On-device AI: Google MLKit (accurate pose detection, selfie segmentation, face detection) plus Apple Vision. One gotcha worth knowing if you go this route: MLKit ships no arm64 simulator slices, so every MLKit call site is gated behind #if !targetEnvironment(simulator) and a Podfile post-install hook strips its linker flags for simulator builds. Took me a while to land on that.

The backend is Supabase: 30 Deno edge functions, 24 SQL migrations. The centerpiece is coach-chat, an AI coach that streams Gemini responses and can call 17 tools against the user's real data — pull a metric range, compare two photo scores, read their Hevy workout volume by muscle group, list their Strava activities, generate a goal image, recall things from earlier conversations. The other big cluster is a report pipeline: nine generate-* functions behind a queue worker and a cron, each writing one chapter of a weekly report.

Model choice is boring on purpose. Gemini 3.5 Flash runs everything in the product. Reports used to run on the pro-tier model; I moved them to Flash in late May and generation went from 35–44 seconds to 10–15 with no quality complaints. Nobody ever churned because a report was insufficiently eloquent. They churn when it takes 40 seconds.

AI cost has its own guardrails, because the coach is my biggest infra line at roughly $257 a month:

  1. A cron checks trailing-24h Gemini spend and alerts Slack past $35/day total or $5/day for any single user (one user hammering the coach is a bug or an abuser, either way I want to know).
  2. CI has a prompt-size ratchet — a PR that grows the coach's system prompt fails the build unless I consciously raise the budget.
  3. A scheduled Claude agent periodically pulls per-module prompt instrumentation from PostHog and drafts the next prompt cut for me to review. Every proposed cut has to beat the current prompt on a replay harness of real conversations before it ships.

Claude Code writes most of the code. The repo has a CLAUDE.md that encodes the architecture rules — enum-driven navigation (never ad-hoc sheet bindings), the design system components, what never to touch — so every session produces code that looks like the rest of the app. And the part that changed how I work most: a skill called verify-on-sim that builds the app, installs it in the simulator, and drives it through RocketSim's CLI — it reads the accessibility tree, taps and swipes by element ID, and screenshots each step. A green build tells you it compiles. This tells you the feature works, before I even look at it.

Testing is 53 XCTest files, almost all logic engines (scoring, coach tools, recovery math) rather than UI tests. CI on GitHub Actions covers only the edge functions — lint, typecheck, the prompt ratchet. The iOS build itself ships from my machine. At one person, CI for iOS is ceremony.


The Release Manager

Shipping used to eat an evening. Now it's one command: /release.

What it actually does, in order: checks the live App Store and TestFlight build numbers to figure out whether this is a new version or a follow-up build, reads the git log since the last tag, drafts release notes from it, bumps version and build in project.yml, regenerates the project, commits and tags, archives with manual signing via fastlane match (certs live in a private repo, so a new machine is a clone away from shipping), uploads to TestFlight, and syncs App Store Connect metadata.

The metadata sync covers 9 localizations — en-US, en-GB, en-AU, German, French, Spanish (Spain and Mexico), Italian, Brazilian Portuguese. Screenshots upload per locale and auto-mirror: en-US copies to en-GB and en-AU, es-ES to es-MX. I localized the listing long before the app UI because it's the cheapest ASO surface there is.

Work tracking is GitHub Issues with P0–P3 priority labels and theme labels. /issues next picks the top-priority open issue and starts working on it; /issues new files one from whatever conversation I'm in. I resisted a real project management tool for months and I no longer feel bad about it.


The Content Studio

This is the weird one. I post comic carousels on TikTok and Instagram featuring a mascot called GainFrame Guy, and the whole thing is a deterministic pipeline.

How a carousel gets made: post ideas live in a posts.json. A build script sends Gemini's image model the mascot reference images (base64, so the character stays on-model) and has it draw only the art on a blank background — no text at all. The raw art gets cached to disk. A second script composites every word with Python's Pillow: fixed font, fixed sizes, fixed positions, fixed red accent color, page-number pills. Final slides are 4:5 at 1080×1350, converted to WebP, and synced to an iCloud drafts folder where I post them from my phone.

Why the split matters: I used to let the image model render the text, and every slide came out styled slightly differently — the account looked like a garage sale. Now the art is AI and the typography is code, so nothing drifts, and re-texting a cached slide costs nothing. Art generation runs about 4 cents a slide. 150+ carousels so far.

That's the general lesson of this whole post, honestly: when AI output drifts, stop prompting harder and move that part into code. AI for the art, Python for the text.

Other specifics that took real trial and error:

Also in this department: a promo video I paid a motion designer $49 for after he cold DMed me on Reddit (first cut arrived in nine hours and beat the screen recording I'd been using for months), a founding-creator program that's starting to get inbound applications from TikTok fitness creators, and Resend for email — a trainer waitlist form on the site posts to a Cloudflare Function that adds the address to a Resend audience and pings me.


The SEO Department

The blog has 141 posts and 11 free tools, all on a Next.js static export hosted on Cloudflare Pages. Hosting cost: $0. Push to main, it deploys.

The keyword pipeline spends nothing on tools. No Ahrefs, no SEMrush. It's a five-stage chain of Claude Code skills:

  1. Product context — one canonical file describing what the app actually is, so no generator ever invents features.
  2. Competitor discovery — find and classify competitors from search results and "alternatives to X" queries.
  3. Competitor scan — profile what each one ranks for and charges.
  4. Keyword discovery — expand seeds through Google autocomplete, read the live SERP to judge difficulty, cross-reference my own Search Console data over MCP, and append scored ideas to a backlog file with volume tiers.
  5. Article generation — one generator for guides and roundups, a separate one for "X vs Y" comparison posts. I edit, it deploys on git push.
Google Search Console performance chart over 3 months showing 2.84K total clicks and 129K impressions with a steep climb starting in June
Search Console, last 3 months: 2.84K clicks, 129K impressions. The climb on the right is compounding content, not a campaign.

Organic search went from about 7 clicks a day to about 100 over 90 days. The single best page is a "best AI body fat apps" roundup pulling roughly 170 clicks a week.

What works and what doesn't, from my Search Console rather than a guru thread:

Two rules I learned by breaking them: don't touch a page's title or meta twice within a week, because you can't tell which change did what. And check whether your last batch of posts is even indexed before writing more — I once spent a week writing into an indexing backlog, which is fighting the wrong blocker. I also decided against programmatic SEO — no generated page factories. Partly because I think Google eventually torches those, partly because I didn't want my name on one.


The Revenue Team

RevenueCat handles subscriptions. The structure that's survived experimentation: yearly gets a 7-day free trial, monthly gets none. The paywall leads with the yearly plan framed as a per-month price — same $39.99/year, completely different feel. I'm A/B testing that framing right now: as of last week the price-anchor variant converts 30.1% of paywall views into trial starts against 24.1% for the old trial-explainer version, but it hasn't hit significance yet so I'm not calling it. There's also a price experiment running I'm not ready to write about.

Beyond the main paywall, every Pro feature gates itself with a preview of itself: tap a muscle comparison as a free user and you see your real before/after muscle map with the detailed breakdown blurred behind the button. Open the coach and it answers one real question from your data before the ask. Show the thing, lock the depth. This pattern out-converts every generic "unlock premium" screen I've tried.

The piece I'd tell anyone to copy is the webhook. RevenueCat fires a webhook on every subscription event; an edge function turns it into Slack messages. Purchases, renewals, cancellations, billing issues — each with plan, country, and a "find this user in PostHog" link. Trial cancellations arrive flagged "reach out," and I sometimes email the person the same day and just ask why. The answers are blunt and more useful than any survey I've run.

Slack channel with real-time GainFrame alerts: paywall dismiss survey answers, an NPS response, and two trial-cancelled alerts with reach-out prompts (user details redacted)
This morning's feed: paywall-dismiss survey answers ("too expensive," twice), an NPS response, and two trial cancellations flagged for outreach. Identifiers blurred.

The scoreboard for all of this is a weekly audit skill that pulls RevenueCat, PostHog, and analytics into one dated report and compares me against indie-sustainable targets I set up front: $850 MRR at month 1 was the first gate, then $1.6K at 3 months, $3K at 6, $6K at 12. That report is where decisions come from, not vibes.

Trial-to-paid went from 21% to 44% in five weeks with zero ad spend. Most of that story is embarrassing: my analytics had logged 10 trial conversions in a window where RevenueCat counted about 48 — a wrong-flavor API key silently failing plus two event bugs — so the first "win" was just measuring correctly, and the rest was paywall work informed by finally having real numbers.


The Data Analyst

PostHog for product analytics on both the app and the site, GA4 running alongside it, Search Console for search. All three are wired into Claude over MCP, so "why did installs dip Tuesday" is a question I type, not a dashboard safari. A North Star dashboard gets checked daily; Slack gets alerts for website download clicks and body-fat scanner runs, in the same channel as the revenue events, so the phone buzz IS the dashboard most days.

Two hard-won specifics:

The doctrine, learned the expensive way: fix measurement before optimizing anything. My tracking was missing about 80% of trial conversions and I "optimized" against that noise for weeks.

Qualitative sides of the loop: Crisp for support chat, a public Featurebase board for feature requests, and an in-app feedback bot that posts straight to Slack with device, version, and photo count attached.


What I Fired: Paid Ads

I spent $5,674 on ads over three months:

Blend it against actual payer LTV of about $18 and the best case was ~$114 to acquire a paying user. I stopped all of it on May 10. Installs dropped to the organic floor for a few weeks, then started climbing again — except this time the growth came from things I own that don't stop working when I stop paying. That failure funded this playbook: the blog, the TikTok pipelines, and the free tools all got serious the week the ads died.

What the machine costs to run, since people always ask: Gemini for the product AI is the big line at roughly $260 a month (with the alerting above so it can't quietly triple). Image generation is ~4 cents a slide. Hosting is $0 on Cloudflare Pages. SEO tooling is $0. RevenueCat and PostHog are both free at my scale. Apple takes $99 a year plus their commission. The promo video was $49 once. The expensive input is the two focused hours a day, and the AI subscriptions that make those hours count.

Infographic: one founder, six stations. A circuit diagram of Build, Ship, Attract, Rank, Convert and Measure stations looping around a Claude Code agent hub, with stack details for each station and headline stats of $944 MRR, 21 to 44 percent trial conversion, 15x organic traffic, 4.9 star rating and zero paid ads since May
The whole circuit on one page. Each station feeds the next; the founder does taste and decisions.

If You're Starting One of These

Steal in this order:

  1. RevenueCat webhook to Slack. One evening of work. Changes your relationship with churn from a weekly chart to a conversation you can have the same day.
  2. Fix your conversion tracking before optimizing anything. Mine was 80% wrong and I optimized against noise for weeks. Reconcile your analytics against your payment provider's numbers — they will not match, and the payment provider is right.
  3. Free calculators on a static site. They rank faster than articles, keep ranking, and earn the backlinks your articles never will. Rate-limit anything that costs you API money.
  4. Comparison and roundup posts before informational posts. Shopping-intent queries send clicks; informational queries send impressions that die in the AI overview.
  5. Put your dev rules in a CLAUDE.md. Architecture, design system, what never to touch. The compounding from every AI session writing code that matches your app is real.
  6. When AI output drifts, move that part into code. Prompting harder is a treadmill. My comics only became consistent when the text became Python.

Not a huge business yet. But it grew 45% last month on channels I own, the content from March still pulls installs today, and the whole thing runs on maybe two focused hours a day. Slower than ads, but it compounds, and it's mine.

Happy to answer questions about any piece of this.

See What the App Actually Is

GainFrame is the AI body-composition app all of this machinery exists to grow: body fat %, physique score, and per-muscle ratings from a progress photo, plus an AI coach grounded in your real numbers.

Download GainFrame Free

Related Articles