Orkait's email service. Cloudflare Worker. Multi-provider. Transactional + marketing. Scoped API keys.
An HTTP service you POST JSON to, and it sends an email. No SDK. No SMTP config. No per-product email logic scattered across 12 repos.
- Ships pre-built transactional templates (verify-email, password-reset, api-key-request, api-key-delivery)
- Ships pre-built marketing templates (welcome, announcement, newsletter)
- Providers: Resend and SendGrid, swappable at runtime via env
- Strategies: single / failover / round-robin / priority with caps
- Scoped API keys stored in D1 (hashed), revocable, optional monthly quota, optional expiry
- Admin endpoints to create / list / patch / revoke keys
- IP-based brute-force throttle on auth
- CORS strict-mode
- Audit log for admin actions
Lives at a single Worker URL. One binary. Add a template file, deploy, done.
| Route | Method | Auth | Scope |
|---|---|---|---|
/v1/health |
GET | - | - |
/v1/templates |
GET | API key | templates:read |
/v1/transactional/send |
POST | API key | transactional:send |
/v1/marketing/send |
POST | API key | marketing:send |
/v1/raw/send |
POST | API key | raw:send |
/v1/admin/keys |
POST, GET | admin token | - |
/v1/admin/keys/:id |
GET, PATCH | admin token | - |
/v1/admin/keys/:id/revoke |
POST | admin token | - |
/v1/admin/audit |
GET | admin token | - |
API keys use Authorization: Bearer mb_.... Admin uses Authorization: Bearer mba_....
Full scope matrix
| Scope | Grants access to |
|---|---|
transactional:send |
POST /v1/transactional/send |
marketing:send |
POST /v1/marketing/send |
raw:send |
POST /v1/raw/send |
templates:read |
GET /v1/templates |
* |
all of the above (wildcard) |
Each key gets a JSON array of scopes. Least-privilege is the default play - give a landing page key only transactional:send, a newsletter tool only marketing:send, etc. If a key leaks, the blast radius stays small.
Transactional (user-triggered, 1-to-1)
| Name | Purpose | Required context |
|---|---|---|
verify-email |
Email confirmation link | verificationUrl, optional userName, expiryHours |
password-reset |
Password reset link | resetUrl, optional userName, expiryHours |
api-key-request |
Admin notification when someone requests an API key | requesterName, requesterEmail, useCase |
api-key-delivery |
Hand-off the key itself to approved requester | apiKey, product, optional docsUrl, playgroundUrl, quotaSummary |
Marketing (operator-triggered, bulk)
| Name | Purpose | Required context |
|---|---|---|
welcome |
Post-signup onboarding | product, optional tagline, nextSteps[], ctaLabel/ctaUrl |
announcement |
Launch / feature / update blast | headline, optional subheadline, bodyHtml or bodyText, highlights[], ctaLabel/ctaUrl, imageUrl |
newsletter |
Periodic digest | items[] (each with title, summary, url, tag), optional issue, intro, footerNote |
All templates are zod-validated. company defaults to the DEFAULT_COMPANY env var unless you override it per request. Every marketing template accepts unsubscribeUrl and renders a footer link.
bun install
# First time: create the D1 database
wrangler d1 create mailbuddy-keys
# Copy database_id into wrangler.toml
wrangler d1 execute mailbuddy-keys --remote --file=migrations/0001_init.sql
wrangler d1 execute mailbuddy-keys --remote --file=migrations/0002_quota_ratelimit_audit.sql
# Secrets
bun run secret:resend # RESEND_API_KEY
bun run secret:sendgrid # SENDGRID_API_KEY (optional, for failover)
bun run secret:admin # ADMIN_TOKEN for /v1/admin/*
bun run deployLive at https://<worker-name>.<subdomain>.workers.dev.
curl -X POST https://mailbuddy.<sub>.workers.dev/v1/admin/keys \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "rustbox-landing",
"scopes": ["transactional:send"],
"monthlyQuota": 1000
}'Response contains rawKey - save it. It is never shown again (hash only in D1).
Revoke any key by id:
curl -X POST https://mailbuddy.<sub>.workers.dev/v1/admin/keys/<id>/revoke \
-H "Authorization: Bearer $ADMIN_TOKEN"Transactional (template-driven)
curl -X POST https://mailbuddy.<sub>.workers.dev/v1/transactional/send \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{
"template": "verify-email",
"to": "[email protected]",
"context": {
"userName": "Jane",
"verificationUrl": "https://app.example.com/verify?token=abc"
}
}'Marketing (bulk, up to 100 recipients per request)
curl -X POST https://mailbuddy.<sub>.workers.dev/v1/marketing/send \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{
"template": "announcement",
"recipients": ["[email protected]", "[email protected]"],
"campaignId": "2026-04-beta-launch",
"context": {
"headline": "Rustbox Beta is live",
"subheadline": "Cloud runtime for untrusted code",
"bodyText": "Eight languages, 36ms median latency, evidence-backed verdicts.",
"ctaLabel": "Open playground",
"ctaUrl": "https://rustbox.orkait.com/playground"
}
}'campaignId flows through as a Resend tag / SendGrid category so you can filter per-campaign later.
Raw (no template, you provide html + text)
curl -X POST https://mailbuddy.<sub>.workers.dev/v1/raw/send \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "[email protected]",
"subject": "Hi",
"html": "<h1>Hello</h1>",
"text": "Hello"
}'Escape hatch for anything the template registry doesn't cover yet.
Component overview
Request -> CORS guard -> Auth (scope check) -> Rate limit on fail -> Route
|
v
Quota check + reserve (D1)
|
v
Template render (zod)
|
v
Strategy + Provider
|
v
Resend / SendGrid REST
- Providers extend
BaseEmailProvider(justsend+verify). Both Resend and SendGrid hit the REST API viafetch- no SDK, Worker bundle stays tiny. - Strategies pick which provider to use per send. Swap via
EMAIL_STRATEGYenv. Priority strategy caps each provider at a monthly limit before falling through. - Templates live in
src/templates/<category>/<name>.ts. Each has a zod schema, a subject function, and a render function returning{ html, text }. Register intemplates/registry.ts. - KeyStore is the D1 wrapper. Hashes keys with SHA-256 before insert. Atomic quota counter.
- RateLimiter tracks auth failures per IP per minute in a dedicated D1 table. Only records on failure, skips rate-check on valid token (so legit calls never lock themselves out).
- AuditLog writes to
admin_auditon every admin mutation. Includes IP, user-agent, action meta.
- Create
src/templates/<category>/<name>.ts:
import { z } from "zod";
import { createTemplate } from "../base";
import { ctaButton, paragraph, wrapLayout } from "../layout";
const CtxSchema = z.object({
company: z.string(),
userName: z.string().optional(),
foo: z.string(),
});
export const myTemplate = createTemplate(
"my-template",
"transactional",
CtxSchema,
(ctx) => `Subject with ${ctx.foo}`,
(ctx) => {
const content = paragraph(`Hi ${ctx.userName ?? "there"}`);
return {
html: wrapLayout(content, { company: ctx.company }),
text: `Hi ${ctx.userName ?? "there"}`,
};
}
);- Register in
src/templates/registry.ts:
import { myTemplate } from "./transactional/my-template";
const all: EmailTemplate[] = [
// ...existing
myTemplate as unknown as EmailTemplate,
];bun run deploy./v1/templatesnow lists it.
Layout helpers (wrapLayout, ctaButton, paragraph, linkFallback) give every template a consistent dark-mode-aware shell.
Extend BaseEmailProvider
src/providers/<name>.ts:
import { BaseEmailProvider } from "./base";
import type { EmailData, EmailResult } from "../types";
export class MyProvider extends BaseEmailProvider {
async send(data: EmailData): Promise<EmailResult> {
// POST to provider REST, return { success, messageId, provider }
}
async verify(): Promise<boolean> {
// Ping provider to validate key
}
}- Wire in
src/providers/index.tsbuildProviders()switch. - Add env var for API key +
ENABLED_PROVIDERSentry.
wrangler.toml vars
| Var | Default | Purpose |
|---|---|---|
EMAIL_STRATEGY |
single |
single / failover / roundrobin / priority |
ENABLED_PROVIDERS |
resend |
Comma list: resend, sendgrid |
DEFAULT_FROM |
Orkait <[email protected]> |
Fallback sender |
DEFAULT_REPLY_TO |
[email protected] |
Fallback Reply-To |
DEFAULT_COMPANY |
Orkait |
Injected into template context when not provided |
ALLOWED_ORIGINS |
rustbox domains | CORS allowlist, comma-separated |
PROVIDER_LIMITS |
{} |
JSON caps per provider for priority strategy |
| Secret | Required | Purpose |
|---|---|---|
ADMIN_TOKEN |
yes | Bearer for /v1/admin/* |
RESEND_API_KEY |
if resend enabled | Resend provider |
SENDGRID_API_KEY |
if sendgrid enabled | SendGrid provider |
- Never stored plaintext - API keys are SHA-256 hashed before insert; raw key is returned once on creation.
- Timing-safe comparisons for admin token check.
- Rate limit on auth failures - 10 per minute per IP (D1-backed). Valid tokens skip the check entirely.
- Scoped least privilege - every caller key has an explicit scope list.
- CORS strict-mode - browser requests from non-allowlisted origins get 403.
- Audit trail - every admin mutation writes to
admin_auditwith IP, UA, action meta. - Soft revoke with
revoked_attimestamp, so you keep history. - Optional expiry per key.
- Optional monthly quota per key, lazy-reset on next send after window elapses.
mailbuddy/
βββ migrations/ D1 SQL migrations
β βββ 0001_init.sql
β βββ 0002_quota_ratelimit_audit.sql
βββ src/
β βββ index.ts Worker entry + routing
β βββ env.ts Env interface
β βββ auth.ts admin + caller auth
β βββ scopes.ts scope matrix + validation
β βββ keystore.ts D1 wrapper for api_keys
β βββ ratelimit.ts auth failure tracking
β βββ audit.ts admin action log
β βββ cors.ts strict-mode CORS
β βββ constants.ts
β βββ email-service.ts orchestrator
β βββ utils/
β βββ providers/ base + resend + sendgrid
β βββ strategies/ single + failover + round-robin + priority
β βββ templates/
β β βββ base.ts, layout.ts, registry.ts
β β βββ transactional/
β β βββ marketing/
β βββ routes/ health, templates, raw, transactional, marketing, admin
βββ wrangler.toml
βββ tsconfig.json
βββ package.json
βββ README.md
bun install
bun run type-check # tsc --noEmit
bun run dev # wrangler dev (local)
bun run deploy # wrangler deployNo test suite yet - this is an early-stage service. If you want to add one, bun test + a Worker-local harness (Miniflare or the built-in Wrangler test runner) are both viable.
Internal. Orkait proprietary. Do not redistribute.