8.1 When to reach for custom code
The prebuilt services and pipelines cover a surprising amount. Before you write a custom service, it's worth knowing where the line is — custom code is powerful but carries more responsibility (you maintain it, bundle it, and keep it within the sandbox's API).
Prefer config and composition first
Reach for the built-ins when you can:
- Storing/serving files, hosting a site → a
filemount (Part 4). - Storing validated records → a
datamount (Part 4). - Selecting records by content → a
querymount (Part 6). - Rendering JSX to HTML → a
templatemount (6.6). - Multi-step flows, fan-out, branching, reshaping → a
pipeline(Part 7). - Wrapping another mount with one fixed flow → a
wrapper(7.10). - Forwarding to one external API with injected auth →
proxy(8.10). - A stable SMS surface over a provider →
smsplus an adapter (8.10).
A great deal of "backend logic" is really data shaping and composition, which transforms and pipelines do declaratively, with retries and idempotency handled for you.
Write custom code when you need
- Real computation or control flow beyond JSONata — algorithms, parsing, non-trivial branching.
- A third-party SDK — Stripe, OpenAI, Supabase, etc. (8.3 covers what runs).
- A protocol or format adapter the built-ins don't speak — including a
loadable storage adapter that backs a whole
file/data/querymount with your own backend (8.9). - Custom auth or signing logic, webhook verification, bespoke transforms.
If your need is "call an external API unchanged," start with proxy; if it is
"call it and reshape the result," a wrapper, pipeline external call, or custom
service may fit. If it is "orchestrate several of my own mounts," a pipeline
usually wins.
What you're signing up for
A custom service is untrusted, sandboxed code — there is no unsandboxed path. That means:
- It runs under hard limits (wall-clock, memory, outbound budget) and can be terminated; design for that (8.8).
- It can reach only what its mount grants (8.5) — default-deny.
- It runs against an explicit supported API surface (8.3), not full Node.js; dependencies must be bundled at build time.
- Each invocation gets a fresh sandbox — no in-memory state survives between
requests (use
ctx.stateor the data store).
These constraints are what let RS2 run your code (or AI-generated code) safely next to everything else. The next sections cover the two contracts — JavaScript and WebAssembly — and how to deploy, grant, and debug.
Two engines, one contract
- JavaScript (V8 isolates) — a single-file ES module. The common choice; rich SDK compatibility (8.3).
- WebAssembly (Wasmtime) — a Rust component against the published WIT world (8.4). For when you want Rust or a non-JS language.
Both satisfy the same host contract (the shared conformance suite is the source of truth), so grants, limits, and capabilities work identically across them.
← Part 7 — Pipelines · Manual home · Next: 8.2 The JavaScript service contract →