Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

SaaS Starter API — Bun + TypeScript + Cloudflare

A production-grade backend foundation built with Bun, TypeScript, Hono and the Cloudflare Workers ecosystem (D1, KV, R2, Queues, Cron Triggers). Designed for a small engineering team to take a real SaaS product from zero to production and scale over time.


1. Project overview

This is not a CRUD demo. It ships with:

  • Versioned REST API (/api/v1) with OpenAPI docs generated from route definitions
  • Auth module: JWT access tokens, rotating refresh tokens, API keys (nb_live_/nb_test_)
  • Users + Organizations (multi-tenant authorization model: user → organization → role)
  • File uploads to R2 with ownership checks and content-type allowlists
  • Inbound webhooks: HMAC signature verification, replay protection, idempotent processing
  • Background jobs via Cloudflare Queues (with DLQ) and Cron-based cleanup
  • Structured JSON logging, request correlation ids, rate limiting, security headers, CORS allowlist, request-size caps, idempotency-key support for writes

2. Architecture

HTTP request
  → env validation → request id → security headers → CORS → access log
  → rate limit (KV fixed window)
  → auth middleware (JWT | API key) → authorization (roles/scopes)
  → validation (Zod) → thin handler → service → repository → D1/R2/KV/Queue
  → response envelope / global error handler

Layer responsibilities:

Layer Responsibility
Route OpenAPI definition, validation wiring, HTTP concerns only
Controller Folded into thin handlers (see note below)
Service Business logic, orchestration, transactions
Repository Data access exclusively (Drizzle queries)

Note on controllers: @hono/zod-openapi couples validation, response typing and the handler in one place. Adding a separate controller file would create pass-through-only modules, so controllers are folded into 5-line route handlers; all business logic still lives in services and all SQL lives in repositories.

Key decisions:

  • D1 as primary database — SQLite at the edge, no connection pooling needed at any scale you'll hit early; Drizzle provides typed schema/migrations. Swap to Neon/Supabase/Turso by replacing src/db/index.ts + repositories (the rest of the codebase never touches SQL drivers).
  • JWT via Web Crypto (HS256), passwords via PBKDF2 — no Node-only crypto dependencies; runs identically in Workers and Bun.
  • Rate limiting: KV fixed window — eventual consistency means limits can be slightly exceeded under high concurrency; fine for abuse protection. For hard quotas, replace KvRateLimiter with a Durable Object implementation behind the same RateLimiter interface.
  • Durable Objects intentionally not used — architecture supports adding them later.
  • Email delivery is queue-backed with pluggable providers (console by default, Resend included); add your provider account before relying on it in production.

3. Technology stack

Concern Choice
Runtime/PM Bun
Language TypeScript (strict, noUncheckedIndexedAccess)
Framework Hono + @hono/zod-openapi
Validation Zod
Database Cloudflare D1 + Drizzle ORM
Cache/Limits Cloudflare KV
Storage Cloudflare R2 behind a StorageService interface
Jobs Cloudflare Queues (+ DLQ) & Cron Triggers
Lint/Format Biome
Tests bun test (unit / integration / e2e)
CI/CD GitHub Actions

4. Prerequisites

  • Bun ≥ 1.1
  • A Cloudflare account (free tier works locally; paid for queues/cron in production)
  • wrangler (bundled as dev dependency)

5. Installation

git clone <your-repo-url> && cd api
bun install
cp .dev.vars.example .dev.vars    # fill in local secrets

6. Environment configuration

Variable Where Required Notes
ENVIRONMENT wrangler [vars] yes development | staging | production
LOG_LEVEL wrangler [vars] yes debug | info | warn | error
ALLOWED_ORIGINS wrangler [vars] yes Comma-separated CORS allowlist (no wildcard)
JWT_SECRET wrangler secret yes ≥ 32 chars; openssl rand -hex 32
TURNSTILE_SECRET_KEY wrangler secret optional Enables server-side Turnstile verification
EMAIL_PROVIDER wrangler [vars] optional console (default) or resend
RESEND_API_KEY wrangler secret optional Needed when EMAIL_PROVIDER=resend

Environment validation (src/config/env.ts, Zod) fails fast per isolate when critical configuration is missing or malformed. Secrets are never committed — see .env.example.

7. Local development

bun run dev              # wrangler dev (D1/KV/R2/Queues all simulated locally)
bun run typecheck
bun run lint             # biome check
bun run format
bun test                 # unit + integration + e2e
bun run test:watch

Queue consumers run automatically under wrangler dev; cron triggers can be tested with wrangler dev --test-scheduled then curl http://localhost:8787/__scheduled?cron=0+3+*+*+*.

8. Database setup

bun run db:generate        # drizzle-kit generate → migrations/*.sql (review the diff!)
bun run db:migrate:local   # apply to local D1
bun run db:migrate:staging # apply to remote staging D1
bun run db:migrate:production
bun run db:studio          # browse schema/data
bun run db:seed:local      # seed dev data

Safety rules:

  • db:migrate:* applies forward-only migrations from migrations/.
  • Never hand-edit an applied migration file; generate a new one instead.
  • Destructive changes (drop column/table): generate the migration, review the SQL diff in the PR, verify a backup exists (wrangler d1 time-travel info), and coordinate the deploy.
  • Production deploys run migrations before the worker deploy; a failed migration aborts deploy.

