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 file mount (Part 4).
  • Storing validated records → a data mount (Part 4).
  • Selecting records by content → a query mount (Part 6).
  • Rendering JSX to HTML → a template mount (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 authproxy (8.10).
  • A stable SMS surface over a providersms plus 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/query mount 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.state or 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 →