← Home

Internal

LinkLabs Planning

Architecture, features, roadmap, data model, security, and DevOps — all in one place.

Production ReadyLast audit: 2026-05-18

Product

Product Vision

LinkLabs is a programmable link control platform. It starts with short links, but the real product is control: who can open a link, when it works, where it routes, what happens after access, and what the owner learns from the event.

“Normal tools shorten and track links. LinkLabs lets users control, secure, and programlinks.”

Product Pillars

  • Clean link management
  • Strong access control
  • Smart routing
  • Built-in file sharing
  • Useful analytics
  • Real-time notifications
  • Developer APIs and webhooks
  • Team and business controls

Non-Negotiables

  • Fast public link opens
  • Strong abuse prevention from day one
  • Clear ownership and auditability
  • Mobile-first dashboard
  • Dark mode
  • Clean monorepo, typed contracts
  • Cloudflare-native deployment
  • No product feature work before foundation is stable

Infrastructure

Architecture

100% Cloudflare-native — Pages for the dashboard, three purpose-built Workers (API, Redirector, Jobs), D1 SQLite for all relational data, R2 for private object storage. No traditional servers. No cold starts. Edge everywhere.

System Topology

Dashboard User (Browser)
Link Visitor (Browser / Bot)
Developer (API Client)
↓ HTTPS↓ HTTPS↓ Bearer ll_*

Cloudflare Pages

Next.js 16 · React 19

Static export → out/

Redirector Worker

Raw fetch · <10ms

Port 8788 (dev)

API Worker

Hono 4.12 · Port 8787

Auth + CRUD + Webhooks

RedirectorService Binding (zero-HTTP)API

D1

SQLite · 24 mig.

R2

Object storage

Jobs Worker

Cron / 15 min

Resend

Email delivery

API Worker

:8787

Hono 4.12.18

linklabs-api.suckerlabs.workers.dev
D1 → linklabs-prod/devR2 → linklabs-files-prod/devSB: ← Redirector (notifyOnOpen)
  • REST API — links, folders, tags, files, workspaces
  • Better Auth 1.6.9 — sessions, Google OAuth, emailOTP
  • API key auth — Bearer ll_<64hex>, SHA-256 hashed at rest
  • HMAC-SHA256 webhook delivery via Promise.allSettled fan-out
  • R2 upload: files/{workspaceId}/{id}/{filename}
  • Role enforcement — owner / admin / member / viewer
  • Token-bucket rate limiting: 60 req/min (general), 5/min (OTP)
  • CSRF exact-path matching + origin whitelist
  • Body size enforcement: 1 MB general, 50 MB files, 2 MB images
  • OpenAPI docs at /api/docs

Redirector Worker

:8788

Raw fetch handler

linklabs-redirector.suckerlabs.workers.dev
D1 → linklabs-prod/devSB: → API Worker (notifyOnOpen, no external HTTP)
  • Alias lookup + status/expiry/inactivity/max-views enforcement
  • Password challenge: PBKDF2-SHA256 (100k iter, per-link salt)
  • Email unlock: 6-digit OTP, 5 min TTL, 5-attempt limit
  • HMAC-SHA256 signed access cookies — per-flag granularity
  • grantAndContinuePage for reliable cookie commit before redirect
  • Rule evaluation: geo / device / browser / time / referrer / split_test
  • Sticky AB via encrypted per-link cookies
  • ctx.waitUntil — non-blocking view count + last-opened writes
  • ctx.waitUntil — link_events insert for analytics
  • Unique visitor fingerprint via FNV-1a(IP+UA) — deduplicated in D1
  • notifyOnOpen via Service Binding → API (zero-latency, no TCP)
  • OG meta override (custom title/desc/image) for social preview
  • UTM parameter append on destination URL

Jobs Worker

:8789

Cron — every 15 min (prod) / 1 min (dev)

