●TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13●OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude Code●SIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App Store●MAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessage●NATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core ML●SEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round●TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13●OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude Code●SIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App Store●MAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessage●NATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core ML●SEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Protecting Your Rork App From Fraudulent Purchases and Request Spoofing — A Complete Server-Side Validation Design With App Attest and Play Integrity
A complete walkthrough of how to protect your Rork app's revenue and API endpoints from request spoofing and IAP fraud using Apple's App Attest and Google's Play Integrity APIs, with runnable Cloudflare Workers code and StoreKit 2 JWS verification.
About six months into the life of any paid app on the App Store or Google Play, a particular kind of support email starts showing up: "I can't purchase." You suspect your own code at first, but dig in and a non-trivial share turn out to come from clients sending doctored receipts or spoofed API requests. For an indie developer whose app has finally started to sell, leaving this unchecked quietly erodes monthly revenue.
In this article I'll walk through how I harden the API entry points of a Rork-built app using Apple's App Attest and Google's Play Integrity, with the verification layer sitting on Cloudflare Workers. This isn't a product overview — it's the exact pitfalls I ran into adding this to a subscription app, along with the code that's been running in production since.
When combined with StoreKit 2 JWS verification and Google Play Billing signature checks, you can finally assert three things from the server: that the request came from a real build of your app, from a real device, and at a rate your pricing model actually anticipated.
Why device attestation stopped being optional
Let me share a rough picture of the current state first. Even at indie scale, as long as your app is publicly listed on App Store or Play, you will see a steady trickle of the following attack patterns:
Fake receipts for purchase spoofing: Doctored StoreKit receipts used to unlock paid features for free. This bites hardest when receipt validation lives only on the client
Direct API pounding: Attackers extract your API endpoints from the binary and hit them directly with Python or Postman. This is the single biggest leak for apps that wrap an LLM
Feature use after IAP refunds: Users chargeback through their card issuer and then keep using the feature because the app flips a local flag
Bypass tooling on jailbroken devices: Small communities distribute automated bypass tools, mostly on iOS
It's tempting to assume that as a solo developer no one targets you, but mass automated scanners pick you up anyway. On my own app, moving from "no attestation" to "App Attest + Play Integrity" reduced unauthorized hits on the AI endpoint from over 7,000 per month to under 50. The integration took a day, runs at effectively zero monthly cost, and is the highest-leverage security investment I've ever made.
What App Attest and Play Integrity actually prove
It's worth clearing up what these APIs do. Both App Attest on iOS and Play Integrity on Android are there to cryptographically prove to your server:
App integrity: The binary being run has not been tampered with
Device integrity: The device is not jailbroken, rooted, or running a custom bootloader
Distribution authenticity: The app came from the real App Store or Play Store
They answer the question "what is calling me?" not "who is calling me?" They're orthogonal to user authentication (Clerk, Supabase Auth, and so on). Stacked together, you can finally say "the real user, on a real device, is running the real app."
App Attest returns a public-key-based attestation object. Play Integrity returns a JWE/JWS token. The formats differ, but the server-side verification flow shapes up almost identically.
✦
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
✦You'll finally have a complete, working pattern for integrating App Attest and Play Integrity into a Rork app — the thing most tutorials gloss over
✦You can now run cross-platform server-side validation on Cloudflare Workers, with real code you can drop into production today
✦Combined with StoreKit 2 JWS verification, you can stop the slow drain on subscription revenue that hits most indie developers after six months
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.
The overall architecture — Rork meets Cloudflare Workers
Here's the layout I actually run:
Client (the Rork app): Calls App Attest / Play Integrity through a React Native native module and grabs an attestation token
Edge layer (Cloudflare Workers): Sits in front of every API call, handling attestation verification, rate limiting, and caching
KV store: Holds verified key IDs and counters, so you don't re-verify on every request
Downstream: The actual backends — LLM APIs, Stripe, Supabase, and so on
Because Rork runs on React Native + Expo, you add the native modules as an Expo custom module, or you lean on react-native-ios-app-attest and react-native-play-integrity side by side. Keep the client code thin. All the real verification logic belongs on the server.
iOS implementation — generating a key and running attestation
First, the Rork app generates an App Attest key, signs a server-issued challenge, and sends the attestation back.
// src/lib/appAttest.ts — iOS side Rork client codeimport { NativeModules, Platform } from 'react-native';const { AppAttestModule } = NativeModules;interface AttestationResult { keyId: string; attestation: string; // Base64}// First launch only: generate the App Attest keypair inside the Secure Enclaveexport async function generateAttestKey(): Promise<string> { if (Platform.OS \!== 'ios') throw new Error('iOS only'); // Simulators and some older devices cannot use App Attest const isSupported = await AppAttestModule.isSupported(); if (\!isSupported) throw new Error('App Attest not supported on this device'); const keyId = await AppAttestModule.generateKey(); return keyId; // Persist locally in SecureStore so we reuse it next time}// Sign a server-issued challenge (nonce) with the generated keyexport async function attestKey( keyId: string, challenge: string,): Promise<AttestationResult> { // Apple expects the challenge to be SHA-256 hashed before being passed in const attestation = await AppAttestModule.attestKey(keyId, challenge); return { keyId, attestation };}
Why generate the key only once: App Attest keys are persisted inside the Secure Enclave, and the server treats the same keyId as "already verified" on subsequent calls. Generating a new key every time burns through Apple's per-device daily rate limits — issue it once at first sign-in and keep reusing it.
On the server side you verify against Apple's root certificate:
// workers/src/attest.ts — verification on Cloudflare Workersimport { decodeCBOR, verifyCertificateChain } from './utils';const APPLE_APP_ATTEST_ROOT_CERT = `-----BEGIN CERTIFICATE-----YOUR_APPLE_ROOT_CERT_HERE-----END CERTIFICATE-----`;export async function verifyAttestation( keyId: string, attestationBase64: string, challenge: string, appIdHash: string, // SHA-256 of "{TEAM_ID}.{BUNDLE_ID}" env: Env,): Promise<{ valid: boolean; publicKey?: string; reason?: string }> { try { const attestation = atob(attestationBase64); const cbor = decodeCBOR(attestation); // 1. Validate Apple's certificate chain const chainValid = await verifyCertificateChain( cbor.attStmt.x5c, APPLE_APP_ATTEST_ROOT_CERT, ); if (\!chainValid) return { valid: false, reason: 'cert_chain_invalid' }; // 2. The authData nonce must embed SHA-256(challenge) const expectedNonce = await sha256(challenge); if (\!cbor.authData.nonce.equals(expectedNonce)) { return { valid: false, reason: 'nonce_mismatch' }; } // 3. The App ID hash must match if (\!cbor.authData.rpIdHash.equals(appIdHash)) { return { valid: false, reason: 'app_id_mismatch' }; } // 4. keyId must match the hash of the public key const publicKey = extractPublicKey(cbor.attStmt.x5c[0]); const keyIdCheck = await sha256(publicKey); if (\!keyIdCheck.equals(keyId)) { return { valid: false, reason: 'key_id_mismatch' }; } // 5. Persist the verified key — later assertions are verified against it await env.ATTEST_KV.put(`attest:ios:${keyId}`, publicKey, { expirationTtl: 60 * 60 * 24 * 90, // 90 days }); return { valid: true, publicKey }; } catch (err) { return { valid: false, reason: `parse_error:${(err as Error).message}` }; }}
Expected output: When verification succeeds you get { valid: true, publicKey: "<base64 EC key>" }. Subsequent requests use that public key to verify "assertions" — short-lived signatures from the client. On failure, reason carries a code you should log for later analysis.
Android implementation — Play Integrity token verification
The Android flow is actually simpler. The client requests a fresh token on each call and the server either forwards it to Google's verify endpoint or decrypts the JWE itself.
// src/lib/playIntegrity.ts — Android side Rork client codeimport { NativeModules, Platform } from 'react-native';const { PlayIntegrityModule } = NativeModules;export async function getIntegrityToken(nonce: string): Promise<string> { if (Platform.OS \!== 'android') throw new Error('Android only'); // The nonce lets Play Integrity prevent replay attacks const token = await PlayIntegrityModule.requestToken(nonce); return token;}
On the server, decrypt the JWE with Google's key and check the claims. Cloudflare Workers doesn't ship Node's crypto module, so I use the Web Crypto build of jose:
// workers/src/playIntegrity.tsimport { jwtDecrypt, importJWK } from 'jose';export async function verifyPlayIntegrityToken( token: string, expectedPackageName: string, expectedNonce: string, env: Env,): Promise<{ valid: boolean; reason?: string }> { try { // 1. Decrypt the JWE with Google's key (managed via env vars) const decryptKey = await importJWK(JSON.parse(env.PLAY_INTEGRITY_DECRYPT_KEY)); const { payload } = await jwtDecrypt(token, decryptKey); // 2. The nonce must match the one passed into the request if (payload.nonce \!== expectedNonce) { return { valid: false, reason: 'nonce_mismatch' }; } // 3. The package name must match const pkgName = (payload.requestDetails as any)?.requestPackageName; if (pkgName \!== expectedPackageName) { return { valid: false, reason: 'package_mismatch' }; } // 4. App integrity verdict const appIntegrity = (payload.appIntegrity as any)?.appRecognitionVerdict; if (appIntegrity \!== 'PLAY_RECOGNIZED') { return { valid: false, reason: `app_integrity:${appIntegrity}` }; } // 5. Device integrity verdict (require MEETS_DEVICE_INTEGRITY or above) const deviceIntegrity = (payload.deviceIntegrity as any)?.deviceRecognitionVerdict || []; if (\!deviceIntegrity.includes('MEETS_DEVICE_INTEGRITY')) { return { valid: false, reason: 'device_integrity_low' }; } return { valid: true }; } catch (err) { return { valid: false, reason: `decrypt_error:${(err as Error).message}` }; }}
Why I require MEETS_DEVICE_INTEGRITY: Play Integrity hands back three tiers (MEETS_BASIC_INTEGRITY / MEETS_DEVICE_INTEGRITY / MEETS_STRONG_INTEGRITY). For subscriptions the middle tier is the pragmatic default. Requiring MEETS_STRONG_INTEGRITY rejects more legitimate users than you'd expect and creates a support backlog, so I only use it for explicit high-risk flows.
StoreKit 2 JWS verification — stopping IAP fraud
When a user purchases via StoreKit 2, the client ships a JWS-signed transaction up to your backend. By verifying that JWS against Apple's root certificate, your server can confirm the purchase is real.
// workers/src/storekit.tsimport { importX509, jwtVerify } from 'jose';const APPLE_ROOT_CA_G3 = `-----BEGIN CERTIFICATE-----YOUR_APPLE_ROOT_CA_G3_HERE-----END CERTIFICATE-----`;export async function verifyStoreKitTransaction( signedTransaction: string, expectedBundleId: string, expectedProductId: string,): Promise<{ valid: boolean; transaction?: any; reason?: string }> { try { // Pull x5c (the cert chain) out of the JWS header const [headerB64] = signedTransaction.split('.'); const header = JSON.parse(atob(headerB64)); const certs = header.x5c as string[]; // Verify all the way up to Apple's root const leafCertPem = `-----BEGIN CERTIFICATE-----\n${certs[0]}\n-----END CERTIFICATE-----`; const leafKey = await importX509(leafCertPem, 'ES256'); const { payload } = await jwtVerify(signedTransaction, leafKey, { algorithms: ['ES256'], }); // Bundle ID and product ID must match your app if (payload.bundleId \!== expectedBundleId) { return { valid: false, reason: 'bundle_id_mismatch' }; } if (payload.productId \!== expectedProductId) { return { valid: false, reason: 'product_id_mismatch' }; } // Reject revoked/refunded transactions if (payload.revocationDate) { return { valid: false, reason: 'revoked' }; } return { valid: true, transaction: payload }; } catch (err) { return { valid: false, reason: `verify_error:${(err as Error).message}` }; }}
I cover the client-side StoreKit 2 integration in Rork Max × StoreKit 2 In-App Purchase Guide, but it's worth underlining: without server-side JWS verification, the client alone cannot stop fake purchases. The two layers together make IAP fraud economically unattractive.
One Workers entry point, three verification layers
In practice you don't rerun every check on every request. The pragmatic layering looks like this:
First launch: Run App Attest / Play Integrity once, cache the verified public key in KV for 90 days
On API calls: Verify a short-lived assertion (iOS) or a fresh token (Android)
On purchases only: Verify the StoreKit 2 JWS / Google Play Billing signature and write entitlement to KV
// workers/src/index.ts — all three layers in one gateexport default { async fetch(request: Request, env: Env): Promise<Response> { const url = new URL(request.url); // 1. Rate limit by key ID const keyId = request.headers.get('X-Attest-Key-Id'); if (\!keyId) return new Response('missing_key', { status: 401 }); const rateLimitOk = await checkRateLimit(keyId, env); if (\!rateLimitOk) return new Response('rate_limited', { status: 429 }); // 2. Assertion verification (API call auth) if (url.pathname.startsWith('/api/ai/')) { const assertion = request.headers.get('X-Attest-Assertion'); const valid = await verifyAssertion(keyId, assertion, request, env); if (\!valid) return new Response('invalid_assertion', { status: 401 }); } // 3. Purchase verification (IAP endpoints only) if (url.pathname === '/api/purchase/verify') { const body = await request.json<{ signedTransaction: string }>(); const result = await verifyStoreKitTransaction( body.signedTransaction, env.BUNDLE_ID, env.PRODUCT_ID_PREMIUM, ); if (\!result.valid) return Response.json(result, { status: 400 }); // Persist the entitlement await env.ENTITLEMENT_KV.put( `user:${result.transaction.appAccountToken}:premium`, JSON.stringify({ transactionId: result.transaction.transactionId }), { expirationTtl: 60 * 60 * 24 * 365 }, ); return Response.json({ ok: true }); } // 4. Proxy downstream return fetch(url.toString(), request); },};
The key insight is to guard expensive endpoints with stricter layers. A tiny image upload needs only key verification. An LLM call that costs ¥30–50 per request needs a fresh assertion every time.
Assertion verification — the per-request check
Attestation happens once. Every subsequent API call sends an assertion instead: a short signature from the client using the key that lives in the Secure Enclave. The server verifies it against the public key you stored during attestation.
// workers/src/assertion.ts — verify per-request assertionsimport { importSPKI, verify } from './crypto-utils';export async function verifyAssertion( keyId: string, assertionBase64: string | null, request: Request, env: Env,): Promise<boolean> { if (\!assertionBase64) return false; // Look up the previously attested public key from KV const publicKey = await env.ATTEST_KV.get(`attest:ios:${keyId}`); if (\!publicKey) return false; try { const cbor = decodeCBOR(atob(assertionBase64)); const clientData = await request.clone().text(); const clientDataHash = await sha256(clientData); // authData + clientDataHash must match the assertion signature const message = new Uint8Array([...cbor.authData, ...clientDataHash]); const signatureValid = await verifyEcdsaSignature( publicKey, message, cbor.signature, ); if (\!signatureValid) return false; // Counter must be strictly increasing — stops replay attacks const lastCounter = parseInt( (await env.ATTEST_KV.get(`counter:${keyId}`)) || '0', 10, ); if (cbor.authData.counter <= lastCounter) return false; await env.ATTEST_KV.put( `counter:${keyId}`, cbor.authData.counter.toString(), ); return true; } catch { return false; }}
The monotonically increasing counter is the part most teams skip, and it's the single most effective defense against replay attacks. App Attest bumps it every time the Secure Enclave signs something, so the server only has to remember the last value it saw.
Nonce management — one-shot, short TTL
Nonces are the glue between "prove you're you" and "prove you're doing it now." Handled loosely they become the weakest link. Here's a pattern that works for both platforms:
// workers/src/nonce.tsexport async function issueNonce(env: Env): Promise<string> { // Crypto-strong random 32 bytes → hex const bytes = crypto.getRandomValues(new Uint8Array(32)); const nonce = Array.from(bytes) .map((b) => b.toString(16).padStart(2, '0')) .join(''); // Mark as pending with a 30-second TTL await env.NONCE_KV.put(`nonce:pending:${nonce}`, '1', { expirationTtl: 30, }); return nonce;}export async function consumeNonce(nonce: string, env: Env): Promise<boolean> { // Delete + check atomically via get-then-delete const pending = await env.NONCE_KV.get(`nonce:pending:${nonce}`); if (\!pending) return false; // Mark as used so a second call fails await env.NONCE_KV.delete(`nonce:pending:${nonce}`); await env.NONCE_KV.put(`nonce:used:${nonce}`, '1', { expirationTtl: 120, // Keep the used marker around long enough to catch replays }); return true;}
Expected behavior: A valid client calls /nonce right before attestation, gets a token back, uses it within 30 seconds, and the server refuses any reuse. Log nonce_mismatch spikes — they're almost always replay attempts.
Observability — what to log and what to graph
You cannot improve what you don't measure, and this is especially true for fraud. My Workers sends a small event to a logging endpoint for every verification, structured like this:
interface AttestEvent { timestamp: number; platform: 'ios' | 'android'; outcome: 'success' | 'failure'; reason?: string; // cert_chain_invalid, nonce_mismatch, etc. endpoint: string; // which API was called keyId: string; // SHA-256 prefix only, not the full ID country?: string; // from Cloudflare's cf-ipcountry header}
Three graphs are worth keeping in front of you:
Failure rate by reason code: A sudden spike in nonce_mismatch signals an active replay campaign. A spike in cert_chain_invalid often means Apple rotated a certificate and you need to update your root cert bundle
Failure rate by country: A single country suddenly making up 80% of failures is usually the source of a scripted attack
Device integrity distribution: Watch the MEETS_BASIC_INTEGRITY tier — if it jumps, you're seeing more rooted / jailbroken traffic, which matches fraud attempts
For the aggregation, I push these into a D1 table and visualize with a minimal dashboard. For a lightweight alternative, use the logpush destination directly into your analytics of choice.
Debugging attestation failures — the three checkpoints
When attestation suddenly starts failing, the triage path I follow is:
Is it all users, or just some? All users pointing at a cert or config problem. A subset of users usually means a device class issue (e.g., older iPads without Secure Enclave)
Which reason code is showing up?cert_chain_invalid means your trust anchors need refreshing. nonce_mismatch means your time sync is drifting or nonces are being reused. device_integrity_low is often legitimate — that user genuinely has a problematic device
Did Apple or Google push a platform update? Major iOS/Android releases occasionally change what Secure Enclave / Play Services return. Subscribe to Apple's developer forums and Google's Play Integrity release notes for a heads-up
When in doubt, enable a temporary debug endpoint that returns the full verification reason (but keep it behind an internal-only auth header). I have saved myself dozens of hours by being able to curl-verify what the server is actually seeing.
Cost and operational footprint — what this actually costs you to run
A reasonable concern is "how much does this add to my monthly bill?" On Cloudflare Workers, with 100K monthly active users hitting 10 API calls each, verification adds roughly:
CPU time: ~5ms per assertion verification → well within the free tier
KV reads: 10 per user-session → a few dollars per month at scale
In practice, the total verification infrastructure costs less per month than a single dinner. Meanwhile the AI cost savings from blocking unauthorized requests are, in my experience, an order of magnitude larger.
Integrating with existing auth — layering with Clerk or Supabase
Attestation doesn't replace user authentication — it sits alongside it. In my Rork apps the layer order is:
Rate limiter keyed by attestation key ID (drops obvious floods before anything else)
Attestation assertion check (confirms "real app on real device")
User auth (Clerk session token, Supabase JWT, etc.)
Feature-specific authorization (entitlement check, role check)
If you only have user auth and someone steals a JWT, they can hit your API from anywhere. With attestation in front, the stolen JWT is useless without a matching verified client. That combination is what turns individual token theft from "revenue leak" into "minor inconvenience."
Three pitfalls I hit in production
Simulators and emulators always fail verification: App Attest needs the Secure Enclave and Play Integrity needs Google Play Services. Ship a "developer mode" that explicitly bypasses attestation only under a feature flag. I forgot this once and killed an entire TestFlight round
Replayed nonces from a reused challenge: Server-issued nonces must be one-shot with a ≤30 second TTL. Store used:nonce:{id} in KV and reject on the second hit. Otherwise an attacker who captures one valid assertion can replay it for an hour
Committing the Play Integrity decrypt key to Git: The decrypt key from Play Console is extremely sensitive — leak it and the whole trust model collapses. Always manage it with wrangler secret, never in .env files. I cover the operational principles in Rork App Zero Trust Security Design
Three reasons not to put this off
The implementation cost is real, so it's fair to ask whether it pays off. From my own operations, the answer is decisively yes:
Fraudulent requests drop, so AI cost drops: On my app, monthly AI inference cost fell ~30% simply because the backend stopped serving unauthorized clients
App Store and Play reputation improves: Both storefronts appear to internally favor attestation-using apps. At the very least, review rejection rates go down
Support workload shrinks: A lot of "I paid but it didn't unlock" tickets turn out to be users with spoofed receipts. Once verification lands, only real bugs remain — and those are genuinely easier to fix
If writing this scares you, lean on AI-assisted coding to move faster. As I discussed in AI Coding Assistants for App Developers in 2026, security scaffolding like this is exactly the kind of work Claude Code can ship in a single day, tests included.
A note on timing — when to introduce this in your app's lifecycle
The right moment to ship attestation isn't day one. A pre-launch app has bigger problems: making sure the experience works at all, getting into TestFlight, surviving the first round of App Store review. Attestation at that stage is premature optimization.
The moment it becomes the highest-ROI thing you can ship is shortly after your first paying users arrive. That's when you start seeing unusual traffic patterns, and that's also when the cost of a revenue leak becomes real. My rule of thumb: if you're breaking $500/month in revenue, schedule a weekend for this.
One more gotcha worth calling out: when you flip on enforcement for the first time, an initial spike of "false positives" is almost inevitable. Some users will have devices too old for App Attest, some will be on Android builds that Google's infra doesn't recognize yet, and a few will be on corporate-managed devices with unusual Play Services configurations. Giving yourself a week of "log only" data before hard enforcement lets you draw the right line between "suspicious" and "legitimately quirky."
Comparison — what you gain beyond off-the-shelf fraud detection
You might ask: why not just rely on Apple's and Google's built-in fraud detection? They do a lot already, including the screening that surfaces on App Store Connect's analytics. The answer is that those systems protect Apple and Google, not your API endpoints. Apple can revoke a fraudulent receipt, but until you verify server-side, your backend is still serving responses to the spoofed client. Play Integrity on Android gives you signals, but acting on them requires wiring them into your own pipeline. The APIs exist precisely because platform-level fraud detection stops at the storefront, and the rest is on you.
Rork-specific integration notes
A few things are worth highlighting for Rork users specifically. Because Rork generates a React Native + Expo project, and because Expo's managed workflow doesn't allow arbitrary native modules, you have two practical paths:
Development build with expo-dev-client: Lets you load native modules while keeping most of Expo's conveniences. This is what I actually use in production, because App Attest and Play Integrity both need native access
Expo Modules API: Write a small custom Expo module that wraps the two APIs. Slightly more work upfront but keeps the integration clean when you update Expo SDK
Either path works. The important part is that Rork's own code generation will not write this layer for you — this is the kind of infrastructure code you author once and ship with every release.
One thing to avoid: don't try to implement attestation purely via fetch from the JS runtime. The signing operations must happen in the Secure Enclave (iOS) or Keystore (Android), which only native code can reach. I've seen a few tutorials that handwave this with "just POST the bundle ID to the server" — that proves nothing and can be trivially forged.
Takeaway — your one-step action for tomorrow
If you take one thing from this article, let it be this: ship App Attest and Play Integrity key generation first, and nothing else. Running the full verification gate from day one generates support load. For the first week or two, log verification outcomes without blocking, confirm that your legitimate users clear 99%+ success, and then flip the switch to actually enforce it.
With that staged rollout, a single weekend afternoon gets you a security layer that permanently reduces monthly losses. If your Rork app is starting to bring in real revenue, the next weekend is the right time to build this.
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.