1. Why think about environments at all
In typical web development, sooner or later you end up with a trio: local development, a test server, and a production environment. In the world of ChatGPT Apps it’s the same, but with an extra twist: the client (ChatGPT) is always in the cloud, even when you’re developing “locally.”
If everything runs only on your laptop behind a random tunnel address, several unpleasant effects appear. First, the URL keeps changing, and you don’t remember which endpoint Dev Mode is currently pointing at. Second, performance and networking don’t resemble production conditions. Third, the local environment often uses different keys, different services, and generally lives in a parallel reality.
On the other hand, living “always in prod” is bad too. Any change can suddenly break flows for live users, especially if you already have integrations like Stripe, OAuth, or ACP-based payments. Legally and from a policy standpoint this is problematic as well: experimenting on real users is not the best path to the Store.
So the goal of this lecture is to form a simple but strict mental model: there is local dev, there is staging, there is production, and there is Dev Mode as a way to direct ChatGPT to the right environment. Not one big “my laptop with a tunnel that sometimes suddenly turns into prod.”
2. A peculiarity of ChatGPT Apps: the client is always in the cloud
In a classic SPA application, you often run both the client and the server locally: the browser on localhost, the backend on localhost, and everything happily talks within one machine.
In ChatGPT Apps that doesn’t happen. The client (ChatGPT + your widget) always lives in OpenAI’s infrastructure. Even if your app’s code is running on your laptop, the request path looks like this:
sequenceDiagram
participant User as User
participant ChatGPT as ChatGPT (cloud)
participant Tunnel as HTTPS tunnel
participant App as Your Next.js + MCP
User->>ChatGPT: Message / widget click
ChatGPT->>Tunnel: HTTPS request to the App URL
Tunnel->>App: Proxying to localhost
App-->>Tunnel: Response (UI/JSON)
Tunnel-->>ChatGPT: Response
ChatGPT-->>User: Updated chat + widget
Even when you’re “just testing locally,” you are already living in a distributed system: there’s a cloud client, a network, a tunnel, and your local server.
This matters because:
- The local environment is not “everything is local.” It’s “cloud → tunnel → local server.”
- When you later add staging and production, the scheme differs only in where ChatGPT sends requests: to the tunnel, to a staging domain, or to the production domain.
3. Local dev: what your current setup looks like
Let’s see what this general scheme looks like for you now.
After modules 2–6 you most likely have this picture:
- A Next.js dev server started with npm run dev (usually http://localhost:3000).
- A local MCP server (often a separate process, for example http://localhost:2091).
- An HTTPS tunnel (ngrok, Cloudflare Tunnel, etc.) that exposes your Next.js/HTTP endpoint publicly at an address like https://abc123.ngrok.app.
In ChatGPT Dev Mode you point to this public URL, and ChatGPT starts calling your application. All of this is the local dev environment.
Key properties of local dev:
- The local environment gives a very fast feedback loop. You change code in VS Code, Next.js does a hot reload, and the widget updates in a couple of seconds.
- You can break anything here, use mock data, test keys, and odd configs.
- There are no real users; hardly anyone other than you even knows about this URL.
It usually looks like this:
graph LR
subgraph Dev Laptop
Next[Next.js dev server]
MCP[MCP server]
end
ChatGPT((ChatGPT Cloud))
Tunnel[[HTTPS tunnel]]
ChatGPT --> Tunnel --> Next
Next --> MCP
To avoid mixing up local/staging/production, it’s helpful for the application itself to “know” where it’s running. In your app code, it’s useful to explicitly fix that you’re currently in the dev environment. The simplest step is to introduce a small environment configuration module.
For example, let’s create a file app/config/env.ts:
// app/config/env.ts
export type AppEnv = 'local' | 'staging' | 'production';
export const APP_ENV: AppEnv =
(process.env.NEXT_PUBLIC_APP_ENV as AppEnv) ?? 'local';
export const isProd = APP_ENV === 'production';
Here we:
- Introduce a typed enumeration of environments.
- Read the NEXT_PUBLIC_APP_ENV variable (you will later set it to different values on dev/staging/prod).
- By default assume we’re in 'local' so local development works “out of the box.”
This doesn’t deploy anything yet, but it already gives a point to build on: your code understands which environment it’s running in.
Next, you can, for example, show the environment right in the widget so you don’t mix things up.
// app/components/EnvBadge.tsx
import { APP_ENV } from '../config/env';
export function EnvBadge() {
return <span>ENV: {APP_ENV}</span>;
}
Such a small badge seriously helps avoid “am I on staging or in prod right now?,” especially when the widget looks identical.
4. Staging: a dress rehearsal for the production environment
A staging environment is a “rehearsal for production.” It’s no longer your laptop with a dev server, but a remote server or a Vercel deployment where the built code is uploaded.
From ChatGPT’s perspective, staging looks almost like production: a convenient, stable HTTPS endpoint with a domain like https://staging.giftgenius.app, where:
- the code is already built (npm run build completed successfully);
- environment variables similar to production are used (same names, same format) but with test keys;
- the same external services are available (Stripe sandbox, test OAuth accounts);
- the network topology is similar to production (for example, the same database type and the same region).
Why staging is needed in the context of ChatGPT Apps:
First, staging is convenient for running end-to-end scenarios. For example: user in ChatGPT → ChatGPT launches your app → the widget asks the user → an MCP tool is called that goes to an external API → returns recommendations → the widget shows the result. Such a scenario via a random tunnel on your local machine may behave one way, while in staging it will be different: latency, networking, and resources are closer to reality there.
Second, staging lets you test integrations that are simply scary to run locally. For example, payments: Stripe, ACP/Instant Checkout, etc. In staging you configure test keys, test webhooks, and run the scenarios “for real,” but without real money.
Third, staging is a place for team verification. If you have multiple developers, a designer, QA, a product manager—everyone needs a shared URL that doesn’t depend on whose laptop is currently on or whose tunnel just went down.
It’s convenient to picture staging like this:
graph LR
ChatGPT((ChatGPT Cloud))
AppStaging["GiftGenius Staging https://staging.giftgenius.app"]
ChatGPT --> AppStaging
And inside https://staging.giftgenius.app you might be running Next.js, an MCP server, a staging database, and everything else.
We won’t go into Vercel deployment details in this lecture; that’s the topic of later lessons. For now, just accept as given: staging is a separate environment, as close to production as possible in configuration and in how ChatGPT reaches it.
5. Production: live server and real users
The production environment is where real users and real money arrive. There is no “I’ll quickly tweak main and see what happens” here—any changes should be deliberate, tested, and, whenever possible, have a rollback path.
The production domain must be stable. It’s not a random ngrok URL, but a normal name like https://giftgenius.app or something similar. This is the address you specify in your App settings for the Store: when a user finds your application in the ChatGPT Store and launches it, ChatGPT will call this endpoint.
Production usually has higher requirements:
- Stability. Low error rates, predictable response times, correct behavior under load. In later modules we’ll talk about SLOs/SLIs, but intuitively it’s “the application should ‘almost always’ work and ‘almost always’ respond quickly.”
- Security. Only the secrets you need, least-privilege permissions, careful handling of PII and money.
- Limiting experiments. No “restarted the dev server again” in the middle of the day; experiments go through feature flags, A/B testing, or separate dev/staging environments, not by poking at the production server directly.
In ChatGPT terms, production is no longer about Dev Mode but about the published App: it’s available to users via the Store or org settings, goes through review, and must be reliable enough not to embarrass you before moderation.
6. Dev Mode vs production use of the App: how things are connected
Now for the most common confusion: ChatGPT’s Dev Mode is not a “separate environment.” It’s more of a routing switch that tells ChatGPT which URL to target when you test your application.
In Dev Mode you can:
- connect the local application via a tunnel;
- connect the staging environment;
- even temporarily point Dev Mode at production (which you usually shouldn’t do).
Formally, Dev Mode tells ChatGPT: “Here’s the manifest of my App, here’s the URL of my MCP/Apps SDK endpoint. Use it when I launch this application.” And you can change that URL.
After publishing to the Store, the App gets an official production endpoint. That’s what will be used for real users, and you can’t just change it: you need a new version, review, etc.
In practice, a reasonable scheme for your training app might look like this:
graph TD
subgraph Dev Mode
DevApp["GiftGenius Dev App
(Dev Mode)"]
end
subgraph Store
ProdApp["GiftGenius
(Store App)"]
end
UserDev[You / team] --> DevApp
UserProd[Real users] --> ProdApp
DevApp -->|tunnel URL| LocalEnv[Local dev
https://abc123.ngrok.app]
DevApp -->|staging URL| StagingEnv[Staging
https://staging.giftgenius.app]
ProdApp -->|prod URL| ProdEnv[Production
https://giftgenius.app]
You configure the Dev Mode application GiftGenius Dev so that it usually points to local dev (through a tunnel) and, when needed, to staging. The Store application GiftGenius is strictly bound to the production URL.
Sometimes teams also create a separate App for QA, like GiftGenius Staging, which points only to the staging URL. That’s handy if you have a large QA team; for this course, one dev App is enough.
It’s important to think like this: Dev Mode is a personal sandbox for you and your team, where you can change URLs, tweak metadata, and restart the tunnel. The production App in the Store points only at production and plays by stricter rules.
7. Linking Git branches, domains, and ChatGPT App
Environments are not just servers. They are also code branches and App configurations in ChatGPT. Sooner or later you’ll want to tell at a glance from a URL or App name which version of the code is running there.
A simple minimal approach is this.
For individual feature development you use feature/* branches, for example feature/new-recommendation-algo. You run the code locally + tunnel. ChatGPT Dev Mode usually points to the same dev endpoint where you take turns running local versions. A separate App for each feature branch is overkill.
For integrating features before release, you can have a develop or staging branch. Everything in that branch is automatically deployed to the staging environment, for example to a Vercel preview URL like https://giftgenius-staging.vercel.app. You can create a separate Dev Mode App for it or periodically point the shared Dev App to this URL.
The main (or master) branch is only tested code. That’s what gets deployed to the production URL and is linked to the Store application GiftGenius.
It might look roughly like this:
| Environment | Git branch | URL | ChatGPT App |
|---|---|---|---|
| Local dev | |
|
GiftGenius Dev (Dev Mode) |
| Staging | |
|
GiftGenius Dev or GiftGenius Staging |
| Prod | |
|
GiftGenius (Store) |
Remember APP_ENV from app/config/env.ts? The values 'local'/'staging'/'production' correspond directly to the “Environment” column: in local dev you run the app with APP_ENV=local, the staging deployment with APP_ENV=staging, and production with APP_ENV=production.
This table isn’t bureaucracy; it’s a way to avoid debugging by guessing “what version is even running on this domain right now?”
In the code itself, you can strengthen this link a bit. For example, display not only the ENV but also the commit/branch in the widget’s debug mode:
// app/config/buildInfo.ts
export const BUILD_COMMIT = process.env.NEXT_PUBLIC_BUILD_COMMIT ?? 'dev';
export const BUILD_ENV = process.env.NEXT_PUBLIC_APP_ENV ?? 'local';
// app/components/BuildInfo.tsx
import { BUILD_COMMIT, BUILD_ENV } from '../config/buildInfo';
export function BuildInfo() {
return <small>Build: {BUILD_ENV}@{BUILD_COMMIT}</small>;
}
If during deployment you set the commit SHA in NEXT_PUBLIC_BUILD_COMMIT, the widget will honestly show which exact code is currently running. On staging/prod this sometimes saves hours of debugging.
8. Mini practice: draw your environment diagram
Before diving into Vercel and logs, it’s helpful to literally “draw on a napkin” a diagram of your environments. This could be a mermaid diagram in README.md, a whiteboard sketch, or even a picture in a notebook.
For our training app GiftGenius, the diagram might look like this:
graph TD
subgraph ChatGPT
DevMode["Dev Mode
(you and the team)"]
Store["Store
(real users)"]
end
subgraph Servers
Local[Local dev
Tunnel → localhost]
Staging[Staging
staging.giftgenius.app]
Prod[Production
giftgenius.app]
end
DevMode --> Local
DevMode --> Staging
Store --> Prod
A useful exercise for you right after the lecture:
- List all the environments you already have: local with a tunnel, possibly an early Vercel deployment, anything else.
- Next to each, note which Git branches are deployed there.
- Also note which ChatGPT Apps (or connectors) point where.
- Mark with arrows where ChatGPT goes for each server.
If you’re not working alone, create a file architecture/environments.md in the repository. This immediately reduces the chance of “our staging is down, but nobody knows what its URL even is.”
To tie this back to your application, in Dev Mode right now create one App GiftGenius Dev and decide: by default it points to the local environment’s tunnel, and when you want to test a whole release, temporarily reconfigure it to the staging URL. In the next lectures you’ll learn to deploy staging/prod on Vercel and link this to environment variables.
If you boil it down to one idea: treat environments and Dev Mode as a coordinate system for your App. Local is for fast development, staging is for a dress rehearsal, production is for real users, and Dev Mode is your switch between them—not a separate magical environment.
9. Common mistakes when working with environments and Dev Mode
Mistake #1: living only on localhost + tunnel and treating that as prod.
This approach feels convenient: “why do I need staging and prod, my tunnel works, ChatGPT connects.” But the tunnel has an unstable URL, different network characteristics, and the whole setup depends on one laptop. As soon as you need something like an OAuth callback, Stripe webhooks, or an MCP Gateway, the lack of proper staging/prod will hurt.
Mistake #2: confusing Dev Mode with a separate environment.
Many think: “I have Dev Mode, so I have a dev environment.” In reality, Dev Mode just tells ChatGPT where to go: to the tunnel, to staging, or even to prod. Dev Mode is a client setting, not a server setting. You create the server-side environments (local/staging/prod) yourself: deploy code, configure domains, environment variables.
Mistake #3: pointing Dev Mode at production to “do a bit of testing.”
Technically this is possible: you can enter the production URL in Dev Mode and play with the App as if it were local. The problem is you suddenly start testing on real users, real data, and possibly real money. Any bug in the tool or widget can cause failures for production users—and you might not immediately realize where it’s coming from. Keep Dev Mode on dev/staging and use the Store App for production.
Mistake #4: no explicit map of “branch ↔ environment ↔ URL ↔ App.”
If no one on the team can immediately answer which branch deploys to staging, what its URL is, and which ChatGPT App points to it, that’s a guaranteed source of chaos. You’ll get “it works for me, not on staging, and something else in prod.” A simple table or markdown file with this map pays for itself many times over.
Mistake #5: underestimating the difference between local dev and staging.
Locally, you run a dev server, you have one set of keys, one set of services, and one network. In staging, the code is built and runs in a different environment, with different limits, timeouts, and routes. If you test only locally and keep staging “for show,” critical bugs will surface in prod. Build the habit: local development first, then a check on staging, and only then release to production.
Mistake #6: trying to solve every problem via ChatGPT while ignoring the environment setup.
Sometimes when problems arise, developers start “asking ChatGPT what happened” instead of looking at the picture: which App points to which URL, which environment failed, where the logs are. Our environment scheme today is the foundation for the next lecture, where we’ll debug systematically: read logs, use the MCP inspector, and only then blame the model.
GO TO FULL VERSION