linklabs-jobs.suckerlabs.workers.dev
D1 → linklabs-prod/dev
  • Batch-expire links past expires_at timestamp
  • Batch-expire links past inactive_after_seconds threshold
  • Reconcile unique_visitor_count from link_events aggregate
  • Expiry warning emails via Resend — 24h window, atomic DB claim
  • Webhook retry — 5 attempts, exponential backoff: 1m→5m→30m→2h
  • Webhook delivery timeout: 10 s per attempt
  • Delivery deduplication via UUID delivery IDs

Dashboard API Call

  1. 1Browser sends request with session cookie
  2. 2Cloudflare Pages (Next.js) routes → client fetch to API Worker
  3. 3Hono middleware: CORS → CSRF → rate-limit → auth
  4. 4Better Auth verifies session in D1
  5. 5Route handler queries D1 / R2, returns JSON

Short Link Open

  1. 1Visitor browser hits Redirector Worker at edge
  2. 2D1 lookup by alias → validate status/expiry/limits
  3. 3If protected: serve HTML challenge (password / OTP)
  4. 4Verify grant: HMAC cookie or fresh OTP verification
  5. 5Evaluate routing rules in priority order → pick destination
  6. 6ctx.waitUntil: insert link_event, update view count
  7. 7Service Binding → API for notifyOnOpen (if enabled)
  8. 8301 redirect to final destination URL

Webhook Fan-out

  1. 1API action fires event (link_created, file_uploaded, …)
  2. 2Query all matching webhooks for workspace
  3. 3Promise.allSettled — deliver all in parallel
  4. 4HMAC-SHA256 sign payload with per-webhook secret
  5. 5On failure: Jobs Worker retries (exp. backoff, 5 attempts)
  6. 6Delivery result stored in webhook_deliveries table

D1 Schema — 14 Table Groups · 24 Migrations

users / sessions / accounts / verification

Better Auth — auth layer

organizations / members / invitations

Better Auth org plugin

workspaces / workspace_members

Product — multi-tenant boundaries

domains

Custom domain management (schema only)

folders

Partial unique index on (workspace_id, name) WHERE status=active

links

Core entity — alias, destination, status, rules, access, expiry, UTM

link_rules

Conditional routing — max 20/link, priority-ordered

link_access_policies

Access control schema — routes not yet wired

link_events

Analytics — opened/downloaded/blocked, FNV-1a visitor hash

files

File metadata — R2 key, content-type, byte size, status

webhooks / webhook_deliveries

Event subscriptions + delivery tracking

api_keys

Bearer tokens — SHA-256 stored, ll_ prefix, scoped

tags / utm_templates

Link categorisation + UTM preset management

audit_logs

Schema exists — no write paths yet

R2 Object Layout

files/{workspaceId}/{fileId}/{filename}Private file uploads — served via /api/files/:id/download
avatars/{userId}User profile images — served via /api/profile/avatar

Stack

Tech Stack

Versions pinned as of 2026-05-07. All services run on Cloudflare — no traditional servers.

Frontend

Next.js 16.2.5Dashboard framework
React 19.2.6UI library
Tailwind CSS 4.2.4Styling
Lucide React 1.14.0Icons

Backend

Cloudflare WorkersEdge runtime
Hono 4.12.18API framework
Wrangler 4.88.0CLI + local dev
Zod 4.4.3Validation

Data

Cloudflare D1SQLite at edge
Drizzle ORM 0.45.2Schema + queries
Drizzle Kit 0.31.10Migrations
Cloudflare R2Object storage

Auth & Email

Better Auth 1.6.9Sessions + OAuth
Resend 6.12.3Transactional email
Google OAuthSocial login
emailOTP pluginOTP verification

Monorepo

pnpm 10.33.0Package manager
Turborepo 2.9.9Build orchestrator
TypeScript 6.0.3Type system
Node 25Local runtime

Planned Future

Cloudflare QueuesAsync event buffering
Durable ObjectsStrict counters
Analytics EngineHigh-volume clicks
Cloudflare TurnstileCAPTCHA

Features

Feature Inventory

9 categories · full implementation status across every capability.

83

Live

1

Beta

31

Coming Soon

115

Total Features

Core Link Management

17 live3 soon

Full lifecycle control — create, edit, organize, and retire

85%

Create short links

Instant alias generation with auto-fallback

Custom aliases

Pick memorable slugs; reserved list enforced

Edit destinations

Update target URL in-place, no re-share needed

Pause & resume

Temporarily disable without losing config

Archive links

Retire and free up alias for reuse

Folder organization

Group links by project with soft-delete folders

Tag categorization

Multi-tag with color coding, M:N join table

Search & filters

Alias / URL / status filters, URL-persisted state

Bulk actions

Pause / resume / archive selected links at once

Duplicate links

Clone full config including rules and settings

Notes & labels

Internal documentation per link

Link preview (interstitial)

Opt-in confirmation page before redirect

Social preview override

Custom OG title / description / image per link

QR code generation

Auto-generated, downloadable per link

UTM builder

Append tracking params to destination URL

UTM templates

Reusable preset UTM configurations per workspace

Bulk export CSV

Download all link data for backup/migration

Custom domains

Bring your own domain with CNAME pointing

Bulk import CSV

Migrate links from other platforms

Link templates

Save common configurations as reusable presets

Expiry & Limits

7 live3 soon

Time-based and usage-based access controls

70%

Time-based expiry

Exact date or duration presets (15m / 1h / 24h / 7d)

Max views limit

Auto-expire after N opens

Inactivity auto-expiry

Expire after period of no opens

One-time access

Single-use links (maxViews: 1)

Expiry warning emails

Resend alert 24h before expiry, atomic DB claim

Batch expiry job

Jobs Worker cron expires past links every 15 min

Inactivity cleanup job

Jobs Worker cron for inactive_after_seconds

Scheduled availability

Time-windowed pause/resume

Expiry redirect fallback

Send to fallback URL after expiry

Auto-clean expired

Automatic archival of expired links

Security & Access Control

10 live6 soon

Multi-layer protection enforced at the edge

63%

Password protection

PBKDF2-SHA256, 100k iterations, per-link salt

Email unlock (OTP)

6-digit code via Resend, 5-min TTL, 5-attempt cap

HMAC access cookies

Per-flag signed grants, 1-hour expiry

grantAndContinuePage

200 HTML commit-then-redirect to prevent cookie race

Token bucket rate limiting

60 req/min general, 5/min OTP (per Worker instance)

CSRF protection

Exact regex path matching on all mutating API routes

Security headers

X-Frame-Options DENY, HSTS, COOP, X-Content-Type nosniff

Body size limits

1 MB API, 50 MB file uploads, 2 MB images

MIME type validation

Strict allowed-type list for file uploads

FNV-1a visitor hash

Anonymized fingerprint — no raw IP/UA stored

Email whitelist

Config stored in link_access_policies — enforcement pending

IP allowlist / blocklist

Edge-enforced IP-level access control

Country allowlist / blocklist

Geo-fencing via CF-IPCountry

Turnstile CAPTCHA

Cloudflare CAPTCHA on challenge pages

Access logs

Per-link attempt history with IP + timestamp

Decoy mode

Return fake destination on wrong password

Smart Routing

10 live3 soon

Dynamic destinations based on visitor context

77%

Geo routing

CF-IPCountry header → per-country destinations

Device routing

Mobile vs desktop UA parse → different URLs

Browser routing

Chrome / Safari / Firefox per-destination

Time-based routing

Schedule windows with day/hour conditions

Referrer routing

Source-specific destinations

Weighted A/B testing

Traffic split 0-100%, sticky assignment

Sticky assignment

Encrypted per-link cookie, 90-day expiry

Rule priority ordering

Deterministic evaluation via priority column

Fallback destination

Default URL when no rule matches

Rule inline editing

Edit config, destination, priority in dashboard

Language routing

Accept-Language header → locale destinations

Multi-variant splits

3+ destination A/B/C tests

Rule debugger

UI showing which rule matched on last open

Analytics

10 live3 soon

Privacy-first — no third parties, all edge-native

77%

Total views

Raw open count stored on links row

Unique visitors

FNV-1a hash, daily reconciled via Jobs Worker cron

Referrer tracking

Source attribution per click

Device breakdown

Mobile / tablet / desktop split

