◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/AI Models
◉ AI Models/2026-04-27Advanced

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.

Rork584OpenAI5Apps SDKChatGPTMCP4Distribution

✦ Premium Article

A new distribution channel 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.

// apps-sdk/src/server.ts
import { Hono } from "hono";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
import { z } from "zod";
import { createTask, listTasks } from "@core/tasks";
 
type Env = { DB: D1Database; AUTH_JWKS_URL: string };
 
const app = new Hono<{ Bindings: Env }>();
 
app.post("/mcp", async (c) => {
  // Verify the OAuth access token on every request (audience = your own MCP server)
  const authed = await verifyBearerToken(c.req.header("authorization"), c.env.AUTH_JWKS_URL);
  if (!authed) {
    return c.json({ error: "unauthorized" }, 401, {
      "WWW-Authenticate":
        'Bearer resource_metadata="https://your-mcp.example.com/.well-known/oauth-protected-resource"',
    });
  }
 
  const server = new McpServer({ name: "task-app", version: "1.0.0" });
 
  server.tool(
    "create_task",
    "Call this when the user mentions something they intend to do later. Do not use it to change an existing task — call update_task for that.",
    {
      title: z.string().describe("The user's own wording, copied verbatim. Do not summarize or rephrase."),
      due_date: z.string().optional().describe("Due date as YYYY-MM-DD. Omit it if the user never mentioned a date."),
    },
    async ({ title, due_date }, ctx) => {
      const userId = await resolveUserId(ctx, c.env.DB);
      const task = await createTask(c.env.DB, { userId, title, dueDate: due_date });
      return {
        // Text the model can add to the conversation (the UI is bound via registerTool's _meta, below)
        content: [{ type: "text", text: `Created task "${task.title}"` }],
        structuredContent: { taskId: task.id, title: task.title },
      };
    }
  );
 
  server.tool(
    "list_tasks",
    "Fetch the user's current tasks. Always call this before updating or completing a task.",
    {},
    async (_, ctx) => {
      const userId = await resolveUserId(ctx, c.env.DB);
      const tasks = await listTasks(c.env.DB, userId);
      return {
        content: [{ type: "text", text: `${tasks.length} tasks found` }],
        structuredContent: { tasks },
      };
    }
  );
 
  // On Workers, use the transport that speaks web-standard Request / Response
  const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: undefined });
  await server.connect(transport);
  return transport.handleRequest(c.req.raw);
});
 
export default app;

Two things matter most here. First, ship authorization checks on day one. When I re-read the official docs, I could not find the request-signature header this article used to describe (an HMAC in x-openai-apps-signature), so I've removed it. What the docs do describe is verifying an OAuth 2.1 access token on every request, and confirming that traffic really comes from ChatGPT with mTLS (an OpenAI-managed client certificate) or an allowlist of its published egress IPs. If you defer all of these, your MCP endpoint becomes a public RPC for anyone who finds the URL.

Second, return both structuredContent and content from every tool. The structured payload is concise data the model can reuse in later reasoning; the content array is text that accompanies the conversation. 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.

Third, pass a description alongside the tool name — that's the second argument to server.tool() above. Leave it out and the only clue the model has is the identifier create_task. This is the single biggest lever on call accuracy, so it gets its own section next.

✦

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.

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

◉ AI Models2026-08-07
Cutting MCP Tools Didn't Make Anything Lighter — 2,377 Bytes of Definitions vs 110,298 Bytes of Response
I wrote a minimal MCP server for a wallpaper catalog and measured the byte cost of tool definitions against the byte cost of responses. Here is which side actually matters, and the real reason to merge tools.
◉ AI Models2026-10-14
Take the Prompt Out of the App: An Edge Function Gateway With the Guard on the Output Side
A system prompt shipped inside your app is readable from the day it ships. This walkthrough moves the prompt into a Supabase Edge Function, lets the client name only a task, and puts the guard on an output allowlist, with tested code.
◉ AI Models2026-10-05
Sonnet 5.5, GPT-6.1 Sol, or Opus 5.5? I Decide by the Kind of Fix Before I Pick the Model. A Week of Logging Where My Rork Credits Went
After Rork's model menu settled into Sonnet 5.5, GPT-6.1 Sol, and Opus 5.5, I sorted my fixes into three kinds and assigned a model and effort level to each. Here is how I read the changelog, the tiny ledger that tracks where each credit goes, and the rule I drew from a week of entries.