# The App Store Gauntlet

> Build log · Month 5 of 12

the craft between "works on my simulator" and "a stranger installed it." Privacy manifests, account deletion, Sign in with Apple, RevenueCat wiring, and eating review rejections.

Day 121–150 · 30 posts · Building in public as @caelum0x4

**Day 121** — This month: the App Store. The gap between "runs in my simulator" and "a stranger downloaded it" is full of craft nobody tweets about — privacy manifests, account deletion, review rejections. So I'm going to tweet about it. Someone has to.

**Day 122** — The privacy manifest (`PrivacyInfo.xcprivacy`) is mandatory now. You declare every data type you collect and *why*, plus "required reason" APIs. I keep a shared manifest in the core and each app extends it. Declare it once, honestly, or fail submission.

**Day 123** — Rejection #1: CalDesk got bounced for requesting health-adjacent data without a clear enough usage string. The `NSUsageDescription` can't be lazy — "needs your data" gets rejected; "to log meals against your daily nutrition goals" passes. Say the actual why.

**Day 124** — Account deletion is non-negotiable: if users can sign up in-app, they must be able to delete the account *in-app*, not via support email. Built one shared flow in the core: confirm, revoke tokens, purge server data, sign out. Every app inherits it.

**Day 125** — Account deletion has a nasty edge: what about purchases, synced data, an in-flight subscription? I delete identity + personal data, keep anonymized aggregate telemetry (never tied to them anyway), and explain exactly that on the confirm screen.

**Day 126** — Thread: the App Store submission checklist I wish I'd had on day one.
  - Privacy manifest declaring every data type + required-reason APIs. Wrong = rejection.
  - In-app account deletion if you have accounts. Support-email-only won't pass.
  - Sign in with Apple if you offer any third-party social login. Required, not optional.
  - Real usage strings: state the actual purpose, not "needs access."
  - No placeholder content, no broken links, no "coming soon" buttons. Reviewers tap everything.

**Day 127** — Sign in with Apple rule that catches everyone: if you offer Google/social login, you MUST also offer Sign in with Apple. Not a preference — guideline 4.8 territory. I built it into shared auth so no app can ship social login without it. Compliance by architecture.

**Day 128** — Sign in with Apple gotcha: Apple gives you the user's email/name *only on the first authorization*, once, ever. Miss it and you can't get it again without the user revoking and re-authing. I persist it on first callback immediately. There's no second chance.

**Day 129** — RevenueCat keeps me sane across a fleet of paywalls. One dashboard, one SDK, handles receipt validation, subscription state, cross-platform entitlements. Rolling my own StoreKit receipt validation twelve times is how I'd lose the year. Buy the boring infra.

**Day 130** — StoreKit bug: entitlement state is *async* and can lag on cold start. I flashed the paywall to a paying user for a split second before the receipt resolved. Fix: default to "loading," never "locked," until entitlement is known. Never accuse a paying customer.

**Day 131** — Reviewers tap everything. A rejection came because a settings link went to a placeholder page. They grade the build in front of them, not your intent. So I do a full "tap every button, follow every link" pass before submission. No dead ends.

**Day 132** — App Review notes are underrated. A clear note — test creds, how to trigger the paywall, why you need a permission — turns a likely rejection into an approval. Reviewers are humans with a queue. Make their job easy and they'll make yours easy.

**Day 133** — Screenshots are marketing, not documentation. My first set was literal UI dumps and they were dead. Redid them with a one-line value prop per shot. Same app, very different tap-through. The store listing is the top of your funnel — don't treat it as an afterthought.

**Day 134** — Rejection #3: a demo account that "didn't work" for the reviewer — because it required an email verification they couldn't complete. Fix: a pre-verified review account that skips the friction. If your reviewer can't get in, you're rejected regardless.

**Day 135** — Guideline 2.1 (completeness) is where "minimum viable" dies. An app with three real screens and nine "coming soon" gets bounced. Ship fewer, *finished* screens. Better a small complete app than a big skeleton. Reviewers can smell a demo.

**Day 136** — Thread: my per-app App Store submission runbook.
  - Version + build number bump. Changelog written for humans, not "bug fixes."
  - Screenshots refreshed, one value prop per shot.
  - Privacy manifest diff reviewed. New data collection = new declaration.
  - Reviewer note: test creds, how to trigger the paywall, why each permission.
  - Tap every button, follow every link. No dead ends.
  - It's a checklist you rerun: 20-minute submit vs. a lost afternoon.

**Day 137** — TestFlight before the store, always. External testers catch the "works on my device" bugs — older phone, different locale, tiny screen. My simulator is a liar of omission; it never runs out of storage or loses signal mid-sync. Real devices, real conditions.

**Day 138** — AdMob for the free apps (Cipher, Vertex): mediation set up, but the real work is *where* the ad goes. An interstitial mid-puzzle would nuke retention. Ads at natural stops — after a result, between sessions — not mid-flow. Monetize the pause, never the peak.

**Day 139** — App Tracking Transparency: touch the IDFA and you must ask, most users say no, and that's fine. I designed the ad setup to work without tracking consent, because assuming a "yes" you won't get is building on sand. Consent-optional by default.

**Day 140** — Halfway through the year next month, and here's the App Store truth: getting *approved* is a solvable checklist. Getting *found* in the store is the actual boss, and no privacy manifest helps with that. Approval is table stakes. Discovery is the war.

**Day 141** — Localization is a distribution lever I underused. Just localizing the store listing (title, keywords, screenshots) for a few languages opens whole markets the algorithm otherwise hides you from. Translating the *listing* is cheaper than the app and moves installs.

**Day 142** — Keyword craft in App Store Connect is brutal: 100 characters, comma-separated, no wasted spaces, don't repeat your app name (already indexed). I treat it like a tiny SEO problem. The store search bar is a distribution channel I ignored for months.

**Day 143** — A rejection I deserved: shipped a build with a debug menu reachable by a long-press I forgot to gate. Reviewer found it. Embarrassing and fair. Now the release build strips debug affordances via a compile flag. If a reviewer can reach it, so can a user.

**Day 144** — In-app purchase restore is required and everyone forgets it. New phone, reinstall, "where'd my subscription go" — you MUST have a "Restore Purchases" button. RevenueCat makes it one call, but you still have to add the button. Missing it = instant rejection.

**Day 145** — Opinion: Apple's rules are annoying and mostly *right*. Account deletion, privacy manifests, restore purchases, Sign in with Apple — I resented each and each protects the user. The gauntlet makes the fleet better. I just wish it told you the rule before the rejection.

**Day 146** — The economics no one mentions: $99/yr across a 12-app fleet is nothing per app. The expensive part is the *time* per submission — screenshots, review notes, rejection round-trips. So I systematized it. Amortize the process, not just the fee.

**Day 147** — Shipped a fleet-wide compliance pass: every app with accounts now has in-app deletion, Sign in with Apple, restore purchases, and a privacy manifest — all inherited from the core. Fix the platform once, twelve apps comply. Architecture beats checklists.

**Day 148** — Month 5 lesson: "done" has a legal and platform definition, not just a technical one. Code that works isn't shippable; code that works *and* passes review *and* respects data rights is. The gauntlet is where hobby projects become real products.

**Day 149** — Next month: Huddle, self-hosted team chat on Matrix + LiveKit. After a month of platform *rules*, I get to build platform *infrastructure* — real-time messaging and screenshare. The hardest distributed-systems problems in the fleet. Excited and slightly scared.

**Day 150** — [N] approvals, [N] rejections, [N] resubmissions this month, bracketed until I total them honestly. The store is no longer a mystery gate — it's a checklist I can run. That's a real unlock. Now back to deep engineering. Real-time systems, here we go.

← [Month 4: Cipher & the Retention Loop](https://ereboslab.com/blog/month-4-cipher-and-the-retention-loop/) · [Month 6: Huddle: Real-Time Is Hard](https://ereboslab.com/blog/month-6-huddle-real-time-is-hard/) →

[← All build-log months](https://ereboslab.com/blog/)