Browser stats

Chrome / Safari / Firefox / other

Geo breakdown (country)

Country-level from CF-IPCountry

Top cities

City-level attribution

A/B variant analytics

Per-variant click breakdown

Period selector

24h / 7d / 30d / all-time with live chart

File download counts

Per-file download tracking

UTM breakdown

Campaign / source / medium analysis

OS stats

Windows / macOS / iOS / Android

Bot filtering

Detect and exclude automated traffic

File Sharing

7 live4 soon

R2-backed secure file delivery with access gates

64%

File upload

Drag-and-drop, 50 MB limit, MIME validation

R2 private storage

keys: files/{workspaceId}/{id}/{filename}

Auto-generated short link

Short link created per upload

Password protection

Same PBKDF2-SHA256 mechanism as links

OTP protection

Email unlock before download

Instant revoke

Block download access immediately via status

Download tracking

Count per-file with notifyOnDownload

File expiry

Time-based expiration on file links

One-time download

Self-destruct after first download

Max download count

Cap total downloads per file

Virus scanning

Third-party security API integration

Notifications & Webhooks

7 live3 soon

Real-time event delivery — email and HTTP

70%

Link open alerts

notifyOnOpen — email per access via Service Binding

File download alerts

notifyOnDownload email per download

Expiry warning emails

24h window, atomic DB claim to prevent dupe

Webhook fan-out

Promise.allSettled parallel delivery per event

HMAC-SHA256 signing

Per-webhook secret, X-Signature header

Webhook retry

5 attempts, 1m→5m→30m→2h backoff via Jobs Worker

Delivery logs

Per-delivery status + response in webhook_deliveries

First open only

Alert only on initial visit

Dashboard alert panel

In-app notification inbox

Daily/weekly digest

Summary email with top stats

Developer & API

6 live3 soon

Programmatic access, OpenAPI docs, webhook events

67%

REST API

Full CRUD for all resources via Hono routes

API keys

ll_<64hex> prefix, SHA-256 stored, per-workspace

Scoped permissions

read / links:write / files:write / webhooks:write

OpenAPI docs

Swagger UI at /api/docs via Hono middleware

Webhook events

link.created / file.uploaded / link.expired etc.

Webhook delivery logs

Full request/response history

Official SDK

TypeScript client library

CLI tool

Command-line link management

Embed script

Track conversion events on destination pages

Auth & Team

9 live1 beta3 soon

Better Auth 1.6.9 — sessions, OAuth, OTP, invitations

69%

Email + password auth

Better Auth native, bcrypt-hashed

Google OAuth

Social login via Better Auth provider plugin

Email OTP login

6-digit code login via emailOTP plugin + Resend

Session cookies

Better Auth session table, HttpOnly

Workspace creation

Auto-created on email verification

Workspace switching

Multiple workspaces per user

Member invitations

Email invite with role, 7-day TTL

Role enforcement

Owner / admin / member / viewer on every mutating route

Profile avatar upload

R2-backed avatar with 2 MB limit

Billing plansBETA

Free / Pro / Business UI (not enforced)

Usage limits / quotas

Per-plan enforcement

Audit logs

Schema exists, write logic pending

SSO / SAML

Enterprise authentication

Roadmap

Development Phases

Phase-by-phase plan from foundation to business layer.

Phase 0

Foundation

Complete

Professional repo before product features

  • pnpm / Turborepo monorepo
  • Next.js dashboard shell + Cloudflare Worker shells
  • Shared packages (db, auth, email, ui, config, validators)
  • D1 / Drizzle schema + Better Auth wiring
  • Resend email + Google OAuth
  • Protected dashboard + workspace bootstrap
  • Account / security settings + session management
  • R2 avatar upload / remove
  • Core short link API + redirector
  • First operations-console redesign

Phase 1

MVP Link Control

Complete

Useful single-user / private beta product

  • Sign up / sign in / sign out + email verification
  • Workspace creation after email verification
  • Create / edit / archive short links + custom aliases
  • Pause / resume + redirector production path
  • Time expiry (with presets) + max views + inactivity expiry
  • Folder organization + tag organization
  • Password protection (PBKDF2-SHA256)
  • Basic analytics: 7d/30d, referrers, unique visitors
  • Bulk actions + URL-persisted filters
  • QR code generation
  • Duplicate links + bulk export CSV

Phase 2

Secure Sharing & Notifications

Complete

Files, email unlock, and real-time alerts

  • Email unlock + OTP unlock
  • File upload to R2 + secure download links
  • Download analytics
  • Notify on open (Resend) + expiry warning emails
  • Webhooks — CRUD + delivery + retry (5 attempts, exp. backoff)
  • Service binding: Redirector → API for OTP without external HTTP

Phase 3

Smart Links

Complete

Conditional routing and link intelligence

  • Country / device / time / referrer routing
  • Weighted A/B testing with sticky assignment
  • UTM parameters — stored on link, appended on redirect
  • Notes / internal labels
  • Social preview metadata (OG override)
  • Link interstitial (preview) pages
  • Rule priority ordering

Phase 4

Developer Platform

Complete

Public API and integrations

  • Public REST API with scoped API keys
  • Webhook signing (HMAC-SHA256) and delivery logs
  • OpenAPI docs (Swagger UI at /api/docs)
  • Embed tracking script — pending
  • SDK and CLI — planned

Phase 5

Business Layer

In Progress

Teams, billing, governance

  • Teams — workspace members with roles (done)
  • RBAC — role-checked on all mutating routes (done)
  • Workspace invitations — email invite flow (done)
  • Team settings page (done)
  • Billing — UI only, not enforced (beta)
  • Usage limits — pending
  • Audit logs — table exists, writes pending
  • Workspace ownership transfer — pending
  • SSO / SAML — future

Database

Data Model

Cloudflare D1 (SQLite). All timestamps as Unix milliseconds via integer(mode: "timestamp_ms"). Better Auth owns its own auth tables separately.

Entity Relationships

workspace → workspace_members (1:N)
workspace → folders (1:N, soft-delete)
workspace → links (1:N)
workspace → files (1:N)
workspace → webhooks (1:N, soft-delete)
workspace → api_keys (1:N)
workspace → tags (1:N)
link → link_rules (1:N, priority ordered)
link → link_access_policies (1:N, schema only)
link → link_events (1:N, analytics)
link → file (N:1, optional)
link ↔ tags (M:N via link_tags)
webhook → webhook_deliveries (1:N)

workspaces

One per team. Auto-created on email verification.

idnamesluglogocreatedAtupdatedAt

workspace_members

Roles enforced on every mutating route.

workspaceIduserIdrole (owner/admin/member/viewer)createdAt

links

Core entity. 25+ fields.

alias (unique)destinationUrlstatus (active/paused/archived)passwordHashrequireEmailUnlockshowInterstitialexpiresAtmaxViewsviewCountuniqueVisitorCountinactiveAfterSecondslastOpenedAtnotifyOnOpennotifyLastSentAtotpHashotpEmailotpExpiresAtotpAttemptsutmSource/Medium/Campaign/Term/Content/ReferralogTitle/Description/ImageUrlnotesfolderIdfileIdworkspaceId

link_rules

Evaluated in priority order by redirector. Sticky 90-day cookies for split_test.

linkIdkind (geo/device/time/referrer/split_test)config (JSON)destinationpriority

link_access_policies

Schema exists. No route handlers written yet.

linkIdkindconfig (JSON)createdAt

link_events

Written via ctx.waitUntil. Future: Queue-backed buffering.

linkIdvisitorHash (FNV-1a)referrercountrydevicebrowsercreatedAt

files

R2 key: files/{workspaceId}/{id}/{filename}

idworkspaceIdnamemimeTypesizer2KeystatuspasswordHashdownloadCount

folders

Soft delete preserves audit trail. Unique name per workspace (excluding deleted).

idworkspaceIdnamestatus (active/deleted)

webhooks

Soft delete. Delivery logs in webhook_deliveries.

idworkspaceIdurlsecret (HMAC key)events[]status

api_keys

Bearer prefix ll_. Hash compared at request time.

idworkspaceIdnamekeyHash (SHA-256)scopes[]lastUsedAtexpiresAt

tags

M:N via link_tags junction table.

idworkspaceIdnamecolorcreatedAtupdatedAt

audit_logs

Schema exists. No writes anywhere in codebase yet.

idworkspaceIduserIdactionresourceIdmetadatacreatedAt

Migration History (24 migrations)

0000Initial Better Auth + product tables
0001workspace_members_user_idx
0002Auth rate-limit + verification table improvements
0003Google OAuth provider support columns
0004Unique index on folders(workspace_id, name)
0005Normalize all timestamps text → integer Unix ms
0006Email unlock columns on links
0007DEFAULT values, soft-delete status, missing indexes, FK
0008Webhook delivery tables + retry queuing
0009Retry tracking columns
0010notifyOnOpen column on links
0011notifyLastSentAt for throttle tracking
0012Variant column on link_events for A/B
0013notifyOnDownload for file events
0014expiryWarningSentAt column
0015showInterstitial column
0016tags + link_tags tables
0017Workspace invitation flow tables
0018UTM params, notes, OG override columns on links
0019Workspace logo, UTM templates, viewer role
0020updatedAt on tags
0021Case-insensitive name indexes
0022utmReferral column on links
0023Partial unique index on folders.name (excludes deleted)

Security

Security & Abuse Plan

URL shorteners attract abuse. Safety is a product requirement from day one.

Create-Time

  • Validate destination URLs (protocol + format)
  • Block unsupported protocols
  • Reserve protected aliases
  • Rate limit link creation
  • Store creator, workspace, source metadata

Open-Time

  • Enforce link status before redirect
  • Enforce expiry and view limits
  • Password: PBKDF2-SHA256, 100k iterations, per-link salt
  • OTP: SHA-256 hashed, 5-min expiry, 5-attempt limit
  • HMAC-signed access cookies (1hr, per-flag granularity)
  • CSRF: exact regex path matching, not substring
  • 1MB body limit (50MB files), strict MIME validation
  • Token bucket rate limiting (60/min general, 5/min strict)
  • Security headers: X-Frame-Options DENY, HSTS, COOP, nosniff
  • FNV-1a visitor hash (no raw IP stored)

Admin Controls

  • Disable link instantly
  • Revoke file instantly
  • Soft-delete folders and webhooks
  • Workspace-level emergency disable — planned
  • Audit logs — schema exists, writes pending

Known Gaps (MVP)

  • Password unlock form: no per-IP attempt counter (mitigated by PBKDF2 cost + Cloudflare edge rate limiting)
  • Email whitelist: stored in policy config but not yet enforced in redirector
  • Concurrent OTP: only one OTP per link — concurrent visitors clobber each other's code (acceptable for MVP)
  • Distributed rate limiting: in-memory token bucket is per-Worker-instance (no Workers KV coordination)
  • notifyLastSentAt: stored but the jobs worker doesn't check it before sending (possible duplicate alerts)

Planned Hardening

  • Workers-KV-backed per-IP counter for password submissions
  • Cloudflare Turnstile CAPTCHA on challenge pages
  • Malware / phishing scanner integration
  • Bot Management signal integration
  • Admin moderation dashboard
  • Country + IP allow/blocklists in redirector enforcement

100K

PBKDF2 iterations

5 min

OTP expiry

HMAC

SHA-256 cookies

1 MB

Body limit

Services

Service Boundaries

Strict ownership boundaries — each service does exactly one job.

apps/web

Cloudflare Pages · Next.js 16 · React 19

Owns

  • Auth screens and session-aware dashboard UI
  • 5 dashboard pages: Overview, Links, Folders, Files, Settings
  • Full link management: CRUD, bulk actions, filters, analytics
  • File upload / download / revoke
  • Client API calls via apiFetch() (X-Workspace-Id header)
  • Shared UI from @linklabs/ui (Radix / Shadcn patterns)

Not Responsible For

  • Auth server routes
  • Product data writes
  • Redirect decisions
  • File object serving

services/api

Cloudflare Worker · Hono · localhost:8787

Owns

  • Better Auth routes (/api/auth/*)
  • Workspace / member / invitation APIs
  • Link CRUD, folders, tags, rules
  • File metadata APIs (R2 upload/delete)
  • Email OTP send + verify
  • Profile avatar upload via R2
  • Webhook CRUD + signing
  • API key CRUD + scope enforcement
  • OpenAPI docs (/api/docs)

Not Responsible For

  • Redirect decisions
  • Background jobs
  • Heavy analytics aggregation

services/redirector

Cloudflare Worker · Raw fetch · localhost:8788

Owns

  • Resolve aliases to destinations
  • Enforce status / expiry / inactivity / max-views
  • Serve password challenge + HTML form
  • Email unlock challenge + OTP verification
  • HMAC-signed access cookies (per-flag granularity)
  • grantAndContinuePage for reliable cookie commit
  • Evaluate routing rules (geo/device/time/referrer/split_test)
  • Record view count + last-opened via ctx.waitUntil
  • Record link_events for analytics
  • Unique visitor counting
  • notifyOnOpen via API service binding

Not Responsible For

  • Dashboard APIs
  • Heavy analytics aggregation

services/jobs

Cloudflare Worker · Cron · localhost:8789

Owns

  • Batch-expire links past expires_at (every 15 min)
  • Batch-expire links past inactive_after_seconds threshold
  • Reconcile unique_visitor_count from link_events
  • Send expiry warning emails via Resend (24h window, atomic claim)
  • Retry failed webhook deliveries (5 attempts, exp. backoff)

Not Responsible For

  • Real-time processing
  • Dashboard APIs

Shared Packages

packages/db

Drizzle schema, D1 client helpers, 24 migrations

packages/auth

hashLinkPassword / verifyLinkPassword (PBKDF2), hashOtp / verifyOtp, timingSafeEqual

packages/email

Resend adapter, email content helpers

packages/ui

Shared React UI primitives (Radix / Shadcn patterns, Tailwind v4)

packages/config

APP_NAME, shared constants

packages/validators

Zod schemas shared across services and frontend

DevOps

Environments & Development

Three environments: local dev (remote D1), local dev (local D1), and production. Prod and dev are 100% separate databases.

PROD

Production URLs

Web

linklabs.pages.dev

API

linklabs-api.suckerlabs.workers.dev

Redirector

linklabs-redirector.suckerlabs.workers.dev

Jobs

linklabs-jobs.suckerlabs.workers.dev

D1

linklabs-prod (database)

R2

linklabs-files-prod (bucket)
DEV

Local Dev URLs

Web

localhost:3000

API

localhost:8787

Redirector

localhost:8788

Jobs

localhost:8789

D1

linklabs-dev (remote cloud)

R2

linklabs-files-dev (dev bucket)

Note: pnpm dev uses remote D1 bindings. Use pnpm dev:local for isolated local SQLite.

Daily Dev

pnpm dev

Start web + api + redirector (remote D1)

pnpm dev:local

Fully local, isolated D1 state

pnpm dev:jobs

Start jobs worker (rarely needed)

Database

pnpm db:generate

Create new migration after schema change

pnpm db:migrate:local

Apply migration to local SQLite

pnpm db:migrate:remote

Apply to dev cloud D1

pnpm db:migrate:prod

⚠ Apply to production D1

Deploy

pnpm deploy:api

Deploy API worker

pnpm deploy:redirector

Deploy redirector worker

pnpm deploy:jobs

Deploy jobs worker

pnpm deploy:workers

Deploy all 3 workers

pnpm deploy:web

Build + deploy website

Code Quality

pnpm typecheck

TypeScript type check (run before push)

pnpm build

Build everything (CI)

pnpm format

Auto-format with Prettier

pnpm smoke:links

E2E smoke test on dev servers

Environment Variables by Service

services/api

BETTER_AUTH_SECRETBETTER_AUTH_URLWEB_ORIGINREDIRECTOR_ORIGINRESEND_API_KEYRESEND_FROMGOOGLE_CLIENT_IDGOOGLE_CLIENT_SECRET

services/redirector

DEFAULT_DESTINATIONAPI_URLCOOKIE_SIGNING_SECRET

services/jobs

RESEND_API_KEYRESEND_FROMREDIRECTOR_ORIGIN

apps/web

NEXT_PUBLIC_API_URL

First-Time Setup (New Dev)

  1. 1npm install -g pnpm && nvm use
  2. 2npx wrangler login (Cloudflare account)
  3. 3pnpm install
  4. 4cp services/api/.dev.vars.example → .dev.vars (fill secrets)
  5. 5cp services/redirector/.dev.vars.example → .dev.vars
  6. 6cp services/jobs/.dev.vars.example → .dev.vars
  7. 7cp apps/web/.env.example → .env.development
  8. 8pnpm db:migrate:remote (apply schema to dev D1)
  9. 9pnpm dev → open http://localhost:3000

Status

Current State

Last audited: 2026-05-18. Production-ready link control platform with full auth, CRUD, redirector enforcement, analytics, file sharing, and a polished dashboard.

What's Not Done Yet

  • Custom domains — schema only, no evaluation logic
  • Queue-backed event buffering — currently direct D1 writes via ctx.waitUntil
  • Email whitelist enforcement — stored in config, not yet enforced in redirector
  • linkAccessPolicies — table exists, zero route handlers
  • auditLogs — table exists, no writes anywhere in codebase
  • Concurrent OTP collision — one OTP per link, concurrent visitors clobber each other
  • Drag-and-drop folder assignment
  • Distributed rate limiting — per-Worker-instance only (not coordinated)
  • Server-side search/sort for links — all client-side, degrades at 500+ links
  • Multi-tag filtering — only single-tag filter supported
  • Lazy file loading — files fetched at page load even when panel not opened
  • Smart routing: kind-specific config not editable after creation (only destination + priority)
  • Smart routing: multi-variant split tests — 2-URL only currently
  • utmReferral — in API/DB/validators but missing from web LinkDraft form

Recent Bug Fixes

  • bugFile download now public — /api/files/:id/download was auth-gated (401 for external users)
  • bugAnalytics date SQL — date(created_at) on Unix-ms integers → date(created_at/1000,'unixepoch')
  • securityCSRF path bypass — path.includes() substring → exact regex /^/api/links/[^/]+/(send|verify)-unlock-otp$/
  • securityRedirector HMAC secret — hardcoded fallback replaced with ephemeral random per isolation
  • securityVisitor hash — weak polynomial → FNV-1a 32-bit (no async overhead)
  • bugOTP re-verification loop — monolithic hasValidAccessGrant replaced with per-gate hasAccessFlag
  • bugEmail+password gate bypass — grantAndContinuePage now redirects to /{alias}, not destination
  • bugCookie race condition — 303 redirect replaced with 200 HTML page + window.location.replace
  • bugFile upload orphaned R2 objects — validation moved before R2 write
  • bugWebhook GET soft-deleted — GET /api/webhooks filtered by status≠disabled
  • datafolderNameExists excludes deleted folders — soft-deleted folders no longer block re-use
  • dataArchived link aliases reusable — excluded from conflict checks in alias generation

Decisions

Open Questions

Unresolved product decisions and resolved ones for reference.

Resolved

Single-user first or workspace-first?

Workspace-first. Default workspace auto-created on email verification.

File sharing Phase 1 or Phase 2?

Phase 2. File sharing API routes and dashboard UI both implemented.

GitHub/Google OAuth in first auth release?

Google OAuth implemented. GitHub not yet.

How should email unlock work with password?

Both gates enforced sequentially via per-flag HMAC access cookies.

Open

  • What should the first public short domain be?
  • Should free users get custom aliases?
  • Should custom domains be paid-only?
  • How aggressive should abuse scanning be before public launch?
  • What analytics retention period by default?
  • Light mode: dark-only for now or ship together?
  • When to switch to Cloudflare Queues for event buffering?
  • When to add Durable Objects for strict counters?
  • Should billing enforcement come before or after custom domains?