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
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
D1
SQLite · 24 mig.
R2
Object storage
Jobs Worker
Cron / 15 min
Resend
Email delivery
API Worker
:8787Hono 4.12.18
linklabs-api.suckerlabs.workers.dev- 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
:8788Raw fetch handler
linklabs-redirector.suckerlabs.workers.dev- 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
:8789Cron — every 15 min (prod) / 1 min (dev)
linklabs-jobs.suckerlabs.workers.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
- 1Browser sends request with session cookie
- 2Cloudflare Pages (Next.js) routes → client fetch to API Worker
- 3Hono middleware: CORS → CSRF → rate-limit → auth
- 4Better Auth verifies session in D1
- 5Route handler queries D1 / R2, returns JSON
Short Link Open
- 1Visitor browser hits Redirector Worker at edge
- 2D1 lookup by alias → validate status/expiry/limits
- 3If protected: serve HTML challenge (password / OTP)
- 4Verify grant: HMAC cookie or fresh OTP verification
- 5Evaluate routing rules in priority order → pick destination
- 6ctx.waitUntil: insert link_event, update view count
- 7Service Binding → API for notifyOnOpen (if enabled)
- 8301 redirect to final destination URL
Webhook Fan-out
- 1API action fires event (link_created, file_uploaded, …)
- 2Query all matching webhooks for workspace
- 3Promise.allSettled — deliver all in parallel
- 4HMAC-SHA256 sign payload with per-webhook secret
- 5On failure: Jobs Worker retries (exp. backoff, 5 attempts)
- 6Delivery result stored in webhook_deliveries table
D1 Schema — 14 Table Groups · 24 Migrations
users / sessions / accounts / verificationBetter Auth — auth layer
organizations / members / invitationsBetter Auth org plugin
workspaces / workspace_membersProduct — multi-tenant boundaries
domainsCustom domain management (schema only)
foldersPartial unique index on (workspace_id, name) WHERE status=active
linksCore entity — alias, destination, status, rules, access, expiry, UTM
link_rulesConditional routing — max 20/link, priority-ordered
link_access_policiesAccess control schema — routes not yet wired
link_eventsAnalytics — opened/downloaded/blocked, FNV-1a visitor hash
filesFile metadata — R2 key, content-type, byte size, status
webhooks / webhook_deliveriesEvent subscriptions + delivery tracking
api_keysBearer tokens — SHA-256 stored, ll_ prefix, scoped
tags / utm_templatesLink categorisation + UTM preset management
audit_logsSchema exists — no write paths yet
R2 Object Layout
files/{workspaceId}/{fileId}/{filename}Private file uploads — served via /api/files/:id/downloadavatars/{userId}User profile images — served via /api/profile/avatarStack
Tech Stack
Versions pinned as of 2026-05-07. All services run on Cloudflare — no traditional servers.
Frontend
Backend
Data
Auth & Email
Monorepo
Planned Future
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 soonFull lifecycle control — create, edit, organize, and retire
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 soonTime-based and usage-based access controls
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 soonMulti-layer protection enforced at the edge
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 soonDynamic destinations based on visitor context
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 soonPrivacy-first — no third parties, all edge-native
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 soonR2-backed secure file delivery with access gates
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 soonReal-time event delivery — email and HTTP
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 soonProgrammatic access, OpenAPI docs, webhook events
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 soonBetter Auth 1.6.9 — sessions, OAuth, OTP, invitations
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
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
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
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
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
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
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
workspaces
One per team. Auto-created on email verification.
workspace_members
Roles enforced on every mutating route.
links
Core entity. 25+ fields.
link_rules
Evaluated in priority order by redirector. Sticky 90-day cookies for split_test.
link_access_policies
Schema exists. No route handlers written yet.
link_events
Written via ctx.waitUntil. Future: Queue-backed buffering.
files
R2 key: files/{workspaceId}/{id}/{filename}
folders
Soft delete preserves audit trail. Unique name per workspace (excluding deleted).
webhooks
Soft delete. Delivery logs in webhook_deliveries.
api_keys
Bearer prefix ll_. Hash compared at request time.
tags
M:N via link_tags junction table.
audit_logs
Schema exists. No writes anywhere in codebase yet.
Migration History (24 migrations)
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.
Production URLs
Web
linklabs.pages.devAPI
linklabs-api.suckerlabs.workers.devRedirector
linklabs-redirector.suckerlabs.workers.devJobs
linklabs-jobs.suckerlabs.workers.devD1
linklabs-prod (database)R2
linklabs-files-prod (bucket)Local Dev URLs
Web
localhost:3000API
localhost:8787Redirector
localhost:8788Jobs
localhost:8789D1
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 devStart web + api + redirector (remote D1)
pnpm dev:localFully local, isolated D1 state
pnpm dev:jobsStart jobs worker (rarely needed)
Database
pnpm db:generateCreate new migration after schema change
pnpm db:migrate:localApply migration to local SQLite
pnpm db:migrate:remoteApply to dev cloud D1
pnpm db:migrate:prod⚠ Apply to production D1
Deploy
pnpm deploy:apiDeploy API worker
pnpm deploy:redirectorDeploy redirector worker
pnpm deploy:jobsDeploy jobs worker
pnpm deploy:workersDeploy all 3 workers
pnpm deploy:webBuild + deploy website
Code Quality
pnpm typecheckTypeScript type check (run before push)
pnpm buildBuild everything (CI)
pnpm formatAuto-format with Prettier
pnpm smoke:linksE2E smoke test on dev servers
Environment Variables by Service
services/api
BETTER_AUTH_SECRETBETTER_AUTH_URLWEB_ORIGINREDIRECTOR_ORIGINRESEND_API_KEYRESEND_FROMGOOGLE_CLIENT_IDGOOGLE_CLIENT_SECRETservices/redirector
DEFAULT_DESTINATIONAPI_URLCOOKIE_SIGNING_SECRETservices/jobs
RESEND_API_KEYRESEND_FROMREDIRECTOR_ORIGINapps/web
NEXT_PUBLIC_API_URLFirst-Time Setup (New Dev)
- 1
npm install -g pnpm && nvm use - 2
npx wrangler login (Cloudflare account) - 3
pnpm install - 4
cp services/api/.dev.vars.example → .dev.vars (fill secrets) - 5
cp services/redirector/.dev.vars.example → .dev.vars - 6
cp services/jobs/.dev.vars.example → .dev.vars - 7
cp apps/web/.env.example → .env.development - 8
pnpm db:migrate:remote (apply schema to dev D1) - 9
pnpm 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?