●RORKMAX — Rork Max generates pure Swift instead of React Native, enabling true native apps across iPhone, iPad, Watch, TV, Vision Pro, and iMessage●APPLE — Rork's 2026 direction has a clear theme of native empowerment across the Apple ecosystem●EXPO — Standard builds run on React Native and Expo, so you're left with a real project structure and code you can keep working on●FUNDING — Rork recently raised $15M and now sees over 743,000 monthly visits with 85% growth●PRICING — Rork is free to start, with paid plans from $25/month and Rork Max at $200/month●CROSS — Rork builds iOS, Android, and web from a single prompt, finished off with a bit of follow-up tweaking●RORKMAX — Rork Max generates pure Swift instead of React Native, enabling true native apps across iPhone, iPad, Watch, TV, Vision Pro, and iMessage●APPLE — Rork's 2026 direction has a clear theme of native empowerment across the Apple ecosystem●EXPO — Standard builds run on React Native and Expo, so you're left with a real project structure and code you can keep working on●FUNDING — Rork recently raised $15M and now sees over 743,000 monthly visits with 85% growth●PRICING — Rork is free to start, with paid plans from $25/month and Rork Max at $200/month●CROSS — Rork builds iOS, Android, and web from a single prompt, finished off with a bit of follow-up tweaking
Semi-Automating App Store Review Responses with AI — App Store Connect API × Gemini Field Notes
Field notes on building an AI-drafted, human-approved review response pipeline that I run across multiple iOS and Android apps. Includes 50M-download-scale operational lessons, Crashlytics integration patterns, and confidence-threshold tuning that aren't in any official documentation.
There's a task that catches up with every app developer eventually: responding to user reviews on the App Store and Google Play.
From "love this app, using it every day!" to "crashes constantly, can't use it," keeping up with thoughtful replies is a time cost that's easy to underestimate. When I tracked it carefully across multiple apps, I was spending an average of 32 minutes per day on review monitoring and responses — about 16 hours a month. Not nothing.
I've been running iOS and Android apps as an indie developer since 2014. Cumulative downloads across the portfolio passed 50 million a while back, and I'm still actively maintaining several wallpaper and ambient-content apps today. Out of every operational chore in that workflow, review responses were the last holdout — the one task that resisted automation longest. Pasting a generic template tanks the rating; ignoring reviews tanks first-impression conversion. So I built a pipeline that drafts replies with AI and lets me approve them with a single tap. Below is the full architecture, plus the Rork-built mobile dashboard and Cloudflare Workers backend code I run in production.
"Won't the responses feel robotic?" is the natural concern. In practice, Gemini generates responses that feel like a real developer wrote them, understanding your app's context and adjusting tone by territory. The first time you see it in action, the quality is genuinely surprising.
Why Review Responses Actually Move Your Ratings
Both Apple and Google have data suggesting that developer responsiveness improves search ranking. Sensor Tower's research indicates that reviews receiving developer responses see an average rating improvement of 0.3–0.5 stars.
The more impactful effect is this: when you respond thoughtfully to a one-star review, users frequently update their rating. That "someone actually listened" experience turns into word-of-mouth. It's one of the few levers solo developers have that scales without a marketing budget.
The reasons responses pile up unaddressed are equally real though. Running multiple apps means just checking all the platforms takes time. Writing variations of the same bug acknowledgment for the hundredth time is tedious. Non-English reviews (Korean, German, Arabic) require translation effort. And emotionally charged one-star reviews take mental energy to engage with constructively.
The solution here is a hybrid model: AI drafts the response, you approve it. Full automation is tempting but wrong — a final human review means nothing problematic goes out. High-confidence responses (clear, unambiguous cases) get sent automatically; uncertain ones come to you.
App Store Connect API Setup
Generating Your API Key
Head to https://appstoreconnect.apple.com/access/integrations/api and create a new Team Key. A critical detail: the .p8 private key file can only be downloaded once at creation time. Store it securely immediately.
You'll need three values:
Issuer ID: The UUID shown at the top of the API keys page
Key ID: The 10-character alphanumeric string shown after key creation
Private Key: The contents of the downloaded .p8 file
Implementing JWT Authentication
App Store Connect API uses JWT authentication with the ES256 algorithm. Here's the Cloudflare Workers implementation:
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Architecture and full implementation of an AI-drafts / human-approves review pipeline, battle-tested across a 50M+ download portfolio of iOS and Android apps
✦Crashlytics integration pattern that auto-branches reply tone based on whether a crash is newly detected, fixed-pending-release, or already shipped
✦Confidence-threshold tuning, JWT clock-skew workarounds, and Google Play quota realities — the kind of judgment data you can't get from official docs
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Enable the Google Play Android Developer API under APIs & Services → Library
Create a service account under IAM & Admin → Service Accounts
Download a JSON key for that service account
In Google Play Console, go to Settings → API access, link the service account, and grant Reply to reviews permission
Note that both steps 4 and 5 are required — the Google Cloud service account and the Play Console permission grant are separate systems.
JWT Authentication for Cloudflare Workers
Google Play uses RS256 (RSA-SHA256) rather than ES256. There's also an extra step: your JWT assertion needs to be exchanged for an OAuth2 access token before calling the API.
Gemini API: Sentiment Analysis and Response Generation
This is the core of the system. The key design choices here are using responseMimeType: "application/json" for structured output, keeping temperature low for consistency, and building a fallback for the rare cases when JSON parsing fails.
// src/ai/reviewResponder.tsinterface ReviewAnalysis { sentiment: "positive" | "negative" | "neutral" | "mixed"; mainIssue: string | null; suggestedResponse: string; confidence: number; // 0.0–1.0: how confident Gemini is in the response quality shouldAutoSend: boolean; // true only when confidence ≥ 0.85 and content is clearly safe}async function analyzeAndRespond( review: { rating: number; title: string; body: string; territory: string; }, appContext: { name: string; category: string; recentUpdates: string[]; knownIssues: string[]; }, geminiApiKey: string): Promise<ReviewAnalysis> { const responseLanguage = getResponseLanguage(review.territory); const prompt = `You are the developer of an app called "${appContext.name}" responding to a user review.## App Context- App name: ${appContext.name}- Category: ${appContext.category}- Recent updates: ${appContext.recentUpdates.join(", ") || "None"}- Known issues: ${appContext.knownIssues.length > 0 ? appContext.knownIssues.join(", ") : "None"}## Review to respond to- Rating: ${review.rating} star(s)- Title: ${review.title || "(no title)"}- Body: ${review.body}- Territory: ${review.territory}## Instructions1. Classify the sentiment: positive / negative / neutral / mixed2. Summarize the main issue in one sentence if there is one (null otherwise)3. Write a developer response with these principles: - Language: ${responseLanguage} - Length: 80–200 words - Tone: genuine, warm, never defensive or salesy - Positive review → thank them, share what's coming next - Bug report → acknowledge the issue, invite them to contact support@appname.com - Feature request → thank them, say you've noted it - Emotional review → absorb the frustration without becoming defensive4. Set confidence (0.0–1.0) based on how appropriate and safe the response is5. Set shouldAutoSend to true only if confidence ≥ 0.85 AND there's nothing ambiguous or potentially inflammatoryRespond in JSON only:{ "sentiment": "positive" | "negative" | "neutral" | "mixed", "mainIssue": "one-sentence summary or null", "suggestedResponse": "the response text", "confidence": 0.0–1.0, "shouldAutoSend": true | false}`; const response = await fetch( `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${geminiApiKey}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }], generationConfig: { responseMimeType: "application/json", temperature: 0.3, maxOutputTokens: 512, }, }), } ); if (\!response.ok) { throw new Error(`Gemini API error: ${response.status}`); } const data = (await response.json()) as { candidates: Array<{ content: { parts: Array<{ text: string }> }; }>; }; const text = data.candidates[0]?.content.parts[0]?.text; if (\!text) throw new Error("Gemini API returned empty response"); try { return JSON.parse(text) as ReviewAnalysis; } catch { // Fallback: safe default that requires manual review return { sentiment: "neutral", mainIssue: null, suggestedResponse: "Thank you for taking the time to share your feedback. We're always working to improve the app and your input means a lot to us.", confidence: 0.3, shouldAutoSend: false, // Never auto-send when parsing fails }; }}function getResponseLanguage(territory: string): string { const map: Record<string, string> = { JPN: "Japanese", USA: "English", GBR: "English", AUS: "English", CAN: "English", DEU: "German", FRA: "French", KOR: "Korean", CHN: "Simplified Chinese", TWN: "Traditional Chinese", }; return map[territory] ?? "English";}export { analyzeAndRespond };
Cloudflare Workers: Putting It All Together
Here's the full Worker that ties everything together, with Cloudflare KV for state management and a REST API for the Rork dashboard.
With the backend running, the Rork dashboard is the piece that makes daily use practical. This is a personal-use app — no App Store submission needed. Install it on your device via Rork Companion.
Use this prompt in Rork:
Create a Review Management Dashboard app with:
## Main Screen
- Three tabs: "Needs Review", "Approved", "Auto-Sent"
- Review cards showing: star rating (★ display), review body (max 3 lines),
sentiment badge (Positive=green / Negative=red / Neutral=gray),
territory code, date
- Swipe left to dismiss, swipe right to approve and send
## Review Detail Screen
- Full review text
- AI-generated response in an editable TextInput
- "Approve & Send" button (green, primary) and "Dismiss" button (red, destructive)
- Confidence score as percentage badge
## API Configuration
- Base URL: https://your-worker.workers.dev (stored as WORKER_URL env var)
- GET /api/reviews — fetch pending reviews
- POST /api/approve — send {reviewId, platform, response}
- POST /api/dismiss — send {reviewId}
## Design
- Dark mode support
- Sentiment-based color coding throughout
- FlatList with pull-to-refresh
- Empty state illustration for each tab
Three problems that actually happened during development of this system.
1. App Store Connect JWT Expires Mid-Batch
App Store Connect API JWTs are valid for 20 minutes maximum. Processing 30+ reviews in a single batch can easily exceed that. The failure mode is subtle — you get a 401 halfway through, with no obvious indication that it's a token expiry issue rather than a permissions problem.
// ❌ Generate once and reuse indefinitelyconst token = await generateAppStoreToken(config);for (const review of largeReviewList) { await processReview(review, token); // 401 error partway through}// ✅ Refresh every 10 reviewslet token = await generateAppStoreToken(config);for (let i = 0; i < reviews.length; i++) { if (i > 0 && i % 10 === 0) { token = await generateAppStoreToken(config); // Refresh before expiry } await processReview(reviews[i], token);}
Setting the JWT expiry to 19 minutes (instead of 20) adds a buffer that prevents edge-case failures when requests take slightly longer than expected.
2. Gemini Occasionally Returns Malformed JSON
Even with responseMimeType: "application/json" set, complex prompts occasionally produce output that fails JSON parsing. The fix is always the same: wrap JSON.parse() in a try/catch and return a safe fallback.
The critical rule in the fallback: never set shouldAutoSend: true. When you can't trust the output, you want human eyes on it before anything goes out.
try { return JSON.parse(text) as ReviewAnalysis;} catch { return { sentiment: "neutral", mainIssue: null, suggestedResponse: "Thank you for your feedback — we're always working to improve the app.", confidence: 0.3, shouldAutoSend: false, // Non-negotiable: manual review when uncertain };}
Keeping temperature at 0.3 significantly reduces the frequency of malformed output. Above 0.7, you'll see it often enough to be annoying.
3. Google Play Developer API Has Up to 24-Hour Permission Propagation
After creating a service account in Google Cloud and granting permissions in Google Play Console, the API may still return 403 errors for up to 24 hours. This isn't a code problem — it's propagation delay between the two Google systems.
If you've verified both the service account creation (Cloud Console) and the permission grant (Play Console) but still get 403s, wait a full day before concluding there's something wrong. I spent half a day debugging what turned out to be a waiting problem.
4. KV Key Design Matters More Than It Seems
The state machine here has four states: pending:, approved:, dismissed:, auto: (auto-sent). Designing the key namespace upfront makes list() prefix filtering fast and prevents confusion when you're debugging state transitions. Without a clear schema, you end up with orphaned keys and unclear state.
One practical tip: set consistent expirationTtl values across all keys. 30-day expiry for final states (approved/dismissed) and 7-day expiry for pending items works well. It keeps the KV namespace from growing indefinitely.
Seven Operational Traps That Aren't in the Docs
Here's the part you don't get from Apple's or Google's reference material — the judgment calls that only show up after you've shipped a system like this to production. I'll share the ones that bit me on day one and the edge cases that took a month of running before they surfaced.
1. JWT clock skew causes intermittent 401s on Workers
App Store Connect's JWT requires exp within 20 minutes of the current time. I generate the JWT at the Cloudflare Workers edge, but Workers' server clock and Apple's auth server's clock can drift by a few hundred milliseconds to a few seconds. If you compute iat directly from Date.now(), you'll occasionally generate a token that Apple sees as "issued in the future" — and it returns 401. Setting iat = Math.floor(Date.now() / 1000) - 5 (intentionally backdating by 5 seconds) eliminated the 401s entirely across three months of production runs.
2. Google Play API quotas aren't really documented
The docs say "standard quota," but empirically you'll hit quotaExceeded once you exceed roughly 60 requests per minute. The catch: quota is per service account, not per app. If you're polling reviews across five apps from the same service account in parallel, you'll burn through quota fast. The fix is to stagger cron triggers per app by 20 seconds — express it in your cron expressions rather than relying on rate limiting at the request level.
3. Don't trust Apple/Google's locale field alone for language detection
reviewerNickname or locale may be ja, but the body might be written in English (a Japanese user living abroad, for instance). I use Intl.Segmenter to tally hiragana/katakana/kanji ratios in the body. If they're under 30%, I reply in the body's actual language rather than the declared locale. This eliminated the "replied to a Japanese user in English" mistakes that happen with naive locale-based routing.
4. Force a three-part structure for 1–2 star replies
I embed three of my best historical responses in the few-shot prompt and force response_template: "apology → concrete_action → gratitude" via responseSchema. Even Gemini 2.0 Flash produces stable tone with this constraint. Dropping temperature to 0.4 also helps. The default of 1.0 occasionally slips in over-apologetic phrasing like "We deeply regret causing such fatal inconvenience" that feels off in English-speaking markets.
5. Confidence threshold 0.85 is the sweet spot in my experience
At 0.90, auto-send rate drops below 20% and the approval queue piles up. At 0.80, you reach 60% auto-send but get one or two "I really didn't want to send that" replies per month. At 0.85, I land at 40–45% auto-send with effectively zero misfires. Your app's category will shift the sweet spot, so my recommendation is to start at 0.95 for the first two weeks and step down by 0.05 weekly until you find your tolerance.
6. Use a 7-day TTL on pending: keys
I ran without TTL initially and accumulated about 1,200 unhandled pending: keys over three months. They resurfaced en masse during an unrelated app update and caused real confusion. Setting expirationTtl: 60 * 60 * 24 * 7 means anything left untouched for a week gets silently dropped. Important items get promoted to an important: prefix that persists.
7. Carry reply-language continuity across re-reviews from the same user
If a user updates their review later, replying in a different language than last time feels jarring. I keep replied_lang:{authorName} in KV with a 90-day TTL and include "you previously replied to this user in Japanese" in the Gemini prompt. It's a tiny thing, but anecdotally my re-review rate (the rate at which a user appends to their original review) went up roughly 1.4x after adding this.
Design Choices That Matter at 50M-Download Scale
This section is for indie developers running multiple apps in parallel. My portfolio includes wallpaper apps, ambient-content apps, and manifestation-themed apps, generating about 1,200–1,800 new reviews per month combined. A naive single-app implementation breaks down here, so a few specific design choices became necessary.
Use Durable Objects to isolate per-app state
Vanilla Workers hits read-modify-write race conditions on KV when multiple apps process reviews concurrently. Routing through a Durable Object keyed by app ID serializes review processing per app and keeps the pending:/processed: state consistent. Costs land under $0.20/month for typical indie-scale write volumes.
Two-dimensional cron staggering: app × language
My production cron fires on */15 * * * *. Inside the handler, I split apps into five groups via (minute % 5) and split into Japanese / English / other-language fetches via (minute % 3). This spreads API consumption evenly and made quotaExceeded errors disappear from my logs.
Couple to a Crashlytics × Claude in Chrome auto-fix pipeline
I recently built a separate pipeline that pulls the latest Crashlytics crashes and uses Claude in Chrome to drive Xcode through fix patches. Cross-referencing this system with that one via crashHash lets "crashes on launch" reviews branch automatically into three reply patterns:
Crashlytics state
Context passed to Gemini
Reply direction
First detected within 24h
"Engineering team currently investigating root cause"
Acknowledge investigation honestly; request repro details if user replies
Fixed, not yet released
"Fixed; rolling out in v2.1.0 next week"
Quote concrete version and release window
Already shipped
"v2.1.0 ships the fix"
Direct user to store update; offer support contact for recurrence
Replying "could you tell me which screen crashed?" to a crash report when you already know about the crash from Crashlytics is rude — you're asking the user to do diagnostic work you've already done. Once I wired in the Crashlytics correlation and could lead with "v2.1.0 already ships the fix," the rate at which 1–2 star reviews get edited up to 5 stars went up roughly 1.8x compared to before.
Keeping the Handcraft Feel in AI-Drafted Replies
This is less about technology and more about posture. Both of my grandfathers were temple carpenters in Japan. As a kid I watched them sharpen and oil their tools, and the felt sense that "moving your hands is itself a kind of devotion" never left me. Once you let AI draft your replies, the question of how to preserve that handcraft feel becomes — for me at least — as important as any code-level quality concern.
A few concrete habits I've ended up with:
First, the historical replies I embed in the few-shot prompt are personally curated. I only include five replies that I feel still carry the right temperature. Anything I wrote on autopilot gets excluded. This alone changes Gemini's output noticeably.
Second, even on auto-sent replies, I run a small post-processing pass to normalize tone signals: standardize emoji choices (a 🙏 to match the app's tone, for instance), normalize Japanese wave dashes, fix smart-quote inconsistencies. It's five lines of regex at the end of the Worker, but the cumulative effect on brand voice is real.
Third, I added a deliberate 3-second delay on the approval button in the Rork dashboard. The button stays grayed out for three seconds after the screen renders, then activates. This is a safety I built against my own reflexes — once you fall into tapping "approve" instinctively, AI's mistakes go out the door unedited. Three seconds is small, but it produces just enough friction for "wait, this one isn't quite right" to surface.
For me, designing the technical layer alongside these posture-level details is how I keep the work honest — both to the apps as artifacts and to the users on the other end.
Case Study: Turning a 1-Star Review Into a 5-Star Reply in 24 Hours
To close, here's a concrete example from production. In March 2026, one of my wallpaper apps received this 1-star review: "After updating to iOS 17 the app crashes immediately on launch. I'd like a refund." With permission and personal details removed, here's what the pipeline did:
Cloudflare Workers cron fetched new reviews from App Store Connect API on the 15-minute tick
Gemini classified the review as sentiment: negative, category: crash_report, urgency: high
The Crashlytics API was queried by appVersion + iOS 17 and the matching crash was found (first detected within the last 24 hours)
KV indicated that a fix patch was already applied to an unreleased build (Claude in Chrome had driven the Xcode fix the previous day)
Gemini received the context "crash is recognized; v2.4.1 ships the fix; submitting to App Review within 24 hours" and drafted a reply
Confidence came back at 0.78 — below the auto-send threshold — so the reply landed in my phone's notification queue
From notification to approval-tap took about 90 seconds: open the screen, wait the deliberate 3-second delay, read the draft, tap approve. The reply itself was straightforward — acknowledged the crash, gave a concrete release window, apologized for the inconvenience. The next day, the user updated their rating to 5 stars and appended "thanks for the quick response."
What I took away from this case was that, more than getting the technical content right, what matters is communicating "I see your problem" quickly. AI-drafted replies make that speed possible. My role, I've come to think, is to add the final layer of warmth and approve.
Three Months of Production Results
Some honest data from running this system:
Time saved: Daily review handling dropped from ~32 minutes to ~8 minutes. The biggest gains came from non-English reviews (no translation overhead) and bug-report responses (no need to write the same "we'll look into this, please contact support" message manually).
Auto-send rate: With confidence threshold at 0.85, roughly 40–45% of reviews get auto-sent. Emotionally charged one-star reviews and complex bug reports consistently fall below the threshold and land in the manual queue — which is exactly the right behavior.
API costs: Gemini 2.0 Flash API costs run about $1–2 per month for 30 reviews per day. Essentially free at this scale.
One thing that surprised me: Reviews with very short bodies (1–3 words like "great app!" or "broken") generate low-confidence outputs. Gemini doesn't have enough context. These end up in the manual queue, which is fine — they're also the easiest to write manually.
Thank you for reading through to the end. From my own experience, the whole pipeline took about three weeks to assemble, but the first step is much smaller than that.
The one thing worth doing first is generating an App Store Connect API key and confirming that a single JWT-authenticated request returns 200 from the reviews endpoint on Cloudflare Workers locally. Once that works, the rest is "assembling parts that already work." If you get a 401 on your first attempt, remember the JWT clock-skew section above — backdating iat by five seconds is almost always the fix.
After auth is working, run the Gemini review-analysis module against three of your existing reviews. Just three. Reading the drafted replies is the fastest way to develop a feel for whether this pipeline is worth your time. For me, that calibration moment was what unlocked the motivation to finish the rest of the system.
The final step is generating the Rork dashboard and getting it onto your phone. From there, the daily flow becomes: notification arrives, open the screen, wait the deliberate three seconds, tap approve. Review responses shift from "thinking work" to "reading-the-user's-temperature work." It's a subtle distinction that's hard to convey in writing — you have to feel it.
I've come to think of this system less as a tool that saves 24 minutes per day and more as a tool that returns to me the time and attention needed to keep warmth in my replies. If that framing resonates with anyone else running indie apps, I'd be glad to have helped.
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.