Changelog
All notable changes to MailSink will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Versioning policy:
0.y.z— MVP / pre-launch. Public API may break.1.0.0— first public launch with stable public API commitment.- After
1.0.0:- MAJOR — breaking API changes (removed endpoints, renamed params, response-shape changes that break clients).
- MINOR — backward-compatible new endpoints, fields, plan changes.
- PATCH — bug fixes, internal refactors, landing copy, docs.
Commit convention: Conventional Commits.
Types used: feat, fix, deploy, config, auth, docs, chore.
Unreleased
0.3.1 - 2026-04-20
Changed
- Stripe flipped to live mode. New products (
prod_UN4uE1wIjEocWBPro,prod_UN4ucs3uOS7IrDTeam) and prices (price_1TOKkDI0C5wI3kssPoZXPLUqPro $15/mo,price_1TOKkFI0C5wI3ksswx7aINdSTeam $49/mo) created.STRIPE_SECRET_KEYandSTRIPE_WEBHOOK_SECRETrotated to live (sk_live_…,whsec_…). Test-mode webhook removed. Storedstripe_customer_idrows cleared since they were test-mode only. - Checkout Session now has
allow_promotion_codes: trueso coupons work at the Stripe-hosted checkout page.
0.3.0 - 2026-04-20
Added
- Session cookie auth —
mailsink_session(HttpOnly, Secure, SameSite=Lax, Domain=mailsink.dev, 30-day TTL). Stored in KV (session:<id>→{account_id, created_at}). Set automatically on the OAuth callback so returning users are recognised across subdomains. authMiddlewarenow accepts eitherAuthorization: Bearer msk_…or a valid session cookie. Programmatic clients unchanged; the dashboard can call/v1/*withcredentials: "include"and no header.GET /v1/me— returns current account, plan, this-month usage, and the plan's concrete limits (inboxes/month, rate limit, TTL, email size, MCP entitlement). Used by the dashboard + pricing page to render the right CTAs for logged-in users.POST /v1/billing/checkout(authed) — creates a Stripe Checkout session for the caller's account. Body:{ "plan": "pro" | "team" }. No more unauthenticatedaccount_idin the request.POST /v1/billing/portal(authed) — Stripe Customer Portal session for subscribers. Return URL points back at/app.POST /auth/logout— clears session cookie and deletes KV entry.- Landing dashboard
/app— three states:- Not signed in → GitHub sign-in panel.
- New signup (OAuth callback redirect with
#key=…) → one-time key reveal card above the dashboard. - Returning user → plan badge, usage grid (inboxes/messages/rate/TTL), and contextual actions (Upgrade to Pro/Team, Manage billing, Sign out).
- Home pricing cards now detect the session on load and swap CTAs. Logged in: "Upgrade to Pro/Team" triggers Stripe Checkout in one click; current plan is badged; lower-tier card becomes "Go to dashboard".
Changed
POST /billing/checkoutandPOST /billing/portalmoved from public namespace to/v1/billing/*and now require authentication. Previously anyone could POST with a targetaccount_idand create a Checkout for someone else's account.- CORS now allows credentials (
Access-Control-Allow-Credentials: true) while still reflecting the request origin. SameSite=Lax on the session cookie is the real CSRF defense — cross-sitefetch()can't attach the cookie even with credentials enabled. accounts.github_usernameis now selected by the auth middleware and surfaced in/v1/meso the dashboard can greet the user by handle.
Fixed
- Returning users no longer need to re-authenticate on every visit to manage their account. Previously there was no session state at all — OAuth only minted an API key and then threw the user back to the landing page with no way to upgrade, view usage, or open Customer Portal without CLI curl calls.
0.2.1 - 2026-04-20
Added
/changelogpage — renders this file at build time viamarked./securitypage — transport/storage/auth model, subprocessors (Cloudflare, Stripe, GitHub), responsible-disclosure contactsecurity@mailsink.dev, explicit non-claims (no SOC 2, no data residency guarantees beyond CF)./statuspage — client-side live checks againstapi.mailsink.dev/v1/healthand the landing origin, plus links to upstream status pages (Cloudflare, Stripe, GitHub).
Fixed
/changelog,/security,/statuswere linked from navigation but returned- Stubs are now real pages.
0.2.0 - 2026-04-20
Added
GET /v1/inboxes/:id/wait-for-code— long-poll endpoint that blocks up to 60s waiting for an OTP to arrive. Query param?timeout=30(default 30, max 60). Returns the same shape as/latest-codeon success, or{code: null, timeout: true, waited_seconds: N}on timeout. Polls D1 every 1s while waiting.GET /v1/inboxes/:id/wait-for-link— symmetric long-poll for verification links.
Fixed
- Landing pages and README previously advertised
/wait-for-codein all code examples (curl, Node, Python) but only/latest-codewas implemented. Endpoints now exist.
0.1.0 - 2026-04-20
First feature-complete MVP milestone. Deployed to production infrastructure running against Stripe test mode for billing smoke tests. Not yet publicly announced.
Added
- Ingress: Cloudflare Email Routing →
mailsink-emailWorker catch-all on 3 shared domains (codenotify.net,letterhub.net,mailkite.net). Raw email stored in R2, metadata in D1. - API (
api.mailsink.dev): Hono worker with 7 tables on D1 + R2 blob storage + KV rate limiting.POST /v1/inboxes— provision throwaway address with TTL.GET /v1/inboxes— list active inboxes.DELETE /v1/inboxes/:id— burn an inbox.GET /v1/inboxes/:id/messages— list received mail.GET /v1/inboxes/:id/latest-code— extract OTP from the latest message.GET /v1/inboxes/:id/latest-link— extract verification link.GET /v1/messages/:id//raw— full message or raw.eml.DELETE /v1/messages/:id.GET/POST/DELETE /v1/domains— user-owned domain management (stubbed for BYOD waitlist; feature-flagged off).GET/POST/DELETE /v1/keys— API key management.GET /v1/health.
- Auth: GitHub OAuth end-to-end (
/auth/github→ callback → API key + redirect tomailsink.dev/app). One-time key reveal. - Billing: Stripe Checkout Sessions (
/billing/checkout) + webhook handler (/billing/webhook) forcheckout.session.completed+customer.subscription.updated+customer.subscription.deleted. Pro ($15/mo) and Team ($49/mo) plans. - Rate limits: KV-backed per-minute per-plan (60/600/3000 for Free/Pro/Team).
Exposes
X-RateLimit-Limit/Remainingheaders (CORS-allowed). - Smart extraction: OTP + magic-link heuristics applied at email ingest.
- Landing (
mailsink.dev): Astro static site on Cloudflare Pages. Home +/app+/docs+/privacy+/terms. - Hourly cleanup cron: expires stale inboxes + messages per plan TTL.
- Shared MCP package scaffold for AI agent integration (not yet published).
Changed
- BYOD (Bring Your Own Domain) hidden from public UI for v0.1 — deferred to
post-launch. Landing links to
mailto:hello@mailsink.dev?subject=BYOD waitlist. - API CORS allows
X-Internal-Keyheader and exposes rate-limit headers so client SDKs can back off gracefully.
Fixed
stripeRequesthelper now recursively flattens nested objects to Stripe bracket notation (metadata[account_id]=xxx) instead of serializing to[object Object]. Without this, customer creation during checkout silently failed and the downstream.bind(undefined)crashed withD1_TYPE_ERROR. (abf269e)/domainsduplicate check now runs before the plan-count cap, so users get a clearer error when re-adding an existing domain. (5523ee1)
Known issues / deferred to next version
- No OG image or favicon.
- MCP package not yet published or smoke-tested against prod.
- Stripe is still in test mode; must flip to live before public launch (see project memory for the migration checklist).