Native Next.js Integration in ChatGPT: In-Depth Analysis - Vercel
# Running a Full Next.js App Natively Inside ChatGPT
When **OpenAI announced the Apps SDK with Model Context Protocol (MCP) support**, it enabled embedding rich web applications directly inside ChatGPT.
But serving static HTML in an iframe is straightforward — running a full **Next.js app** with client-side navigation, React Server Components, and dynamic routing inside ChatGPT’s **triple-iframe sandbox** requires engineering work.
This guide documents how we bridged that gap, and how you can run a modern Next.js app seamlessly inside ChatGPT.
---
## What ChatGPT Apps & MCP Enable
[ChatGPT Apps](https://openai.com/index/introducing-apps-in-chatgpt/) are **interactive widgets** that run inside conversations.
Example: *“Find me a hotel in Paris”* might surface a Booking.com widget with live search results — without leaving the chat.
**Key points:**
- Built on **[Model Context Protocol](https://vercel.com/blog/model-context-protocol-mcp-explained)** (MCP) — an open standard linking AI models to external tools and data.
- ChatGPT discovers and uses tools/resources dynamically, fetching your app’s HTML from an MCP server, then rendering it in its own iframe.
---
> **Tip:** See our [Next.js Starter Template](https://github.com/vercel-labs/chatgpt-apps-sdk-nextjs-starter) — deployable to [Vercel](https://vercel.com/templates/ai/chatgpt-app-with-next-js) — to get a ready-to-run example with all necessary patches.
---
## How ChatGPT’s Triple-Iframe Structure Causes Issues
**Architecture:**
chatgpt.com
└── web-sandbox.oaiusercontent.com (sandbox iframe)
└── web-sandbox.oaiusercontent.com (inner iframe)
└── your app's HTML
### Challenges for Next.js:
- Origin mismatch (app thinks it runs on `web-sandbox.oaiusercontent.com`)
- Asset loading failures (`/_next` chunks from wrong domain)
- Broken client-side navigation
- CORS errors with React Server Components
- HTML mutations causing hydration mismatch
- External links trapped inside iframe
---
## 7 Core Patches to Fix Compatibility
### 1. **Load Static Assets from Your Own Domain**
Use `assetPrefix` in `next.config.ts`:
import type { NextConfig } from "next";
import { baseURL } from "./baseUrl";
const nextConfig: NextConfig = {
assetPrefix: baseURL,
};
export default nextConfig;
And `baseUrl.ts`:
export const baseURL =
process.env.NODE_ENV === "development"
? "http://localhost:3000"
: "https://" +
(process.env.VERCEL_ENV === "production"
? process.env.VERCEL_PROJECT_PRODUCTION_URL
: process.env.VERCEL_BRANCH_URL || process.env.VERCEL_URL);
Ensures `/_next` assets resolve correctly in dev, preview, production.
---
### 2. **Set a Base URL for Relative Paths**
Applied in `layout.tsx` — fixes images, fonts, API calls that use relative paths.
---
### 3. **Patch Browser History to Prevent Origin Leaks**
const originalReplaceState = history.replaceState;
history.replaceState = (state, unused, url) => {
const u = new URL(url ?? "", window.location.href);
const href = u.pathname + u.search + u.hash;
originalReplaceState.call(history, state, unused, href);
};
Also patch `pushState` similarly — keeps URLs relative, protecting sandbox boundaries.
---
### 4. **Rewrite Fetch Requests for Client-Side Navigation**
Override `window.fetch` to point same-origin requests back to your real domain:
if (isInIframe && window.location.origin !== appOrigin) {
const originalFetch = window.fetch;
window.fetch = (input: URL | RequestInfo, init?: RequestInit) => {
// parse input...
if (url.origin === window.location.origin) {
const newUrl = new URL(baseUrl);
newUrl.pathname = url.pathname;
// ...
return originalFetch.call(window, newUrl.toString(), { ...init, mode: "cors" });
}
return originalFetch.call(window, input, init);
};
}
---
### 5. **Add CORS Headers via Middleware**
// middleware.ts
export function middleware(request: NextRequest) {
if (request.method === "OPTIONS") {
const res = new NextResponse(null, { status: 204 });
res.headers.set("Access-Control-Allow-Origin", "*");
// ...
return res;
}
return NextResponse.next({
headers: {
"Access-Control-Allow-Origin": "*",
// ...
},
});
}
export const config = { matcher: "/:path*" };
Handles **preflight OPTIONS** requests and adds universal CORS headers.
---
### 6. **Prevent Parent Frame HTML Mutations**
Use `MutationObserver` to strip unauthorized attributes from ``:
const html = document.documentElement;
new MutationObserver(mutations => {
mutations.forEach(m => {
if (m.type === "attributes" && m.attributeName !== "suppresshydrationwarning") {
html.removeAttribute(m.attributeName!);
}
});
}).observe(html, { attributes: true });
Add `suppressHydrationWarning` to ``.
---
### 7. **Open External Links in User’s Browser**
Intercept click events:
window.addEventListener("click", e => {
const a = (e.target as HTMLElement)?.closest("a");
if (!a) return;
const url = new URL(a.href, window.location.href);
if (url.origin !== window.location.origin && url.origin !== appOrigin) {
window.openai?.openExternal({ href: a.href });
e.preventDefault();
}
}, true);
Uses `openai.openExternal()` to break out of iframe.
---
## Integrating MCP in Your Next.js App
MCP exposes:
- **Resources** — HTML or other content ChatGPT renders.
- **Tools** — Model-invoked actions.
### Register a Resource:
server.registerResource(
"content-widget",
"ui://widget/content-template.html",
{ title: "Show Content", mimeType: "text/html+skybridge" },
async uri => ({
contents: [{ uri: uri.href, mimeType: "text/html+skybridge", text: html }],
})
);
### Register a Tool:
server.registerTool(
"show_content",
{
title: "Show Content",
inputSchema: { name: z.string() },
_meta: { "openai/outputTemplate": "ui://widget/content-template.html" },
},
async ({ name }) => ({
structuredContent: { name, timestamp: new Date().toISOString() }
})
);
---
## Receiving Tool Output in Your App
Watch `window.openai.toolOutput` and sync to state:
const [name, setName] = useState(null);
useEffect(() => {
if (window.openai?.toolOutput?.name) setName(window.openai.toolOutput.name);
}, []);
---
## Recommended React Hooks for Apps SDK
Encapsulate repetitive logic:
- `useSendMessage()` — programmatically send messages to ChatGPT
- `useWidgetProps()` — type-safe tool output access
- `useDisplayMode()` — adjust UI for widget/fullscreen display
---
## Advantages of This Approach
- **Full Next.js feature set**: RSC, streaming, server actions, ISR, dynamic routing, API routes
- **Native-feeling UX**: Back/forward navigation, instant transitions
- **Single patch location**: Applied once in `layout.tsx`
- **Performance**: Faster than full iframe reloads
---
## Get Started Quickly
Deploy the [Next.js Starter Template](https://github.com/vercel-labs/chatgpt-apps-sdk-nextjs-starter).
All 7 patches are included — you can focus on building features, not iframe workarounds.
For broader AI-driven content publishing alongside your ChatGPT apps, consider **[AiToEarn](https://aitoearn.ai/)** — an open-source platform integrating AI content generation, multi-platform publishing, analytics, and monetization.