RORK LABJP
BUILD — Rork Max runs real Macs in the cloud loaded with Xcode and the iOS SDK, writing SwiftUI, compiling, reading the errors and building again. That loop, not the code generation, is what lifts the outputNATIVE — What comes out is pure Swift and SwiftUI, not React Native. Reaching AR, Metal graphics and widgets that React Native cannot touch is the real gap between this and other buildersPLATFORMS — Coverage spans iPhone, iPad, Apple Watch, Apple TV and Vision Pro, plus iMessage. Worth a look if you want to start from a watch app or an extension rather than a phone screenCOMPANION — The Rork Companion app lets you check a generated build on a real iPhone without a paid Apple Developer account, lowering the bar for trying a first project end to endPRICING — Free to start, paid plans from $25 a month, and Rork Max on the $200 Max plan. Worth working out up front how many projects it takes to earn that backDEADLINE — From August 31, 2026, Google Play requires target API level 36 or higher for new apps and updates alike. Ten days out, and the targetSdkVersion of what you generate is yours to verifyBUILD — Rork Max runs real Macs in the cloud loaded with Xcode and the iOS SDK, writing SwiftUI, compiling, reading the errors and building again. That loop, not the code generation, is what lifts the outputNATIVE — What comes out is pure Swift and SwiftUI, not React Native. Reaching AR, Metal graphics and widgets that React Native cannot touch is the real gap between this and other buildersPLATFORMS — Coverage spans iPhone, iPad, Apple Watch, Apple TV and Vision Pro, plus iMessage. Worth a look if you want to start from a watch app or an extension rather than a phone screenCOMPANION — The Rork Companion app lets you check a generated build on a real iPhone without a paid Apple Developer account, lowering the bar for trying a first project end to endPRICING — Free to start, paid plans from $25 a month, and Rork Max on the $200 Max plan. Worth working out up front how many projects it takes to earn that backDEADLINE — From August 31, 2026, Google Play requires target API level 36 or higher for new apps and updates alike. Ten days out, and the targetSdkVersion of what you generate is yours to verify
Articles/Dev Tools
Dev Tools/2026-07-03Advanced

When Your App Store Connect API Pipeline Quietly Drops Days — Field Notes on JWT Expiry, Report Lag, and Reconciliation

Automated App Store Connect API pipelines rarely stop — they leak. This piece breaks down the three silent failure modes behind 401, 404, and 429, and shows how a fetch ledger, a 72-hour backfill window, and a weekly reconciliation query keep daily sales and review data complete.

App Store Connect API5JWT3Sales ReportAutomation8Supabase33Rate LimitsBackfillRork540

Premium Article

At the start of the month I opened App Store Connect, compared the dashboard totals against the daily sales rows I had been accumulating in Supabase, and the numbers were off by about 3% — three days out of thirty were missing their sales rows entirely.

The daily Slack report had arrived every single morning. That was exactly why I trusted the pipeline. In reality, the fetch for a handful of days had failed quietly, and those rows simply never existed when the month closed.

An automated pipeline that stops is easy to notice. One that leaks is not — as long as notifications keep arriving, the holes stay invisible. These field notes document the gap patterns I actually hit while automating sales and review collection with the App Store Connect API (ASC API), along with the detection and repair mechanisms that fixed them, code included.

Gaps Arrive Wearing Three Different Faces

Auditing a month of execution logs, every missing row traced back to one of three causes.

SymptomHTTP statusRoot causeWhat goes missing
Auth failure401JWT expiry, clock skew, lost newlines in the .p8 keyEverything in that run
Report not ready404Apple's daily report generation laggingSales rows for one specific date
Dropped pages429Rate limiting mid-paginationThe later pages of reviews

What makes these three nasty is that they share one property: swallow the exception and everything works fine again tomorrow. Without retries, only the hole for the failed day remains.

Some groundwork first. The ASC API requires an ES256-signed JWT on every request, with a maximum token lifetime of 20 minutes. The Sales and Trends API returns daily reports as .tsv.gz, but the previous day's report is finalized in the early hours Pacific time — which can slide into the evening or even the next day in other timezones. And the rate limits are not publicly documented. Those three facts map directly onto the 401, 404, and 429 failure modes.

