If you already deploy your site to Puter, dynamic workers let you add backend endpoints to it without managing workers separately.
A dynamic worker is server-side code that lives inside your hosted site and deploys itself on demand. Drop a file at __workers/api.worker.js next to your index.html, publish the site as you normally would, and https://your-site.puter.site/__workers/api/... starts serving it. There is no deploy step of its own, no worker name to register, and nothing to keep in sync between the site and its backend.
*.puter.site only, and they don't appear in puter.workers.list() or the Developer Center. Read Limitations before you build on them.A regular Serverless Worker is a separate thing from the site that uses it: you deploy the worker, it gets its own *.puter.work subdomain, and from then on you keep the site and the worker in sync by hand — matching versions, updating URLs, remembering which worker belongs to which site.
With a dynamic worker, the code is just a file in the site, so:
The tradeoff is that there are fewer tools for working with them: they don't show up in puter.workers.list() or the Developer Center. See Limitations.
Inside the directory your site is hosted from, create a folder named __workers. Every file directly inside it whose name ends in .worker.js is a dynamic worker.
my-site/
index.html
style.css
__workers/
api.worker.js -> /__workers/api/...
matchmaking.worker.js -> /__workers/matchmaking/...
Two rules apply to the files:
__workers/nested/thing.worker.js is never deployed. The URL has room for one worker name, so there would be no way to point at a file inside a subfolder..worker.js must match [a-z0-9_-]+ — lowercase letters, digits, underscore, hyphen. The name is also the URL path segment, so sticking to those characters keeps capitalization and URL encoding from getting in the way.Anything else under __workers/ — a README, a nested folder, a helpers.js — is ignored. It is not deployed, and it is not served either.
Dynamic workers are written exactly like regular workers, with the same router API and the same globals:
// __workers/api.worker.js
router.get("/health", async () => {
return { ok: true };
});
router.post("/scores/:game", async ({ request, params }) => {
const body = await request.json();
await me.puter.kv.set(`score:${params.game}:${body.player}`, body.score);
return { saved: true };
});
Everything in the router documentation applies unchanged: route parameters, wildcards, me.puter and user.puter, CORS, and returning objects vs. a Response.
https://<site>.puter.site/__workers/<worker>/<path>
The /__workers/<worker> prefix is stripped before the request reaches your code; the worker sees the remainder:
| Request | Worker sees |
|---|---|
/__workers/api |
/ |
/__workers/api/ |
/ |
/__workers/api/health |
/health |
/__workers/api/scores/chess?top=10 |
/scores/chess?top=10 |
Details worth knowing:
/__workers/API/x and /__workers/%61pi/x both reach api.| Code | Meaning |
|---|---|
404 |
No such worker file under the site's __workers/, or the file is in a subfolder or misnamed. |
503 |
The file exists but the worker could not be started. Worth retrying — it never means the worker isn't there. |
| Your own | Anything your handler returns. |
puter.site only. Dynamic workers are routed on the primary hosting domain. The alternate hosting domain and puter.app (private apps) are not routed yet, and custom domains aren't supported.
Not listed by the Workers API. puter.workers.list() and the Developer Center's Workers view do not show dynamic workers. They have no worker record by design: that's what saves you from keeping one in sync, and it's also why there's nothing to list.
One sandbox per site. All of a site's workers share the same KV and AppData namespace, so they can read and write each other's data. That's intentional — it's how two workers in one site cooperate — but it means you can't keep one worker's data private from another in the same site.