9. Cloudflare setup (one-time)

bunx wrangler login

# Dev/staging/production databases & namespaces — update IDs in wrangler.toml:
wrangler d1 create api-dev && wrangler d1 create api-staging && wrangler d1 create api-production
wrangler kv namespace create CACHE            # repeat per environment
wrangler r2 bucket create api-dev-uploads     # repeat per environment
wrangler queues create api-dev-jobs           # repeat per environment (+ DLQs)

# Secrets (per environment):
wrangler secret put JWT_SECRET --env staging
wrangler secret put JWT_SECRET --env production

10. Testing

Tests run against the real Hono app with a minimal D1-compatible shim over bun:sqlite, so repositories execute real SQL against the real migrations — no query mocks.

tests/
  unit/         # crypto, pagination cursors, errors, rate limiter, webhook verifier, logger redaction
  integration/  # (reserved) repository/service-level DB tests using tests/helpers/env.ts
  e2e/          # full API flows: auth lifecycle, rotation reuse, RBAC, API keys, validation, headers/CORS
  helpers/      # D1 shim + KV/R2/Queue stubs

11. Deployment

bun run build              # dry-run bundle validation
bun run deploy:staging
bun run deploy:production

CI/CD flow: feature branch → PR (lint/typecheck/tests/build) → merge to main auto-deploys staging → manual workflow_dispatch promotes to production (protected environment). See .github/workflows/deploy.yml header for required secrets and rollback commands (wrangler rollback, D1 time travel).

12. CI/CD

.github/workflows/ci.yml fails on lint errors, type errors, test failures, or invalid Wrangler config. Deployments require CLOUDFLARE_API_TOKEN (least privilege: Workers Scripts, D1, KV, R2, Queues edit on the relevant resources only) and CLOUDFLARE_ACCOUNT_ID.

13. API documentation

  • GET /api/v1/openapi.json — spec generated from route definitions (never drifts)
  • GET /docs — Scalar UI
  • Auth: Authorization: Bearer <jwt> or Authorization: Bearer nb_live_/nb_test_...
  • All responses use { success: true, data } / { success: false, error: { code, message, details?, requestId? } }
  • Rate-limit headers: X-RateLimit-Limit|Remaining|Reset; 429 includes Retry-After
  • Idempotency: send Idempotency-Key: <uuid> on POST/PATCH; replays return the cached response for 24h; key reuse with a different payload returns 409

14. Folder structure

src/
├── index.ts                  # fetch/queue/scheduled entrypoint
├── app/                      # bootstrap, router mounts, shared context types
├── config/                   # typed env validation, constants
├── db/                       # drizzle client + schema (users, orgs, sessions, keys, uploads…)
├── middleware/               # request-id, logging, security, error, rate-limit, auth, idempotency
├── modules/
│   ├── auth/                 # routes, service, schemas, sessions/api-keys repositories
│   ├── users/
│   ├── organizations/
│   ├── uploads/              # routes + repository (R2 via storage.service)
│   ├── webhooks/             # verifier (HMAC+replay), service, routes
│   └── health/               # /health, /ready
├── services/                 # cache, queue producer, email providers, storage, turnstile
├── jobs/                     # queue consumer (dispatch by type) + scheduled cleanup
├── lib/                      # logger, crypto, pagination, response envelopes, openapi helpers
├── repositories/             # cross-module data access (users)
├── types/                    # Env bindings type
└── utils/                    # errors hierarchy, dates, strings
migrations/                   # generated SQL (drizzle-kit)
scripts/                      # seed.sql
tests/{unit,integration,e2e}
.github/workflows/{ci,deploy}.yml

15. Common commands

Command What it does
bun run dev Local worker with live reload
bun run build Dry-run worker bundle
bun run typecheck tsc --noEmit
bun run lint / format Biome
bun run test[:watch] Test suite
bun run db:* Migrations/studio/seed (see section 8)
bun run deploy:* Staging/production deploys

16. Production checklist

[ ] ENVIRONMENT=production, LOG_LEVEL=info set in wrangler.toml [env.production]
[ ] ALLOWED_ORIGINS lists exact production origins (no wildcards)
[ ] Secrets added via wrangler secret put: JWT_SECRET (+ TURNSTILE_SECRET_KEY, RESEND_API_KEY if used)
[ ] Production D1 database created; migrations reviewed and applied
[ ] KV namespace, R2 bucket, Queue + DLQ created and bound
[ ] Custom domain configured (routes in wrangler.toml)
[ ] Rate limits reviewed for expected traffic (config/constants.ts)
[ ] Logging verified via `wrangler tail`; log shipper connected if used
[ ] Error tracking hooked into error.middleware.ts if desired
[ ] GET /health and GET /ready return 200 after deploy
[ ] CI/CD deployment tested end-to-end (secrets present, health check passes)
[ ] Rollback strategy documented and rehearsed (wrangler rollback + D1 time-travel)
[ ] Email provider configured and tested (default console provider does NOT deliver)
[ ] Turnstile enabled on public forms if bot protection is required

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages