8.9 Loadable storage adapters (bring your own backend)
The file, data, and query services ship with built-in backends — local
files, durable file-backed data, in-memory data, and the reference query adapter
(4.7). A loadable storage adapter lets you
back any of those mounts with your own deployed code instead, so a data
mount can persist to Redis, a file mount to an object store, a query mount to
a real database — without rebuilding the runtime.
This is the answer to "RS2 doesn't have an adapter for my database." You write one as a normal custom service (Part 8) and point a mount at it.
Loadable adapters run on the JavaScript engine (they use the socket capability, 8.5). A node built without the JS engine rejects an adapter-backed mount at config time with
501.
The idea: an adapter is a message-shaped backend
An adapter is a normal custom JS bundle, but the host sends it a small, service-kind-specific message surface:
| Mount kind | Adapter conversation |
|---|---|
data |
Store-shaped GET/PUT/DELETE /{dataset}/{key}, trailing-slash listings, schemas, and confirmed dataset delete |
file |
HEAD/GET/PUT/DELETE/MOVE plus listings; file content crosses the JSON boundary as base64 |
query |
One POST /query with {query, params, take, skip} returning {rows, total} |
Your bundle translates those messages into the backend's wire protocol (via a
socket or httpOut grant, 8.5) and translates the result back. The query
service performs template substitution and parameter validation before its
adapter call; a file adapter's content is materialized at this boundary, while
the host rebuilds the versioned body used for ETags, ranges, and 304s.
You do not reimplement schema validation, ETags, or the .schemas surface.
The stock service runs unchanged on top of your adapter: a data mount still
validates against its schemas and emits ETags; your adapter only handles
persistence. You implement the storage; the service keeps the contract.
Wiring a mount to an adapter
Two steps. First, deploy the adapter bundle like any custom service (8.6):
rs2 deploy redis-adapter.js --name my-redis --bundle
Then point a data (or file / query) mount at it through a store block:
{ "path": "/data", "service": "data",
"config": {
"store": {
"adapter": "code:my-redis@v1",
"grants": { "db": { "type": "socket", "hosts": ["redis.internal:6379"] } },
"host": "redis.internal", "port": 6379
},
"access": { "read": "all", "write": "A" }
} }
adapter— the deployed code ref (code:<name>@<version>, 8.7). Immutable and versioned like any custom service; bump the version to roll the backend.grants— the adapter's capabilities. An adapter only needs itssocketgrant;request/fetchare default-deny for adapters.- Connection params — the entire
storeblock is handed to your bundle asctx.config, so puthost,port, and other non-secret connection options there. Keep credentials host-side with an injected grant or an operator infra (10.7), not in the adapter's visible config.
The same store block works on file, data, and query mounts — each loads
its adapter lazily on first request.
Resident runtimes: connections pool across requests
A normal custom service gets a fresh sandbox per request (8.2), which would mean reconnecting to your database on every call. Adapters are different: they run as resident runtimes — the bundle is evaluated once and kept warm on its own thread, servicing many requests against the same isolate. A connection your adapter opens stays open for the next request, so you can pool it in an ordinary module-level variable. (Appendix G §10 covers the mechanism.)
Two optional store keys tune the pool:
| Key | Default | Meaning |
|---|---|---|
maxRuntimes |
4 |
Max warm runtimes per mount; the pool grows lazily under concurrent load (each runtime serializes its own jobs) |
idleMs (or idleSeconds) |
60000 / 60 |
Evict a runtime idle longer than this, closing its pooled connections. 0 disables eviction |
A serial workload stays at a single runtime; the pool only grows when concurrent
requests would otherwise queue. A config change rebuilds the tenant, which drops
the whole pool (and its connections) and lets the next request spin up the new
version — that's how a new adapter version cleanly replaces the old one.
Reference adapters
The project ships proven adapters you can use as starting points:
- MongoDB data (
guest-adapters/mongo-data.js) — datasets map to collections, record keys to string_idvalues, with schemas kept in a dedicated collection. - MongoDB query (
guest-adapters/mongo-query.js) — runs aggregation pipelines and returns rows plus total in one round trip.
Use them as templates: an adapter is just store-pattern message handling plus a
wire-protocol client built on RS2Socket. These examples currently assume a
network-trusted, no-auth MongoDB. SCRAM-SHA-256 needs WebCrypto primitives the
sandbox does not yet expose.
When to reach for this
| You want | Use |
|---|---|
| Store JSON records in Postgres/Mongo/Redis | a data adapter |
| Serve files from an object store or remote FS | a file adapter |
| Push queries down to a real database engine | a query adapter |
| Just call an external API from a handler | a plain service + fetch grant (8.5) — not an adapter |
If you only need to call a backend from within one handler, you don't need an
adapter — a normal custom service with a fetch or socket grant is simpler. Reach
for a loadable adapter when you want a whole store mount (with schemas, ETags,
listings, and the agent surface) backed by your own persistence.
← Previous: 8.8 Limits and diagnosing failures · Manual home · Next: 8.10 Proxy and SMS services →