What Happened
Mid-project, the code execution feature in the editor stopped working. No code change on my end. No deployment. Nothing. The same feature that worked yesterday returned a whitelist error today.
The culprit: the public Piston API at emkc.org, which was hardcoded in useCodeEditorStore.ts, became whitelist-only as of February 15, 2026. The error message itself was blunt — host your own instance or request a whitelist key.
This is a real category of failure that has nothing to do with your code quality. A free public API that you built against changes its access model, and your app breaks overnight. No deprecation warning. No migration period. Just a wall.
What Piston Was
Piston is an open-source code execution engine. The project at emkc.org was a free, publicly hosted instance of it — no auth, no keys, just send code and get output back. It supported dozens of languages and was widely used in browser-based code editors and competitive programming tools exactly because it required nothing to get started.
The problem with "requires nothing to get started" is that it also means "requires nothing to keep running" — on their end. When the maintainers decided to restrict access, every app that hardcoded their public URL broke simultaneously.
The Options on the Table
When a free API goes away, you have roughly three paths:
1. Self-host Piston — Piston is open source. You can run your own Docker instance and point your app at it. Full control, no dependency on anyone else's uptime. The cost: you now own a server, a Docker container, and the ops work that comes with it. For a side project or portfolio piece, that overhead is often not worth it.
2. Find a whitelist key or paid alternative — Some APIs that restrict public access offer a key-based tier. This is fine if the service has a stable commercial offering, but adds a credential to manage and a potential cost to absorb.
3. Migrate to a different execution engine — Find an API that does the same job, is actively maintained, and doesn't require auth. This is the fastest path if one exists.
The third option was available here, and it was the right call.
The Fix: Wandbox
Wandbox (wandbox.org) is a free, browser-based code compiler that exposes a clean REST API — no keys, no registration, no rate-limit warnings in the docs. It supports a wide range of languages with specific, versioned compilers.
The API surface is simple:
POST https://wandbox.org/api/compile.json
Content-Type: application/json
{
"compiler": "nodejs-20.17.0",
"code": "console.log('hello world')"
}
Response:
{
"status": "0",
"program_output": "hello world\n",
"compiler_output": ""
}
That's the entire contract. Send compiler name + code, get output back.
Compiler Mapping
The editor supported 10 languages. Each one needed to be mapped to a specific Wandbox compiler string. Wandbox uses versioned compiler identifiers, not generic language names.
const WANDBOX_COMPILERS: Record<string, string> = {
javascript: "nodejs-20.17.0",
typescript: "typescript-5.6.2",
python: "cpython-3.13.8",
java: "openjdk-21.0.2",
cpp: "gcc-13.2.0",
c: "gcc-13.2.0",
rust: "rust-1.82.0",
go: "go-1.23.0",
swift: "swift-5.10",
ruby: "ruby-3.3.0",
};
This map is the only place that needs to be updated if Wandbox releases new compiler versions. Everything else flows from it.
The Configurable Fallback
The one good engineering decision that came out of this incident: instead of hardcoding Wandbox the same way Piston was hardcoded before, a NEXT_PUBLIC_CODE_RUNNER environment variable was introduced.
NEXT_PUBLIC_CODE_RUNNER=wandbox
The execution logic in useCodeEditorStore.ts reads this variable and routes accordingly:
const runner = process.env.NEXT_PUBLIC_CODE_RUNNER ?? "wandbox";
if (runner === "piston") {
// use NEXT_PUBLIC_PISTON_API_URL with auth headers
} else {
// use Wandbox
}
This means if Wandbox ever goes the same route as Piston, or if someone wants to use a self-hosted instance in production, switching is a single environment variable change — no code touched. The app doesn't care which runner it's using. It only cares about getting output back.
The Broader Lesson
Hardcoding a third-party public API URL directly into application logic is a liability. It works right up until it doesn't, and when it stops working it requires a code change to fix — which means a commit, a build, and a deployment, not a config update.
The pattern to follow instead:
- Always read the base URL from an environment variable, even for free APIs you fully trust today.
- Map languages/features to provider-specific values in one place — a compiler map, a model map, a voice map. One file to update, not a grep-and-replace across the codebase.
- Design for fallback from the start. Two runners wired up and switchable via config is not overengineering. It is the minimum resilient setup for any feature that depends on a third-party execution endpoint.
The Piston → Wandbox migration took time that it shouldn't have. The reason it took that long was not the API change itself — it was that the original integration gave no separation between "which runner to use" and "how to call the runner." Untangling those two things is what the refactor actually cost.
Next project: environment variable for every external API URL, day one.