Control-plane API service
services/api is the Bun + Hono control-plane API runtime for route families that have been moved out of Next.js ownership.
Use your OpenSend API key (os_...) with the Resend-compatible API surface.
Local development convention:
bun run dev:api
# service listens on http://localhost:3026 by default
Current endpoints:
POST /emails— transactional send API. Uses the shared send implementation that also backs the compatibility Next.js adapter atPOST /api/emails; preserves OpenSend/Resend-compatible auth, validation, idempotency, queueing, response, and error shapes.POST /emails/batch— transactional batch send API with the same shared behavior asPOST /api/emails/batch.GET /suppressions— list user-scoped suppression records for a full-access API key or dashboard session. Matches the compatibility Next.js adapter atGET /api/suppressions, includinglimit/afterpagination and response fields.DELETE /suppressions/:email— remove a user-scoped suppression for a full-access API key or dashboard session. Matches the compatibility Next.js adapter atDELETE /api/suppressions/:email, including404 { error, code }not-found behavior.GET /healthz— service/version health metadataGET /readyz— static readiness response that does not require AWS, database, queue, or other external credentialsPOST /mcp— Streamable HTTP-compatible JSON-RPC MCP endpoint for agent clients. RequiresAuthorization: Bearer <opensend_api_key>and forwards tool calls to the existing public OpenSend API (OPENSEND_API_BASE_URL, defaulthttp://localhost:3015).
The transactional send and suppression route families are now owned by shared handlers consumed by this Hono service. The existing Next.js handlers remain compatibility adapters for the current public /api/emails and /api/suppressions URLs; no production routing cutover is implied by local service ownership.
Next.js still owns dashboard-only and not-yet-migrated public route families, including API keys, contacts/audience, broadcasts, email read/detail, billing, Better Auth, internal cron, and dashboard UI routes. Webhooks remain available in this service through the current shared adapter.
The stable send/batch DTO, validation, response, and public error-envelope boundary is documented in ../../docs/public-send-contract.md and implemented under packages/core/src/contracts/; adapter code should consume that boundary rather than importing Next.js app-local validation internals.
Transactional send local testing
Start the Hono service on the default port:
bun run dev:api
# http://localhost:3026
Exercise the service route with an OpenSend API key:
bun -e 'const r = await fetch("http://localhost:3026/emails", { method: "POST", headers: { "authorization": "Bearer os_...", "content-type": "application/json" }, body: JSON.stringify({ from: "sender@example.com", to: "recipient@example.com", subject: "Hello", html: "
Hello
" }) }); console.log(r.status, await r.text())'Focused tests for the transactional send route family live in tests/api-emails.test.ts and cover both the Hono service routes and the Next.js compatibility adapters:
bun run test -- tests/api-emails.test.tsFocused suppression parity tests live in tests/suppressions-route.test.ts:
bun run test -- tests/suppressions-route.test.tsThin-adapter pilot pattern
The API keys route family is the first narrow thin-adapter pilot for issue #71:
src/app/api/api-keys/**/route.tsowns only request authorization, JSON/query/route-param parsing, service invocation, and HTTP response mapping.packages/core/src/services/apiKeys.tsowns API-key business rules: pagination bounds, create validation, token generation/hash/preview construction, repository orchestration, not-found semantics, and cache-invalidation hook invocation.- The service accepts explicit dependencies, so behavior can be unit-tested without a Next.js request object and later reused by the Hono control-plane runtime.
Follow-up route moves should preserve this split before adding Hono handlers: keep public API shape stable, move business logic into core or a service layer, then let each runtime provide only an adapter.
MCP server slice
This workspace includes a private local @opensend/mcp package used by the control-plane service and by stdio clients. It intentionally exposes only the first parity slice backed by stable public API routes: send/list/get emails, create/list/get contacts, create/list/get domains, and create/list/get webhooks. Tool calls preserve the existing public API response or error body inside MCP content[0].text with { status, ok, body }.
Codex stdio setup
codex mcp add opensend \
--env OPENSEND_API_KEY=os_... \
--env OPENSEND_API_BASE_URL=http://localhost:3015 \
-- bun /path/to/opensend/packages/mcp/src/stdio.tsJSON-config stdio setup
{
"mcpServers": {
"opensend": {
"command": "bun",
"args": ["/path/to/opensend/packages/mcp/src/stdio.ts"],
"env": {
"OPENSEND_API_KEY": "os_...",
"OPENSEND_API_BASE_URL": "http://localhost:3015"
}
}
}
}Streamable HTTP setup
Run the control-plane API and point clients at /mcp:
OPENSEND_API_BASE_URL=http://localhost:3015 bun run dev:apiHTTP MCP clients must send their OpenSend API key per request:
POST http://localhost:3026/mcp
Authorization: Bearer os_...
Content-Type: application/json
{"jsonrpc":"2.0","id":"tools","method":"tools/list"}Deferred from this slice: received emails, broadcasts, segments, topics, contact properties, API-key management, and packaging/publishing ownership for a public npx command.