~/docs / mercio

Mercio

Serverless functions for the Mercy platform. Upload a zip of Node.js or TypeScript code and get back a public URL that executes your handler on every HTTP request inside an isolated workerd V8 process.


Overview

Mercio is the serverless execution layer of Mercy. Each deployed function:

  • Runs inside a dedicated workerd V8 isolate — fully separated from other functions, no shared globals or memory.
  • Is bundled at upload time by esbuild, so you can use npm packages and TypeScript.
  • Keeps a warm process in a pool so subsequent requests to the same function are served in single-digit milliseconds after the first.
  • Supports any HTTP method. The same URL handles GET, POST, PATCH, DELETE, etc.
  • Works with both a simple custom handler contract and the Hono web framework.

How it works

  1. 01

    Upload

    POST /api/mercio/upload with a zip file and entry point. The API stores the zip in memory and pushes a mercio-builds job onto the BullMQ queue.

  2. 02

    Bundle

    The Worker service picks up the build job, extracts the zip, and runs esbuild to produce a single worker.mjs bundle. The bundle is wrapped in a Cloudflare Workers-compatible shim (export default { async fetch(...) }) so workerd can execute it. The bundle is uploaded to R2 at mercio/<id>/worker.mjs.

  3. 03

    Invoke

    When a request arrives at /mercio/:id, the API enqueues a mercio-invocations job with the full request context (method, path, query, headers, body) and waits for the result using BullMQ's waitUntilFinished.

  4. 04

    Execute

    mercio-runtime dequeues the job. workerdPool.ensure(id) returns a warm workerd process (cold-starting from R2 if needed). The runtime forwards the request to the workerd process via loopback HTTP and collects the response.

  5. 05

    Respond

    The result (status, headers, body) is returned as the BullMQ job value. The API's waitUntilFinished resolves and the original HTTP caller receives the response.


Quickstart

1. Write your handler

index.js
module.exports = async (req) => {
  return {
    status: 200,
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ hello: req.query.name ?? 'world' }),
  }
}

2. Zip and upload

bash
# create the zip
zip hello-world.zip index.js

# authenticate
TOKEN=$(curl -s -X POST https://api.yourdomain.com/auth/login \
  -H 'content-type: application/json' \
  -d '{"email":"you@example.com","password":"yourpassword"}' \
  | jq -r .token)

# upload
curl -X POST https://api.yourdomain.com/api/mercio/upload \
  -H "Authorization: Bearer $TOKEN" \
  -F "zip=@hello-world.zip" \
  -F "name=hello-world" \
  -F "entry=index.js"

Response contains id, status (QUEUED), and invokeUrl.

3. Poll until deployed

bash
ID="<function-id>"

curl -s -H "Authorization: Bearer $TOKEN" \
  https://api.yourdomain.com/api/mercio/$ID | jq .function.status
# "QUEUED" → "BUILDING" → "DEPLOYED"

4. Invoke

bash
curl "https://api.yourdomain.com/mercio/$ID?name=mercy"
# {"hello":"mercy"}

Handler contract

Your entry file must export (or module.exports) an async function that receives a request object and returns a response object.

Request (req)

FieldTypeDescription
methodstringHTTP method — "GET", "POST", etc.
urlstringFull request URL
pathstringPath component only
headersRecord<string, string>Request headers
queryRecord<string, string>Parsed query string parameters
bodystring | object | nullParsed JSON, raw text, or null for GET/HEAD

Return value

FieldTypeDescription
statusnumberHTTP status code. Defaults to 200.
headersRecord<string, string>Response headers.
bodystringResponse body as a string — call JSON.stringify() yourself.

Hono support (recommended)

The Hono web framework works natively in workerd and is the recommended way to build Mercio functions with routing, middleware, and typed responses.

index.ts
import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => c.json({ ok: true }))

app.get('/hello/:name', (c) =>
  c.json({ message: `Hello, ${c.req.param('name')}!` })
)

app.post('/data', async (c) => {
  const body = await c.req.json()
  return c.json({ received: body })
})

export default app

Rules when using Hono:

  • Set "type": "module" in package.json and add hono as a dependency.
  • Run npm install locally before zipping — esbuild resolves Hono from your local node_modules.
  • Use entry=index.ts (or .js) when uploading.
  • All Hono features work: routing, middleware, path params, c.json(), c.text(), etc.

Zip and upload a Hono function

bash
cd my-hono-app
npm install
zip -r ../my-hono-app.zip .

curl -X POST https://api.yourdomain.com/api/mercio/upload \
  -H "Authorization: Bearer $TOKEN" \
  -F "zip=@../my-hono-app.zip" \
  -F "name=my-hono-app" \
  -F "entry=index.ts"

Configuration

Set these in apps/mercio-runtime/.env.

VariableDefaultDescription
REDIS_HOSTlocalhostRedis host for BullMQ
REDIS_PORT6379Redis port
R2_ENDPOINTCloudflare R2 S3-compatible endpoint URL
R2_ACCESS_KEY_IDR2 access key
R2_SECRET_ACCESS_KEYR2 secret key
R2_BUCKET_NAMER2 bucket name
WORKERD_BINautoAbsolute path to the workerd binary (auto-resolved from npm package)
MERCIO_CACHE_DIR/var/mercioDirectory for worker.mjs cache and capnp configs
MERCIO_POOL_MAX20Maximum simultaneous warm workerd processes
MERCIO_IDLE_TTL_MS300000Kill a process idle longer than this (5 min default)

Limits & constraints

  • Zip upload size: 50 MB maximum.
  • No filesystem writes at runtime — workerd V8 isolates have no disk access. Read-only use of bundled assets is fine.
  • State is in-memory and per-process. Do not rely on state surviving a cold start, a pool eviction, or a process crash.
  • Pool max: 20 warm processes by default (MERCIO_POOL_MAX). Least-recently-used processes are evicted when the pool is full.
  • Idle processes are killed after 5 minutes of inactivity (MERCIO_IDLE_TTL_MS).
  • Concurrency: 8 invocation jobs execute simultaneously inside mercio-runtime (BullMQ worker concurrency).