Deploying your agent app
By the end of the course you have a working agent app on your own machine: OpenSwap after lessons 2.1 through 2.6, or Charta, the solo project. This page takes the next step and puts it online. Deploying means turning your Express server and Vite frontend into a single running service on a host, at an address anyone can open.
Both projects are the same shape, so both deploy the same way: an Express server that holds your key and makes the model requests, and a frontend that talks to it through paths like /api/agent or /api/swaps with no hostname attached. That design, which the course used to keep your key out of the browser, is also what makes deployment simple. The host sets your three variables, your code builds the frontend into static files, and one Express server serves both halves.
Charta deploys exactly like OpenSwap. Its only differences are the route name and the conversation store, and both are called out below. If you rebuilt Charta with a framework of your own choosing, the same ideas still hold: produce a static build of your frontend and serve it from the same server that answers the API routes.
A public URL spends real credit
Once your app is online, anyone with the address can make it send model requests, and every request is billed to your key. The route has no login and no spending limit. Share the URL with people you want to use it, keep an eye on your provider's usage page, and read your key, your credit below before you share it widely.
What you need first
Everything on this page assumes the project already runs locally. If you haven't got it running yet, running the code locally covers the download, the .env file, and the port collisions that catch people out.
Check the three tools the deployment needs:
$ node --version
v24.18.0
$ npm --version
11.18.0
$ git --version
git version 2.50.1If git says "command not found", install it from git-scm.com. A GitHub account is also needed, because the deployment starts from a repository. A free one at github.com is enough.
npm start, you're ready. A GitHub account is the only new thing to set up. npm start on your machine before pushing, because the host runs the same code with fewer places to hide errors. environment.js needs Node 20.12 or newer for its loadEnvFile import, but that is a floor, not a target. This page pins the host to Node 24, the same LTS release recommended for your machine, so the deployed app runs on the runtime you tested it on. Prepare the project
Two things need doing before the code can go anywhere: the frontend needs a production build, and the project needs to become a Git repository that doesn't include your key.
First, confirm the app still works and stop it with Ctrl+C. Then create the build:
$ npm run build
vite v6.4.3 building for production...
✓ 8 modules transformed.
dist/index.html 4.08 kB │ gzip: 1.35 kB
dist/assets/index-C6csgGji.css 8.01 kB │ gzip: 2.41 kB
dist/assets/index-Ck6PtvSY.js 105.89 kB │ gzip: 38.41 kB
✓ built in 177msThe dist/ folder is your whole frontend, compiled into plain files a server can hand out without Vite. Next, check the .gitignore in the project root. Downloads from Scrimba may not include one, so make sure these three lines are in a file named exactly .gitignore beside package.json. The first two are the ones running the code locally already asked for; dist is new, because the host builds its own:
.env
node_modules
distIf you're deploying OpenSwap from lesson 2.6C, look at store/conversations.json too. It holds every conversation saved so far, including the examples it came with, and whatever is in it when you commit is what every visitor sees in the sidebar. To start the public copy empty, replace the file's contents with an empty list rather than deleting it, because the server reads it on every request:
{ "conversations": [] }Now the Git steps:
$ git init
Initialized empty Git repository in path-to-your-project/.git/
$ git add .
$ git commit -m "OpenSwap, ready to deploy"
[main (root-commit) 3f8a1c2] OpenSwap, ready to deploy
19 files changed, 17382 insertions(+)Git names its first branch master on some installations. If that happens, rename it with git branch -M main so the rest of the page matches.
Never commit your .env file
This is the same warning as the local setup page, and it matters more now, because the whole point of these steps is to put your code on someone else's server. If .env is committed, your key is exposed to everyone who can see the repository. Revoke the key and generate a new one if that happens. On the host you'll set the values as variables instead, and .env stays a local file.
npm run build you should have a dist folder. Before you commit, run git status: it should list your project files but never .env. If .env shows up, fix the .gitignore first. dist is generated output the host rebuilds, and node_modules is rebuilt from package.json, so both belong in .gitignore. .env is excluded because the host never receives your key file; your values move to the host's variable settings instead. NODE_ENV=production for the install step or prune dev packages before building. The Scrimba packaging step removed dotenv, and environment.js only reads .env when a required variable is missing from the process, so a host that supplies all three never needs the file. Serve the built frontend from Express
On your machine, npm start runs two servers: Express on port 3001, and Vite on port 5173, which forwards /api calls to Express. A host doesn't need that arrangement, and it's simpler to drop it. You change one line's worth of code and open a different port.
Open server.js and add one line after all of the routes, just above app.listen:
// Serve the built frontend from the same server
app.use(express.static("dist"));Then try the production shape locally:
$ npm run server
Environment check:
✓ AI_KEY: configured
✓ AI_MODEL: gpt-5.4-nano
✓ AI_URL: https://api.openai.com/v1
○ Optional environment not configured: GITHUB_TOKEN
OpenSwap server running at http://localhost:3001This time you open the Express URL itself, http://localhost:3001, not the Vite one. The app should look and behave exactly as it did under npm start, because the browser is now loading the same UI from dist/ and calling the API on the same address. When that works, deployment is just "run these same commands somewhere always-on". Stop with Ctrl+C.
Why the address changed
In development, Vite's proxy is what connected the browser to Express, which is why you opened port 5173. Once Express serves the built files itself, frontend and API share one address, and the browser calls /api on the same origin with no proxy involved. Same origin also means no cross-origin setup, which is why this page never touches CORS.
http://localhost:3001 should show the same app you used before. If it does, the project is ready to deploy. express.static("dist") makes Express hand out the compiled frontend, so the browser loads the app and calls /api on one origin, and the dev-only proxy in vite.config.js has nothing left to do. Your routes answer /api, and the static middleware answers only for files that exist in dist, so the two never compete. dist falls through to Express's own 404. If a request for / answers Cannot GET /, the build output was missing when the server started, which is a build-ordering problem, not a routing one. Deploy on Render
Render is walked through here because it connects directly to a GitHub repository and asks for exactly the two commands you just verified. Railway and Fly.io work on the same inputs through their own dashboards and CLIs, and any host that runs a Node server behaves the same way. What the app actually needs from a host is modest: run node server.js, tell it which port to listen on, and provide the environment variables.
Push the project to GitHub first:
$ git remote add origin https://github.com/your-name/openswap.git
$ git push -u origin mainThen, on render.com, sign in, choose New > Web Service, and connect the repository you just pushed. In the form that follows:
- Give the service a name. The name becomes part of the public address.
- Check that the Language is Node.
- Set the Build Command to
npm install && npm run build. - Set the Start Command to
npm run server. - Choose the Free instance type if you don't want to pay for hosting.
- In the environment variables section, add
AI_URL,AI_KEYandAI_MODELwith the same values you use locally. If your app has the GitHub health-check tool, addGITHUB_TOKENas well. - Add one more variable,
NODE_VERSION, with the value24, so the host runs the same Node version you use locally.
Choose Create Web Service and the dashboard streams the logs. A healthy first deployment looks like your local test, with a port the host chose:
==> Running 'npm run server'
Environment check:
✓ AI_KEY: configured
✓ AI_MODEL: gpt-5.4-nano
✓ AI_URL: https://api.openai.com/v1
✓ Optional environment: GITHUB_TOKEN
OpenSwap server running at http://localhost:10000
==> Your service is liveThe localhost in that line is the app describing itself from inside the host. The address to share is shown in the dashboard, something like https://your-app.onrender.com. Open it and use the app as a visitor would.
The start command is npm run server, not npm start. On your machine npm start is convenient because it starts Vite alongside Express, but on a host the built frontend is already being served by Express, and a second Vite process would just spin unused. Charta is identical, except its route is /api/agent, it has no GITHUB_TOKEN line, and its log line reads Charta server running at....
Planning to split the frontend and API instead?
Static hosts such as GitHub Pages or a Netlify site can publish a built frontend, but they can't run server.js, so the API needs its own service. The moment the two halves live on different origins, the browser blocks /api calls and you're into CORS territory. For this course, one service as described above avoids all of it.
onrender.com address. If the ticks are missing, the variables are the thing to fix, not your code. PORT is left unset: server.js already reads process.env.PORT, so the host's choice of port flows straight into app.listen. NODE_VERSION outranks .node-version, .nvmrc and engines in package.json, so it pins the runtime even when Render's default for new services moves on. Render also sets NODE_ENV=production at runtime only, which is why the build step still installs Vite. Your key, your credit
The three variables do the same job they did on Scrimba and in your local .env, and they still fail as a set rather than individually. A valid OpenAI key against OpenRouter's URL fails just as loudly in production as it did in the browser. If you switch providers, change AI_URL, AI_KEY and AI_MODEL together in the host's settings, exactly as provider setup describes.
Two differences from the local setup deserve attention.
First, if you deployed a later OpenSwap lesson, set GITHUB_TOKEN on the host too. On your machine, GitHub's anonymous rate limit is attached to your IP. On a host, your app shares outgoing IP ranges with every other tenant of that platform, so anonymous GitHub requests run out far faster and the health-check tool can start returning 403s even when it works locally. Adding the token is the difference.
Second, the URL is public and the key is yours. A deployed app is a good place to apply the course's own advice about keys: give the deployment its own key so it can be revoked separately, check the provider's usage page after sharing the link, and pick the model with the recommended list in mind, because a heavier model on a public URL adds up quickly. If a key ever leaks, revoke it first and clean up after.
PORT unset, because the host picks it. Before you share the address, open your provider's usage page once so you know where to look when the link starts getting traffic. process.env names as the local one; only the source of the values changed. When the host supplies all three, environment.js skips .env entirely. When one is missing, it stops the server with the same message you saw locally, and its advice to complete .env means, on a host, complete the variables in the dashboard. AI_URL, AI_KEY and AI_MODEL consistent as a set, and note that free OpenRouter routes in production carry availability and data-policy trade-offs that matter more under a public URL than in a scrim. What survives a restart and what doesn't
The course is honest about its conversation stores, and hosting doesn't change that, it just adds more ways for a server to restart.
On Charta, conversations live in a Map in the server's memory, and a restart clears them. On OpenSwap's final form, conversations are saved to store/conversations.json, which survives a restart on your machine. On a host it usually doesn't. Render's free web services have an ephemeral filesystem: anything the app writes is lost every time the service redeploys, restarts, or spins down, and free services spin down after 15 minutes without traffic. The file goes back to the copy in your repository, so saved conversations come and go. That is the limit the course already names: the conversation history is a demo convenience, and a real product puts sessions and a database behind it. Deploying doesn't turn either store into a database, and it isn't meant to.
One more consequence of a single shared store: OpenSwap has no accounts, so everyone who opens your address sees the same conversation list and can reopen any of it. Keep that in mind before you share the link.
Everything else behaves as you'd expect. The agent context is carried in the request the same way it was locally, the model sees the same tools, and Charta's web search stretch goal, which only exists on the OpenAI API, works on the host exactly as it does at home because OpenAI runs that search for you.
Updating the app is one command:
$ git pushRender sees the new commit on the connected branch, runs the build command, and switches traffic to the new version once it starts. If the build fails, the previous version stays live, and the deploy log shows why.
conversations.json is the state every restart comes back to. contextHistory only has to survive between messages, which both stores manage within one process lifetime. If something doesn't work
The logs say Missing environment variables: AI_KEY. The host doesn't have that variable yet, so the environment check stopped the server on purpose. The next line tells you to complete .env, which is the local fix; on the host, open the service's Environment page, add the missing names, and choose Save and deploy. Your local .env is never in Git, so the host has to be told every value directly.
Cannot GET /. Express started without finding anything in dist. Either the build command didn't run npm run build, or app.use(express.static("dist")) is missing from server.js. Check the build command, then the build step's log for a Vite error.
Your service never goes live. Something stopped the server before it listened on a port. Scroll up the deploy log to the first error: it is usually the environment check above, a failed build, or a start command other than npm run server.
A model-not-found or authentication error. The three variables disagree, for instance an OpenAI model ID against OpenRouter's URL. Change all three together in the host's settings, as provider setup explains.
"Local environment loading requires Node.js 20.12 or newer." The host's Node is older than the project's floor. Set NODE_VERSION to 24 and redeploy.
The health-check tool returns 403 or 429. Anonymous GitHub requests share rate limits with the host's other tenants. Set GITHUB_TOKEN in the host's settings and redeploy.
The first request takes a minute. Render's free services spin down after 15 minutes without traffic, and spinning back up takes about a minute. Wait for the page to load; the requests after it are fast. A spin-down also clears saved conversations, as the previous section describes.
A CORS error in the browser console. The frontend and the API are on different origins, which only happens if you split them across two services. Keep them as one service as this page does, or add CORS handling to the API service if a split is what you want.
Everything looks right and it still fails. Bring it to the course Discord with the deploy logs from the host, not just the browser error. Deployment problems are almost always visible in the service logs.
dist or didn't, the start command either reached the listen line or didn't, and a live service either has the variables or doesn't. The environment check at startup names every variable it validated. 
