Skip to content

orkait/mailbuddy

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

16 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ“¬ mailbuddy

Orkait's email service. Cloudflare Worker. Multi-provider. Transactional + marketing. Scoped API keys.

Cloudflare Workers TypeScript Bun Resend SendGrid D1 Zod


πŸ€” What it is

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.


πŸ—ΊοΈ Endpoints

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_....


πŸ›‘οΈ Scopes

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.


πŸ“ Templates

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.


πŸš€ Quick start

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 deploy

Live at https://<worker-name>.<subdomain>.workers.dev.


πŸ”‘ Creating a key

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"

πŸ“€ Sending mail

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.


🧱 Architecture

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 (just send + verify). Both Resend and SendGrid hit the REST API via fetch - no SDK, Worker bundle stays tiny.
  • Strategies pick which provider to use per send. Swap via EMAIL_STRATEGY env. 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 in templates/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_audit on every admin mutation. Includes IP, user-agent, action meta.

🧩 Adding a template

  1. 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"}`,
    };
  }
);
  1. Register in src/templates/registry.ts:
import { myTemplate } from "./transactional/my-template";

const all: EmailTemplate[] = [
  // ...existing
  myTemplate as unknown as EmailTemplate,
];
  1. bun run deploy. /v1/templates now lists it.

Layout helpers (wrapLayout, ctaButton, paragraph, linkFallback) give every template a consistent dark-mode-aware shell.


πŸ”Œ Adding a provider

Extend BaseEmailProvider
  1. 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
  }
}
  1. Wire in src/providers/index.ts buildProviders() switch.
  2. Add env var for API key + ENABLED_PROVIDERS entry.

βš™οΈ Configuration

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

Secrets (wrangler secret put)

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

πŸ”’ Security model

  • 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_audit with IP, UA, action meta.
  • Soft revoke with revoked_at timestamp, so you keep history.
  • Optional expiry per key.
  • Optional monthly quota per key, lazy-reset on next send after window elapses.

πŸ—‚οΈ Repository layout

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

πŸ› οΈ Developing

bun install
bun run type-check       # tsc --noEmit
bun run dev              # wrangler dev (local)
bun run deploy           # wrangler deploy

No 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.


πŸ“„ License

Internal. Orkait proprietary. Do not redistribute.

About

Orkait email service. Cloudflare Worker. Multi-provider (Resend + SendGrid). Transactional + marketing templates. Scoped API keys with D1.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages