Problem
Short URL services accumulate four layers of complexity quickly: link ownership, workspace isolation, analytics collection, and edge redirect performance. When these concerns mix inside a single request handler, a redirect response blocks on database writes, analytics events become best-effort, and workspace boundaries leak through shared state.
Birdor Links is a workspace-first SaaS built on Cloudflare Workers. Its design constraint is simple: a redirect at /r/:slug must remain fast and fail-closed, while workspace management, link analytics, and background jobs can be slower and eventually consistent.
Summary
The architecture separates three concerns explicitly:
- Edge redirect path — KV lookup + D1 policy check, non-blocking, sub-50ms target
- Workspace management API — Hono routes with Better Auth session enforcement, Drizzle ORM on D1
- Background side effects — Analytics Engine writes, CSV import/export via Queues + R2, webhook delivery via Durable Objects
A 34-table D1 schema enforces workspace scoping at the database level, and every write route validates both session freshness and workspace membership before reaching the repository layer.
Architecture
flowchart LR
Client[Client] --> Worker[Hono Worker]
Worker --> Auth[Better Auth Session]
Worker --> Redirect[/r/:slug]
Worker --> API[/api/v1/*]
Redirect --> KV[(KV Cache)]
Redirect --> D1Policy[D1 Policy Read]
API --> D1[(D1 - 34 tables)]
API --> AE[(Analytics Engine)]
API --> Queue[Queue / R2 / DO]Edge redirect path
The redirect route is the hottest path and the most constrained:
GET /r/:slug
1. KV.get(slug) → cached link target
2. If KV miss → D1 link row read
3. D1 policy check: status, expiry, password, max-clicks
4. If pass → edge redirect (not proxy)
5. Background: Analytics Engine write (non-blocking)
This design means a password check adds one D1 read, but it does not add a round trip for analytics. The policy read remains fail-closed: missing or invalid D1 state yields 404 rather than a permissive default.
Workspace authorization
Every management endpoint begins with the same three checks:
flowchart TD
Request[Request] --> Session[Fresh Session]
Session --> Workspace[Active Workspace]
Workspace --> Permission[Resource Permission]
Permission --> Handler[Route Handler]Better Auth provides session validation, including a 15-minute freshness gate that cannot be extended by updatedAt renewal alone. Workspace selection is explicit: callers send workspaceId in the route, and the middleware confirms active membership before reaching the handler.
Repository methods never discover the workspace from the session. They receive workspaceId as an argument. This makes the boundary inspectable: a handler without an explicit workspace scope cannot accidentally access cross-tenant data.
D1 schema boundaries
The 34-table schema uses three organizational principles:
| Boundary | Tables | Ownership rule |
|---|---|---|
| Identity | users, sessions, accounts | Global; Better Auth managed |
| Workspace | workspaces, members, invitations | workspaceId required on reads |
| Resources | links, domains, apiKeys, webhooks, imports | workspaceId + createdBy |
| Background | deliveryLogs, performanceEvents, auditRecords | Channel-checked; separate retention |
Workspace default_domain_id is the only default-domain source. Plan limits come from shared code configuration, not per-row values. The schema baseline 0000_initial_schema.sql initializes a complete empty database; subsequent migrations are additive only.
Non-blocking side effects
Analytics, CSV imports, and webhooks share a common pattern:
flowchart LR
Handler[Route Handler] --> Accept[Accept Durable Record]
Accept --> Queue[Queue Message]
Queue --> Consumer[Queue Consumer]
Consumer --> Retry[Durable Object Lease]
Retry --> VK[Webhook / R2 / D1]The route handler writes an accepted record to D1 and enqueues a message. The Queue consumer claims a Durable Object lease, performs the side effect, and persists completion. Failures retry via Cron recovery with a 60-second cooldown. Queue send failures do not drop work; the accepted record remains eligible for later admission.
Trade-offs
- D1 is SQLite at the edge — good for structured workspace data, not for high-frequency analytics writes. Analytics Engine handles the latter.
- KV caches link targets for fast redirects but can become stale on expiry or password changes. The policy read revalidates current D1 state before redirecting.
- 34 tables in one D1 database simplifies joins but requires schema discipline. Drizzle
relationalQueriesand explicit repository boundaries prevent ad-hoc table proliferation.
Reference
- Source:
src/worker/↗ — Hono routes, services, middleware, and Durable Object implementations - Schema:
src/db/schema/↗ — Drizzle ORM definitions - Architecture record:
docs/ARCHITECTURE.md↗