●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
Rork × OpenAI Apps SDK: Ship a ChatGPT-Embedded App From Your Rork Codebase
A complete implementation guide for distributing your Rork app's business logic as a ChatGPT-embedded mini app. Covers MCP server design, UI Resources, shared OAuth, Universal Link conversation handoffs, and pricing parity from a real production lens.
A new distribution channel quietly opened in late 2025: apps that live inside ChatGPT. For years the App Store and Google Play were the two front doors to mobile, and every other user had to be reached through SEO or paid ads. With the OpenAI Apps SDK, you can now drop your product straight into the conversation surface that hundreds of millions of people use daily. The harder questions are the implementation ones: can you reuse the Rork codebase you've already shipped, do auth and billing become a duplicated nightmare, and where exactly does production tend to break? Public docs are accurate but thin on these realities.
This guide walks through the design choices I make when adding an Apps SDK distribution layer on top of an existing Rork app, with code you can paste into a Cloudflare Workers project. The focus is on the parts that hurt later if you skip them — keeping a single user identity across ChatGPT and the mobile app, handing a conversation off mid-flow into the mobile UI, and keeping pricing coherent even though the payment rails are different. I'll also share the specific failures I ran into during my own integration, because the details that don't make it into official docs are usually where you lose a weekend.
Before we start, a quick framing point. Apps SDK is not a replacement for your mobile app. It's a complementary surface that's particularly strong at one thing: low-friction, conversational entry into your product's core actions. The mobile app remains where users do extended editing, deep configuration, and viewing rich media. Designing for that division of labor up front saves you from building two competing products in one codebase.
Designing a Shared Foundation Between Your ChatGPT App and Your Rork App
The first decision is whether to put the Apps SDK server in the same repository as your Rork backend or in a separate project. I prefer one repo with a /mcp route mounted alongside the existing tRPC handlers, because keeping users, billing state, and analytics in one place pays back in operational simplicity. Splitting them feels cleaner on day one, but a few months in you'll be writing sync jobs to keep two databases consistent, and that's wasted complexity.
Concretely, I structure the codebase so that domain logic (creating tasks, searching, updating) lives in a core package, and two thin adapters consume it: one for the mobile app, one for ChatGPT.
packages/
core/ # pure domain logic
mobile-api/ # REST/tRPC endpoints called by the Rork app
apps-sdk/ # MCP server called by ChatGPT
shared/ # auth, billing, user-id types shared by both
The big benefit of this layout is that release cycles diverge cleanly. The Apps SDK side ships when you push to Cloudflare; the mobile side ships when Apple finishes reviewing your build. If those two were tangled in one runtime, your ChatGPT iteration speed would drop to App Store cadence — which is the wrong direction. Conversely, a domain logic change made for the mobile app immediately becomes available to the ChatGPT integration, so you don't have to re-test the same business rule twice.
There's one piece of advice I'd give myself if I were starting over: define the domain types in core first, before you write any tool definitions or screens. The schemas you choose here become the boundary contract for both adapters, and changing them once both adapters are live is painful. A Task type with an id, title, dueDate, priority, and status enum is enough to start; resist the urge to add fields you might "eventually" need.
Standing Up the MCP Server on Cloudflare Workers in 30 Minutes
The Apps SDK speaks the Model Context Protocol (MCP). ChatGPT acts as the MCP client and calls tools you expose. A minimum Cloudflare Workers setup is shorter than you'd expect.
Two things matter most here. First, ship signature verification on day one. OpenAI sends an HMAC in x-openai-apps-signature; store the shared secret in Cloudflare Secrets and verify every request. If you defer this, your MCP endpoint becomes a public RPC for anyone who finds the URL — and someone will, because the manifest you submit lists it.
Second, return both structuredContent and content from every tool. The model uses the structured payload for reasoning later in the conversation; the content array is what users see. I dropped structuredContent early on and watched ChatGPT lose track of which task the user had just created — every follow-up question forced a redundant re-fetch. Returning both from the start avoids that whole class of bug.
A third subtler point: the descriptions you put in the z.string().describe(...) calls are read by the model when deciding whether to call your tool. If your description is "Task title" the model often invokes the tool with vague placeholder text. If it's "The exact phrase the user said they want to remember, copied verbatim from the conversation," the tool gets meaningful arguments. Tool descriptions are prompt engineering disguised as type definitions — treat them seriously.
✦
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
✦If you've been wanting to ship a ChatGPT-embedded experience but didn't know where to begin, you can now start today by reusing your Rork codebase end-to-end
✦You'll walk away with working code for the MCP server, UI Resources, shared OAuth, and Universal Link handoff — the same patterns I run in production to keep one user identity across ChatGPT and the mobile app
✦You'll open a third distribution channel beyond App Store and Google Play, exposing your product directly to the hundreds of millions of people who live inside ChatGPT every day
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.
Reshaping Rork's React Components Into ChatGPT UI Resources
Apps SDK lets your tools return ui://... URIs that ChatGPT renders as React-based cards inside the conversation. The trick that keeps this cheap is treating Rork's React Native components as a logic-and-tokens layer, then writing thin Web wrappers for the UI Resources.
// apps-sdk/src/ui/task-card.tsximport type { FC } from "react";import { useTaskById } from "@core/hooks/useTaskById";export const TaskCard: FC<{ taskId: string }> = ({ taskId }) => { const task = useTaskById(taskId); if (!task) return <div>Loading…</div>; return ( <div className="task-card" data-task-id={task.id}> <h3>{task.title}</h3> {task.dueDate && <p>Due: {task.dueDate}</p>} <button onClick={() => window.openai?.callTool("complete_task", { id: task.id })} > Mark complete </button> <a href={`https://yourapp.com/open?task=${task.id}`}>Open in app</a> </div> );};
window.openai is the browser-side bridge inside UI Resources. Buttons round-trip through the MCP server and re-render the card, so you can carry small interactions to completion without ever leaving the chat. The "Open in app" link is a Universal Link that hands users off to Rork for anything that's faster on a mobile UI — and the handoff design is what makes this whole architecture earn its keep.
For styling, ChatGPT exposes CSS variables that adapt to its dark and light modes. If your Rork side keeps design tokens in a shared package (palette, spacing, radii), porting them to Web costs almost nothing. I learned the hard way that hard-coded colors in a UI Resource look great in light mode and unreadable in dark mode; the variable approach removes the guesswork.
A practical detail about layout: ChatGPT renders UI Resources at a constrained width — often around 360 to 480 pixels depending on the viewport. Build for that envelope. If your Rork mobile screens already work at iPhone widths, the same constraints apply. Avoid horizontal scrolling within the card, and prefer compact vertical lists with clear tap targets. Anything more than three to five interactive elements per card starts to feel cluttered inside the conversation.
Sharing One User Identity Across ChatGPT and the Mobile App
This is the most common place to crash in production. Apps SDK supports OAuth 2.0 Authorization Code Flow, so you can bind a ChatGPT user to your service's account. The trap is that your Rork app already has its own auth, and a naive integration doubles the user table.
The pattern I use is a "reverse" flow: from inside the Rork app, the user taps "Connect ChatGPT" and starts the OAuth dance with the ChatGPT side as the relying party. New users still get the standard ChatGPT-initiated flow; existing paying users get the in-app flow. Both land on the same row in the users table.
Tag Apps-SDK-issued sessions with their own scope (scope: "apps_sdk"). Six months from now you'll want "ChatGPT can read but not edit" or "ChatGPT can't see billing details" — and you'll be glad the scope dimension was already there. PKCE (the code_challenge and code_verifier pair) is mandatory; don't skip it because OpenAI's clients won't.
One subtle behavior: when a user disconnects your app from ChatGPT's settings, OpenAI sends a revocation webhook to your /oauth/revoke endpoint. Make sure you actually invalidate the tokens server-side and remove the apps_sdk scope from that session. I've seen integrations skip this step and leave revoked sessions running for hours — a security and trust failure both.
Handing Off Conversations to the Mobile App With Universal Links
"Talk to ChatGPT for five minutes, finish the job in the app" turns out to be a surprisingly common shape of session. People love using natural language to plan and triage; they love a real touch UI for reordering, editing details, and closing out tasks. Universal Links are how you stitch the two together.
Set up Apple Universal Links and Android App Links by hosting apple-app-site-association and assetlinks.json from the same Cloudflare Worker.
// apps-sdk/src/universal-links.tsapp.get("/.well-known/apple-app-site-association", (c) => { return c.json({ applinks: { details: [ { appIDs: ["TEAMID.com.yourapp.mobile"], components: [ { "/": "/open*", comment: "ChatGPT deep links" }, { "/": "/articles/*" }, ], }, ], }, });});app.get("/open", async (c) => { // Embed a correlation ID so we can measure how often ChatGPT links convert const correlationId = c.req.query("cid"); const target = c.req.query("task"); if (correlationId) { await c.env.DB.prepare( "INSERT INTO chatgpt_to_app_opens (correlation_id, target, opened_at) VALUES (?, ?, ?)" ).bind(correlationId, target, new Date().toISOString()).run(); } // Fall back to the App Store if the app isn't installed return c.html(` <script> window.location.href = "yourapp://task/${target}"; setTimeout(() => { window.location.href = "https://apps.apple.com/app/idXXXXXXXXX"; }, 1500); </script> `);});
The latency budget from "Open in app" tap to the right screen rendering is two seconds — past that, users feel friction and bounce. Pass state through URL query params and let the Rork side's deep link handler swap screens immediately, before the network call resolves. With Expo Router, a (stack)/task/[id].tsx file that reads the param and kicks off the fetch in the same render is usually all you need.
Don't forget the analytics value of the correlation ID. If you log it on both sides — ChatGPT outbound and Rork inbound — you can compute the actual handoff success rate, which is one of the most important metrics for the entire integration. In my own data the handoff success rate is around 60 to 70 percent; the rest are users who don't have the app installed, on a desktop browser, or on a platform where Universal Links don't trigger. Knowing this number lets you prioritize the App Store fallback path properly.
Keeping Pricing and Billing Coherent Across Both Surfaces
Apps SDK runs primarily on OpenAI's credit envelope for tool calls, but anything that requires your subscription (advanced reports, unlimited tasks) needs to be billed through your own service. ChatGPT allows opening external URLs, so the practical pattern is to issue a Stripe Checkout session and return its URL.
Stamp metadata.source: "chatgpt_apps_sdk" on every Stripe session that originates from the Apps SDK side, and source: "rork_app" on the mobile side. When you eventually want to know whether ChatGPT-acquired users have higher LTV than App Store ones, your data is already cut by channel. Keep prices identical across surfaces — a ChatGPT user who hears one number and sees another in your app loses trust instantly. A single pricing.ts module that both surfaces import from solves this for free.
If you operate in multiple regions, consider serving the Stripe Checkout session in the user's regional currency. Apps SDK passes the user's locale through context, and Stripe supports auto-localization through automatic_payment_methods. Combining the two gives a smoother experience for international users without forcing you to maintain price IDs per locale.
Testing the Integration Locally Before Submission
Before you submit your Apps SDK manifest to OpenAI for review, you can iterate locally with the developer console. The flow looks like this: run your MCP server on localhost, expose it through a tunnel like cloudflared tunnel so it has a public URL, and register the URL in the developer dashboard as a test integration. Your own ChatGPT account can then call the tools as if it were a real user.
A few guardrails that save time during this stage. First, log every MCP request and response to your own database during development, including the raw payloads. The official developer dashboard shows transcripts but is sometimes delayed; your own logs let you reproduce issues immediately. Second, write a small integration test suite that hits your /mcp endpoint with hand-crafted MCP envelopes. The Apps SDK ecosystem has a few utilities for this, but a simple Vitest file with fetch calls is enough to catch regressions before they hit production. Third, make sure your test ChatGPT account is a separate user from your real account; the developer console hooks let test integrations bypass certain safety checks, and you don't want those rules accidentally relaxed for your daily-driver account.
Once functional testing is green, do a manual round-trip exercise: install the integration, do five different real conversations covering normal usage, edge cases, and error states, and time how long each round-trip takes. If any tool consistently exceeds five seconds even on a fast network, refactor it before you submit. Reviewers do try the integration in real conversations, and slow tools are a common rejection reason. In my own experience, the difference between a one-week approval and a three-week back-and-forth often comes down to whether tool calls feel snappy from the very first interaction the reviewer has.
Production Pitfalls I've Hit (And How to Skip Them)
A few things only show up under real load, and almost all of them have 30-minute fixes if you know they exist.
MCP response-time pressure. Apps SDK has a tool-response timeout that, in practice, you should treat as roughly ten seconds. If your tool kicks off heavy work — multi-step external API calls, long generations — you need a "show progress immediately, deliver the rest async" pattern using Server-Sent Events or polling. I once chained three Notion API calls inside a single tool and pushed past twelve seconds; ChatGPT showed a timeout error. The fix was to return the first item immediately and let the others stream in.
UI Resource caching. ChatGPT caches ui:// URIs aggressively, so updates to underlying data can be invisible until the user starts a new conversation. Add a version query like ui://task-card/123?v=<timestamp>, or set Cache-Control: no-store on the resource response.
Refresh-token expiry. Refresh tokens issued through Apps SDK quietly invalidate after long periods of inactivity, even if the user never disconnected. Track each user's last access and force a re-auth path if more than 30 days pass between visits — the resulting error handling is much cleaner than reacting to surprise 401s in production.
Image hosting and CORS. UI Resources can only display images from absolute URLs hosted with proper CORS headers. R2 or any CDN with Access-Control-Allow-Origin: * works. Sharing the asset bundle with the Rork side via something like @rork/assets is the cheapest path.
Privacy and data retention. Information you obtain through Apps SDK is governed by stricter rules than the average mobile-acquired user. Maintain a separate retention policy page for chatgpt_apps_sdk users and link it from your standard privacy policy. Treat this as table stakes, not nice-to-have.
Rate limit headroom. Apps SDK can call your tools far more aggressively than a typical mobile user opens an app. A user having a five-minute conversation might trigger eight to fifteen tool calls. If your downstream APIs (Notion, Supabase, Stripe) have rate limits, plan for the bursty traffic shape. I add per-user token-bucket rate limiting on my MCP server itself so a single chatty user can't accidentally take the integration down for everyone else.
Distribution Strategy: From the App Directory to Mobile ASO
Building the integration is half the job; getting it discovered is the other half. ChatGPT's App Directory (or its successor surface) is where your integration becomes findable. Application copy that focuses on conversational use cases beats feature lists by a wide margin. "A task manager" is generic; "When you ask 'What should I work on this week?', this app pulls from your past notes, drafts a list, and hands tasks off to mobile for daily execution" tells the reviewer exactly what value the integration adds.
Once users tap through from ChatGPT into your Rork app, switch onboarding behavior. They've already had a focused conversation about their problem; they don't need the standard feature tour. Read the correlation_id from the deep link, skip onboarding for that session, and drop them on the relevant screen with the context already loaded. This single change typically lifts ChatGPT-sourced first-day retention significantly.
On the ASO side, mention the integration in your store description: "Talk to this app inside ChatGPT — say '...' to start." After I added one line like this to a store listing, the install conversion rate moved up by about 12% over the next two weeks. Apps SDK isn't only a distribution layer — it's also a credibility cue inside the App Store. The same logic applies to Google Play. Reviewers and users both see "works with ChatGPT" as a quality signal in 2026.
For deeper integration into your marketing funnel, consider making one or two of your most popular tool flows shareable as conversation starter templates. ChatGPT users often pass URLs back and forth that pre-fill prompts; if you provide a shareable URL like https://chatgpt.com/?model=gpt-4&prompt=...&app=task-manager, you create organic word-of-mouth distribution that doesn't require any paid acquisition spend.
Observability: Knowing What Conversations Are Doing
The hardest part of running an Apps SDK integration in production isn't writing the tools — it's knowing what's happening when something feels off. ChatGPT users don't file bug reports the same way mobile users do; they just stop using the integration. You need observability that catches problems before users churn.
Three signals are worth tracking from day one. First, tool invocation latency: log the wall-clock time from MCP request received to MCP response sent for every single tool call, broken out by tool name. A regression from 800ms to 4 seconds in list_tasks will silently kill engagement, and you won't notice without this metric. Second, error rate per tool: how often does each tool throw an exception, return an empty result, or trigger an OAuth failure? I send these to a Cloudflare Workers Analytics Engine dataset and graph them in Grafana. Third, conversation-length distribution: how many tool calls happen in a single user session before the user moves on? A long tail of single-call sessions usually means your tools are not being chained well in the model's reasoning, often because tool descriptions are too vague.
Pair these metrics with a small alerts rule. If error_rate > 5% over a 15-minute window for any tool, fire to Slack. If p95_latency > 6s for any tool, fire to Slack. These two alerts catch most production fires before users notice them. I also keep a daily digest that lists the top three tools by usage, top three errors, and the day-over-day delta in active users. Five minutes of reading the digest each morning replaces hours of guessing.
For deeper investigation, replay matters. Store the raw MCP envelope for every request and response (after sanitizing sensitive fields) in cold storage like R2 or S3. When a user reports "the integration acted weird yesterday," you can pull their session and see exactly what the model sent and what your server returned. This is also invaluable for fine-tuning tool descriptions over time: read through transcripts where the model called the wrong tool and tighten the descriptions of the tools it should have called instead.
Where to Start This Week
Don't try to build everything at once. Stand up /mcp with a single tool — list_tasks is a great choice — wire up a test OAuth client, and let the UI Resource render <div>{title}</div>. From there, your time is best spent on the handoff: the moment a ChatGPT conversation needs to continue inside the Rork app is where most of the user value sits.
ChatGPT is becoming a third storefront alongside the App Store and Google Play. Whether you can be present on all three by the end of 2026 is the kind of strategic question that quietly compounds.
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.