Measure Token Age Instead of Minting Blindly

My first implementation logged nothing more than "got a 401" on failure. That gives you no way to tell whether the token was stale or the key material was broken.

Caching the token together with its issue time, and logging the token's age whenever a request fails, made the diagnosis almost mechanical.

// supabase/functions/_shared/asc-token.ts
import { create } from "https://deno.land/x/djwt@v3.0.2/mod.ts";
 
const ISSUER_ID = Deno.env.get("ASC_ISSUER_ID")!;
const KEY_ID = Deno.env.get("ASC_KEY_ID")!;
const PRIVATE_KEY_PEM = Deno.env.get("ASC_PRIVATE_KEY")!;
 
let cached: { token: string; issuedAt: number } | null = null;
 
// Refresh at 15 minutes — a 5-minute buffer against the 20-minute cap
const REFRESH_AFTER_MS = 15 * 60 * 1000;
 
export async function getAscToken(): Promise<string> {
  const now = Date.now();
  if (cached && now - cached.issuedAt < REFRESH_AFTER_MS) {
    return cached.token;
  }
 
  const pemBody = PRIVATE_KEY_PEM
    .replace("-----BEGIN PRIVATE KEY-----", "")
    .replace("-----END PRIVATE KEY-----", "")
    .replace(/\s/g, "");
  const binaryKey = Uint8Array.from(atob(pemBody), (c) => c.charCodeAt(0));
 
  const privateKey = await crypto.subtle.importKey(
    "pkcs8",
    binaryKey,
    { name: "ECDSA", namedCurve: "P-256" },
    false,
    ["sign"],
  );
 
  const nowSec = Math.floor(now / 1000);
  const token = await create(
    { alg: "ES256", kid: KEY_ID, typ: "JWT" },
    {
      iss: ISSUER_ID,
      iat: nowSec,
      exp: nowSec + 1140, // 19 minutes — one under the cap to absorb clock skew
      aud: "appstoreconnect-v1",
    },
    privateKey,
  );
 
  cached = { token, issuedAt: now };
  return token;
}
 
/** Evidence for deciding whether a 401 was caused by a stale token */
export function tokenAgeMs(): number | null {
  return cached ? Date.now() - cached.issuedAt : null;
}

Setting exp to 1140 seconds rather than the full 1200 protects against the "freshly issued yet already expired" accident that clock skew between your runtime and Apple's servers can produce. In cold-start environments like Edge Functions, key import time widens the gap between iat and the actual request, so that one-minute margin earns its keep.

The other classic 401 cause is the .p8 key losing its newlines. Paste it into a secret store carelessly and you get a key that still parses as PEM but produces broken signatures — a genuinely confusing failure. The reliable fix was procedural: a smoke test that calls /v1/apps once right after every deploy and asserts a 200.

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
How to classify the three silent gap patterns caused by 401, 404, and 429, and detect missing days mechanically with a fetch ledger table
Working code for a 72-hour backfill window that treats Sales API 404s as pending rather than empty, absorbing Apple's report-generation lag
A generate_series reconciliation query for weekly completeness proofs, plus an upsert pattern that keeps review collection idempotent and analysis costs flat
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.

or
Unlock all articles with Membership →
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.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

Dev Tools2026-07-17
The Update That Failed Because a Profile Expired Three Months Ago
Apple signing assets expire quietly and nothing tells you. Here is how to count the days left with the App Store Connect API and put the audit on a weekly Cloudflare Workers cron.
Dev Tools2026-06-23
DAU Went Up but Retention Didn't — Rebuilding Gamification That Actually Sticks in Rork Apps
Points, badges, and leaderboards lift DAU, but retention is a different story. Field notes on a server-authoritative point ledger, streaks that forgive, and leaderboards that don't crush newcomers — with working code for Rork apps.
Dev Tools2026-05-26
Aggregating App Store Connect, RevenueCat, and AdMob into One Morning Digest with Cloudflare Workers — A Six-App Indie Architecture
A practical architecture for indie developers running multiple Rork apps: aggregate App Store Connect API, RevenueCat REST, and AdMob Reporting API in a Cloudflare Workers Cron job and ship a single daily Slack digest each morning.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →