ChatGPT-web: a static page that keeps your API key in your own browser
ChatGPT web interface using the OpenAI API
At a glance
- What is it?
- ChatGPT-web is a Svelte single-page app that talks to OpenAI-compatible chat APIs straight from the browser, with no backend of its own. That makes hosting trivial and key handling your problem, and the Anthropic preset is documented as a testing surface rather than a full translation layer.
- Who is it for?
- Adopt ChatGPT-web if you want one static page that talks to whatever OpenAI-compatible endpoint you already pay for, and you value that the key and the chat history never leave the browser that entered them.
- Can I use it commercially?
- Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
- Is it still maintained?
- Yes. The repository last received commits 7 days ago.
- What is it written in?
- Mainly Svelte, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 6, 2026, and from our analysis. They are not legal advice.
Editorial analysis
No backend: where the key lives and where the prompts go
The architecture is one sentence long. ChatGPT-web is a single-page web app, and the README says prompts are sent directly to the selected provider. There is no server component in this project that sees your API key, because there is no server component at all.
That has one very concrete consequence. You enter the key in the setup screen, and it is kept in your browser's local storage; the app uses svelte-local-storage-store for that, and the same store holds the chats. Open the page on a phone and you enter the key again. Host the page yourself and nothing changes about where the key lives, because hosting a static page does not turn a browser-held credential into a server-held one.
The privacy claim follows from the same design rather than from cryptography. Chats live in local storage, so nothing is stored anywhere else, and the licence is GPL-3.0, which covers the code you fork and host but is not a privacy control. Nothing in the architecture encrypts what already sits in your browser profile.
The storage story is not entirely local storage, either. The dependency list includes dexie at 4.4.5, an IndexedDB wrapper, so part of the app state is in IndexedDB and part in the local storage store. Both are per-browser and per-origin, which means a self-hosted origin is a different store from the hosted demo origin.
Nine provider presets and one compatibility caveat worth reading
The setup screen ships presets for OpenAI, Anthropic, xAI, Google Gemini, Mistral AI, APIpie, OpenRouter and OrcaRouter, plus a custom option for any OpenAI-compatible API. Each preset exists because that provider exposes an OpenAI-shaped endpoint, which is what lets one client serve all of them.
The Anthropic entry carries an explicit warning that is easy to skim past. The README says that compatibility layer is intended primarily for testing and comparison, and that some OpenAI request fields are ignored or translated, with a pointer to Anthropic's own compatibility documentation for current limitations. In practice that means a field you can set in the UI may not reach the model the way you expect, and the behaviour can change upstream without a change here.
The same honesty applies to image generation. It works through OpenAI, xAI, Gemini, OpenRouter or a compatible custom endpoint by prompting show me an image of ..., and the app picks a provider default such as gpt-image-2 for OpenAI, grok-imagine-image-quality for xAI or gemini-2.5-flash-image for Google Gemini. The chat setting can override the default, and providers with no known default need an explicit model ID typed in. A missing image model therefore surfaces as a failed request rather than as a helpful message.
Everything else in the feature list is local behaviour: streaming responses, voice input and spoken replies, Markdown export of a chat, copyable highlighted code blocks, and a bundled list of pre-selected prompts.
Running it locally: Vite, Svelte 5 and the GitHub Pages base path
The development loop is the standard one for a Vite project, and the README gives both halves:
npm ci
npm run devThe build is a single command producing a dist folder:
npm run buildThe package scripts add two details worth knowing. The dev script runs vite, and dev:public runs vite with --host 0.0.0.0, which is what makes the app reachable from another machine or a phone on the same network. And build:github runs the build with a base of /chatgpt-web/, which is the flag that makes the output work when the app is served from a GitHub Pages project path rather than a domain root.
The stack underneath is visible in the dependency list: svelte at 5.56.9 with the sveltekit-era vite plugin, TypeScript with svelte-check for the check script, Bulma for styling with a dark mode companion, dompurify at 3.4.14 for sanitising rendered HTML, gpt-tokenizer for token counting, and svelte-highlight for code blocks. That combination explains several README claims without needing to guess: highlighted code comes from svelte-highlight, safe rendering of model output comes from dompurify, and prompt length limits can be shown before you send.
The package is marked private at version 0.0.0, so this is an application to build and host rather than a library to install.
Docker Compose brings up a dev container and a fake provider
The compose file defines two services, and neither of them is a production deployment. chatgpt_web builds from the repository Dockerfile, mounts the entire project at /app, publishes 5173, and runs with restart: always; the Dockerfile is node:26-alpine, copies the project, runs npm ci, and starts the public dev server. In other words, this is a development container with hot reload, not a built artefact served by nginx.
The second service is mocked_api, published on 5174, which exists so the UI can be exercised without a real provider. Starting both is one command:
docker compose up -dafter which the web app is at http://localhost:5173/. The chatgpt_web service reads its environment from a .env file through env_file, and the repository does contain a .env at its top level, so a fork inherits whatever is in it until you edit it.
Because the whole source tree is bind-mounted and the dev server is what runs, editing files on the host changes the served app immediately. That is exactly what you want for contributing and exactly what you do not want on a public host, where the same compose file would expose a dev server on a port.
The mocked API is a test harness, not a demo mode
The mocked API is the most engineering-minded part of the project and deserves more attention than the README gives it. Configuring it takes three environment keys: VITE_PROVIDER=custom, VITE_API_BASE=http://localhost:5174, and any non-empty value for VITE_API_KEY. Then docker compose up -d mocked_api starts it.
Responses are scripted with inline directives in the prompt itself. A d followed by a number sets a delay, so d2 stalls for two seconds, which is how you exercise the streaming indicator. An l followed by a number sets the sentence count, so l10 gives a ten-sentence reply, which is how you test scrolling, stop buttons and long-answer layout. Combining them as d2 l10 gives both at once.
There are two fixture prompts for the paths that are hardest to fake. Setting the chat's Image Generation Model to mock-image and prompting show me an image of ... returns an image without a provider call, and prompting mock svg without enabling image generation returns safe inline SVG, which is how you check that rendered SVG is sanitised rather than trusted.
For a client of this kind, that is worth more than any screenshot. It means layout, streaming and rendering regressions can be tested deterministically, with no token spend and no dependence on a provider's current model behaviour.
Desktop build and the bundled prompt snapshot
The app can be bundled as a desktop application through Tauri, which requires Rust on the build machine. Two scripts cover the two cases: npm run tauri dev for a development build and npm run tauri build for a compiled one, with the output landing in the src-tauri/target folder.
The desktop route is the interesting one for this particular architecture, because it solves the key problem without changing anything else. A Tauri build is a local application, so the key stops depending on a browser profile and a hosted origin, and the same direct-to-provider calls happen from a native window. It also makes voice input less awkward, since a desktop app can use system speech facilities that a browser tab has to ask permission for.
The pre-selected prompts are handled as a snapshot rather than a live feed. The bundled list contains the 100 most-upvoted text prompts from the public prompts.chat API, stored as prompts.csv under src/awesome-chatgpt-prompts. Refreshing it runs npm run update-prompts, and the updater validates the response before atomically replacing the file, so an API failure or a network failure leaves the existing snapshot intact.
That atomic replace is a small detail with a large lesson about how the project is built: a build step that can leave a repository in a broken state is a build step nobody runs.
Where ChatGPT-web is the wrong tool
The absence of a server is the whole limitation, and it shows up in the same place every time: there is nowhere to put shared state.
There is no shared history, because chats live in local storage or IndexedDB in one browser profile. There is no shared configuration, so a team cannot distribute one prompt preset or one temperature policy without sending files. There is no audit trail, because nothing records what was sent. And there is no way to hide a key from the person using the app, since it is displayed and stored client-side by design.
The second boundary is provider fidelity. The Anthropic preset is scoped by the README to testing and comparison, and unknown providers are handled through a custom OpenAI-compatible base URL with no verification that the endpoint behaves the way the app expects. A provider with a different streaming shape will show up as a UI problem rather than as an error.
The third is the compose deployment. What ships there is a dev server with the repository bind-mounted and restart: always, which is convenient for development and a poor shape for anything shared.
For a team that needs accounts, shared prompts, retention or an audit log, the realistic comparison is a server-side chat application you operate, where the key lives in your infrastructure instead of in a browser profile. This project is the opposite trade: no server to run, no data to protect at rest, and no capability that depends on a team.
Editorial conclusion
Adopt ChatGPT-web if you want one static page that talks to whatever OpenAI-compatible endpoint you already pay for, and you value that the key and the chat history never leave the browser that entered them. Hold off if you need shared history, shared configuration or an audit trail, because there is no server to hold any of that, and hold off if your provider needs request fields that the Anthropic compatibility layer drops, which the README itself scopes to testing and comparison. Verify three things first. Read where the API key lives: you type it into the setup screen, it is kept in browser local storage through svelte-local-storage-store, and every prompt goes from that browser to the provider directly. So self-hosting the page does not centralise anything, and every device you use needs its own key. Check whether your provider's image endpoint matches the defaults, since the app falls back to names such as gpt-image-2 and requires an explicit model ID where no default is known. And if you self-host, decide whether the committed .env in the repository is acceptable in your fork, since the compose file feeds it to the container. Start with npm ci and npm run dev, or point VITE_PROVIDER=custom at the bundled mocked API before you ever spend a token.
Frequently asked questions
How do I run ChatGPT-web locally?
Run npm ci followed by npm run dev for the development server, or npm run build to produce a dist folder for deployment. The dev:public script adds --host 0.0.0.0 so the app is reachable from other devices on the network.
Where are my chats and my API key stored?
Chats are kept in your browser's local storage, and the prompt is sent directly from the browser to the provider you selected. The app also uses Dexie for IndexedDB, so both stores are per-browser and per-origin.
Can I use ChatGPT-web with Anthropic?
There is an Anthropic preset, but the README states that the compatibility layer is intended primarily for testing and comparison, and that some OpenAI request fields are ignored or translated.
How does ChatGPT-web generate images?
You prompt show me an image of ... and the app uses the provider's image endpoint, defaulting to names such as gpt-image-2 for OpenAI or gemini-2.5-flash-image for Google Gemini. Providers with no known default require an explicit model ID.
What is the mocked API for?
It lets you exercise the app without a real provider. Set VITE_PROVIDER=custom, VITE_API_BASE=http://localhost:5174 and any non-empty VITE_API_KEY, then start it with docker compose up -d mocked_api, scripting replies with directives such as d2 l10.