●MAX — Rork Max generates native Swift for iPhone, iPad, Apple Watch, Apple TV, and Vision Pro, with 2-click App Store publishing and no Xcode required●STACK — Standard Rork builds cross-platform mobile apps with React Native (Expo); choosing between the two by use case is the key decision●FOCUS — Unlike web-first tools such as Bolt or Lovable, Rork specializes in native iOS and Android app generation●BUGS — A hands-on review reports Rork resolved about 70% of bugs without manual help, with the remaining 30% needing edits in the exported codebase●FUNDING — Rork raised $2.8M from a16z (Andreessen Horowitz)●PRICING — It is free to start, with paid plans from $25/month, so you can try before committing●MAX — Rork Max generates native Swift for iPhone, iPad, Apple Watch, Apple TV, and Vision Pro, with 2-click App Store publishing and no Xcode required●STACK — Standard Rork builds cross-platform mobile apps with React Native (Expo); choosing between the two by use case is the key decision●FOCUS — Unlike web-first tools such as Bolt or Lovable, Rork specializes in native iOS and Android app generation●BUGS — A hands-on review reports Rork resolved about 70% of bugs without manual help, with the remaining 30% needing edits in the exported codebase●FUNDING — Rork raised $2.8M from a16z (Andreessen Horowitz)●PRICING — It is free to start, with paid plans from $25/month, so you can try before committing
Managing Store Metadata as Code with the App Store Connect API — Turning Manual Edits into a Monthly System
As the apps you ship with Rork pile up, the time spent hand-editing store descriptions and prices stops being negligible. This walks through managing metadata as code with the App Store Connect API and rolling it out across a dozen apps, including the authentication pitfalls.
Back when I was running about ten apps in parallel, I once burned half a day adding a seasonal campaign line to every app's description. Open App Store Connect, pick the app, switch between each language tab, add a sentence at a fixed spot in the description, save. Repeat for every language, for every app. It is not just tedious; a missed paste or a typo always slips in somewhere.
Once a generative tool like Rork lets you mass-produce apps, this "post-launch operation" becomes the bottleneck. Building got fast, but growing them stayed manual. Here I share a design that manages store metadata as code with the App Store Connect API, turning manual edits into a monthly system.
The boundary where manual edits break down
For two or three apps, by hand is fine. The problem is that as the count grows, the work scales as the product of app count and language count. Run five apps in two languages and one wording change means ten edits.
In my experience, past five apps the manual route stops being worth it. And not only on time. Manual work breeds mistakes, and mistakes become review rejections or broken layouts that steal even more time in the end. So systematizing this is less an efficiency play than an investment in accident prevention.
The first wall of the App Store Connect API is authentication
Almost everyone trips on auth first. The App Store Connect API authenticates with a JWT (JSON Web Token), and the way you build that JWT has fine rules; miss one and you get a 401 with no clue why.
Three points to nail. The token expiry is at most 20 minutes, and a longer value is rejected instantly. The aud (audience) is fixed to appstoreconnect-v1. And swap the issuer ID and key ID and it will not pass.
import jwt from "jsonwebtoken";import fs from "node:fs";// Issue the .p8 private key under "Users and Access > Keys" in App Store Connectconst privateKey = fs.readFileSync(process.env.ASC_KEY_PATH, "utf8");export function makeToken() { const now = Math.floor(Date.now() / 1000); return jwt.sign( { iss: process.env.ASC_ISSUER_ID, // Issuer ID (the team-wide UUID) iat: now, exp: now + 19 * 60, // 19 minutes, margin under the 20-min cap aud: "appstoreconnect-v1", }, privateKey, { algorithm: "ES256", // Not RS256. Must be ES256 header: { kid: process.env.ASC_KEY_ID, typ: "JWT" }, } );}
I set exp to 19 minutes to avoid the accident of crossing 20 minutes in the slim gap between generation and the request landing. I myself set it to exactly 20 and spent half a day on intermittent 401s that appeared only on slow-network days. Forgetting to set the algorithm to ES256 is another classic pitfall. The .p8 key is an elliptic-curve key, so specifying RS256 fails at the signing stage.
✦
Thank you for reading this far.
Continue Reading
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
✦The expiry and scope pitfalls of App Store Connect API JWT auth that trip nearly everyone, with the code that avoids them
✦A minimal-script design that holds descriptions, keywords, and prices in one JSON source and pushes only the diff to a dozen apps
✦Criteria for staged rollout and pre-production verification so a bulk push does not break every app at once
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.
Once auth passes, decide "what is the source of truth." I keep the canonical store metadata in JSON in the repository and treat the API as the destination that reflects it. Stop editing in the dashboard; all edits go against the JSON.
This single source has two benefits. One, the diff lives in Git history; you can trace who changed what and when. Two, putting shared fields in shared means an all-app change like a support URL is a one-line edit. A change that took ten edits in the manual era becomes one.
Push only the diff
Resending every field each run hits rate limits easily and wastes work. I fetch the current values, compare against the JSON, and update only the fields that changed.
async function syncLocale(appId, locale, desired, token) { // Fetch the existing localization const current = await fetchLocalization(appId, locale, token); const changed = {}; for (const key of ["subtitle", "keywords"]) { if (current[key] !== desired[key]) changed[key] = desired[key]; } if (Object.keys(changed).length === 0) { console.log(`skip ${appId}/${locale}: no diff`); return { updated: false }; } await patchLocalization(current.id, changed, token); console.log(`updated ${appId}/${locale}:`, Object.keys(changed).join(", ")); return { updated: true, fields: Object.keys(changed) };}
With diff-only pushes, the run log becomes a record of "what changed today." Among a log full of skip, only a few updated lines stand out, so unintended changes are easy to catch.
Staged rollout so you do not break every app at once
The scary part of a bulk push is that mistakes spread in bulk too. A typo in the keyword JSON reflects to every app at once. To prevent this, I always split the rollout into three stages.
Dry run only. Print the diff but call no API
Canary push of one. Reflect to one representative app and verify in the dashboard
Full push to the rest
async function run(mode) { const plan = await buildDiffPlan(); // diff plan across all apps if (mode === "dry-run") { plan.forEach(p => console.log("DIFF", p.appId, p.fields)); return; } const targets = mode === "canary" ? plan.slice(0, 1) : plan; for (const p of targets) { await applyDiff(p); await sleep(800); // spacing to avoid rate limits }}
The sleep(800) is there because the App Store Connect API returns 429 when too many requests arrive in a short window. Spin a dozen apps through at once and you will hit it for sure. Spacing alone stabilizes it, so making this a non-urgent job is safer in production. I run it overnight and check the log the next morning.
Measure the effect by pairing with operational data
Another benefit of systematizing is keeping changes as records. Because what wording changed when lives in Git, you can later line it up against downloads and conversions. In my case, I could confirm afterward that a specific app's organic traffic moved by about 15% between months when I swapped keywords by season and months when I did not. In the manual era, "what changed when" was vague, so that very verification was impossible.
The same goes for revenue. When you align your AdMob reward path and subscription onboarding copy with the store description, the experience from acquisition to purchase becomes consistent. When the description and the actual in-app experience diverge, day-one drop-off rises, so I strongly recommend managing both from one JSON source.
What to automate, and what a human watches
Reflecting metadata can be automated, but deciding what to write should not be, is my stance. Description appeal and keyword selection are decisions tied directly to downloads, and that is the part a human should think through.
Let automation carry only this: reflecting the decided content accurately, completely, and with a record. High-impact operations like price changes keep the step of viewing the dry-run diff with my own eyes before the real push. A system exists to make work fast, not to take over judgment, is the line I draw.
Landing it as a monthly routine
Finally, deciding on a habit keeps it going. At the start of each month, I edit that month's campaign copy and prices into the JSON, then run dry run, canary, full push in that order. Even with ten apps the real work is a dozen minutes, and nothing gets missed.
Compared with the half-days of the manual era, the freed-up time goes back into improving the apps themselves or building new ones. Systematizing operations is unglamorous, but if you keep growing your app count as an indie developer, it is an investment that pays off somewhere. Thank you for reading.
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.