Guidance for AI coding agents working in this repository. Read this before making changes. FOLLOW GUIDANCE AS CLOSELY AS POSSIBLE. If you think the guidance is wrong, raise an issue or flag a maintainer — don't just do what you think is right. This is the source of truth for how we write code, tests, and docs in this repo.
Use these as the source of truth before exploring further:
- README.md — project overview and quickstart.
- doc/architecture.md — backend layered stack (controllers → drivers → services → stores → clients),
PuterServerwiring,Context(ALS), and extensions. - doc/self-hosting.md — running Puter outside hosted infra.
- CONTRIBUTING.md — testing, security, AI-assisted code, PR conventions, Boy Scout Rule.
- SECURITY.md — how to report vulnerabilities (do not file them publicly).
- BUG-BOUNTY.md — bounty program scope.
- TRADEMARK.md — trademark usage.
The backend is organized as a layered stack inspired by Controller–Service–Repository with dependency injection. Every layer only depends on the layers beneath it, and PuterServer (src/backend/server.ts) wires the whole thing together.
| Layer | Lives in | Responsibility |
|---|---|---|
| Controllers | src/backend/controllers/ | Route handlers. Parse + validate input, apply per-route gates (auth, subdomain, rate limit, body parsers via RouteOptions), call services, format responses. |
| Drivers | src/backend/drivers/ | Optional. RPC-style handlers exposed over /drivers/*. Thin shells that validate RPC inputs and call into services/stores. |
| Services | src/backend/services/ | Business logic. Assume the caller is already authenticated/authorized — services do not run auth gates themselves. |
| Stores | src/backend/stores/ | Persistence. Wraps clients with the domain shape services consume (rows, entities, KV namespaces). |
| Clients | src/backend/clients/ | Adapters for external/internal services (sql, redis, s3, dynamo, email, event bus). Knows protocols, not domain concepts. |
| Config | config.*.json → IConfig |
Flat, typed config object every layer receives at construction. |
Each layer receives the layers beneath it through its constructor. Dependencies are explicit and traceable from PuterServer.
- Don't reach across layers. Controllers do not poke clients directly; services do not register routes. If you want to, the abstraction is wrong — fix the abstraction.
- Don't call sideways within a layer for code reuse. If two services need the same logic, lift it into a util/helper. Services should not depend on other services for shared code.
- Prefer explicit arguments over
Context(ALS). Reach for Context only when the value is genuinely request-scoped and would otherwise thread through many layers. Today it's used sparingly — mostly foractorandreq.
Extensions live in extensions/ and parallel the layered stack. They are for non-crucial parts of the system — things Puter still works without if removed. If a feature is load-bearing for clients, it belongs in core, not in an extension (see whoami as the cautionary example).
The extension global (src/backend/extensions.ts) exposes:
extension.registerClient/Store/Service/Driver/Controller(name, Class)for first-class additions.extension.on(event, handler)andextension.get/post/put/delete/patch/use(path, opts?, handler)for the lightweight common case.optsis the sameRouteOptionscontrollers use.extension.import('client' | 'store' | 'service' | 'controller' | 'driver')returns a lazy proxy to instantiated objects.extension.configexposes live config.
- Modules: We transpile and build as needed — write ES modules, not CommonJS.
- TypeScript preferred for new files. Existing JS is fine; convert opportunistically when you're already touching a file.
- Reuse types before inventing them. Search for an existing type first; extend it if close. Only define a new type when nothing fits.
- Make new types findable. Co-locate them with the layer/module that owns them, export from the obvious entry point, and use a descriptive
PascalCasename. Don't hide types in random files where future readers won't grep them. - Naming:
camelCasefor variables/functions,PascalCasefor classes and for files containing a class (AuthService.ts,KVStoreDriver.ts).
Keep comments light. Prefer self-documenting code — clear names, small functions, obvious flow. Add a comment only when:
- The why is non-obvious (a hidden constraint, a workaround, a subtle invariant).
- A non-trivial usage detail would otherwise trip the next reader.
Don't restate what the code already says. Don't write comments that reference the current task or PR — those rot.
When adding new behavior (function, endpoint, driver method, branch of logic), add a test for it.
- Use Vitest for unit and integration tests. Test files go next to the code they test, named
*.test.tsor*.test.js. - Mock data, not methods. Stub the inputs (fixtures, fake rows, sample payloads) rather than mocking the function under test or the layer beneath it. Over-mocking produces tests that pass while production breaks. If you must mock, mock at a real boundary (a client/external service), not within the same layer you're testing.
- Prefer test server over mocking deps. We have existing test utilities for running full test server to boot deps based on config. (see src/backend/testUtil.ts).
- Hit a real database / real client where reasonable. Integration shapes catch the things unit tests with mocks miss.
- Regression tests for bug fixes. A test that fails before your fix and passes after is the cheapest insurance against the bug coming back.
- If something is genuinely hard to test (UI animation, third-party glue), skip it but say so in the PR.
Before opening a PR, scan the diff for:
- Logs, error messages, or responses leaking internal paths, secrets, tokens, env vars, or other users' data.
- Debug routes, test credentials, commented-out auth checks.
- Endpoints returning more than the caller actually needs.
When in doubt, return less. Auth-, permission-, or data-export-related changes deserve an explicit callout in the PR description.
- Run it, don't just compile it. "It type-checks" is not "it works." Exercise the code path end-to-end at least once.
- Read the neighbors before writing. Match the shape of similar things already in the tree. If you genuinely think the existing pattern is wrong, raise it — don't quietly diverge.
- Boy Scout Rule, proportional to the change. Fix the obvious typo, dead import, or missing typehint in files you're already touching. Don't ride a refactor along with a bug fix — that just makes review harder.
- Understand what you commit. AI assistance is fine; shipping code you couldn't defend in review is not.