API service reference
A Node API with signed webhooks, jobs with a dead-letter queue and an outbound partner call, on the SOC 2 baseline.
Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.
Stability The promise an interface ships with.
- Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
- Beta Usable in production; may change before it becomes stable.
- Experimental May change or go away; needs an explicit opt-in in main.yaml.
Start from it: npx mainsdk init --template mainsdk/api-service
The reference architecture for an HTTP API service: a verified inbound webhook, background jobs that retry and then land in a dead-letter queue, and sign-in for the routes people call. Pack: SOC 2 baseline.
The handler is a plain fetch function, so Hono or Express can sit in front of it. The interfaces (webhooks, jobs, auth, http) stay the same.
Runs with no account and no provider key: npm run dev serves it on local drivers. npm test runs the tests: a delivery with a bad signature is refused before the handler, a job that fails three times lands in the dead-letter queue.
Every change and every MainSDK release runs .github/workflows/reference.yml: mainsdk check, mainsdk drill restore --to scratch and mainsdk assure --standard soc2, npm run bench and npm run chaos (the partner down, slow or answering nonsense must never lose a delivery). npm run verify-ci fails when a job is removed.
Moving off Express with raw provider SDKs? mainsdk migrate raw ends here.
Performance
The benchmark harness fails a change when p95 goes over 25 ms on local drivers (npm run bench).
main.yaml
schema: 1
project: prj_template_api_service
baseline: main27
auth:
driver: local
methods: [passkeys, link]
admin: { require: phishing-resistant }
db:
driver: local
jobs:
driver: local
dead-letter: true
webhooks:
inbound:
github: { preset: github, secret: secret:GITHUB_WEBHOOK_SECRET, path: /webhooks/github }
logging:
audit: true
retention: 12m
network:
allow: [status.partner.dev]
default: deny
mode: observe
http:
timeout: 2s
retries: { attempts: 1, backoff: 100ms, max: 1s }
# Evidence for `mainsdk assure --standard soc2`: change control is on (a pull request and a green gate before every merge),
# and the providers the app runs on are registered.
compliance:
change-control: true
providers: [local, ./backup.ts]
backup:
rpo: 15m
rto: 1h
keep: 7d
drill: { every: 30d, to: scratch }
driver: ./backup.tsapp.ts
// The API service reference architecture: an HTTP service with a verified inbound webhook, a background job that
// retries and then lands in the dead-letter queue, and sign-in for the routes people call. Hono or Express can
// replace the fetch handler; the interfaces (webhooks, jobs, auth) stay the same.
import { auth, http, jobs, MainError, webhooks } from 'mainsdk'
const synced: string[] = []
/**
* The job the webhook queues: ask the partner whether it is ready, then record the sync. A partner that is down, slow
* or answering nonsense fails the attempt; the queue retries it and then keeps it in the dead-letter queue, so no
* delivery is lost. A repository named `broken` always fails, to show the retries.
*/
async function syncRepo(input: { repo: string }): Promise<void> {
if (input.repo === 'broken')
throw new MainError('MAIN-APP-0001', { detail: `Could not sync ${input.repo}`, status: 502 })
const res = await http.fetch('https://status.partner.dev/v1/ready')
const body = (await res.json()) as { ready?: boolean }
if (!res.ok || body.ready !== true)
throw new MainError('MAIN-APP-0002', { detail: 'The partner is not ready', status: 503 })
synced.push(input.repo)
}
/** The inbound webhook: the signature is checked over the raw body before this runs; a bad one never gets here. */
async function onDelivery(body: unknown, meta: { id: string }): Promise<void> {
const repo = String((body as { repository?: { name?: string } }).repository?.name ?? '')
// One job per delivery: the provider's delivery id is the idempotency key, so a duplicate queues nothing.
await jobs.run('repo.sync', { repo }, { idempotencyKey: meta.id, retries: 2, backoff: 'fixed' })
}
/** Register the handlers on the running app. It is cheap and safe to repeat, so every request does it. */
function wire(): ReturnType<typeof webhooks.receive> {
jobs.on('repo.sync', syncRepo)
return webhooks.receive('github', onDelivery)
}
const json = (status: number, body: unknown): Response => Response.json(body, { status })
/** The service: its routes, with every interface refusal answered as a problem and never a crash. */
export default async function fetch(req: Request): Promise<Response> {
try {
const receive = wire()
const { pathname } = new URL(req.url)
if (req.method === 'GET' && pathname === '/health') return json(200, { ok: true })
if (req.method === 'POST' && pathname === '/webhooks/github') return await receive(req)
await auth.require(req)
if (req.method === 'GET' && pathname === '/repos') return json(200, { synced })
if (req.method === 'GET' && pathname === '/jobs/dead') return json(200, await jobs.dead.list())
return json(404, { error: 'not found' })
} catch (e) {
if (e instanceof MainError) return json(e.status, { code: e.code, title: e.title, fix: e.fix })
throw e
}
}Was this page helpful?