- downgradeByCustomer now queries DB when key not in memory cache,
preventing cancelled customers from keeping Pro access in multi-pod setups
- recover/verify endpoint falls back to DB lookup when cache miss on email
- Added TDD tests for both fallback paths (4 new tests)
- renderPdf() and renderUrlPdf() now return { pdf, durationMs }
- Timing wraps the actual render with Date.now()
- Log render duration via logger.info
- Add X-Render-Time response header in convert and demo routes
- Update all callers in convert, demo, templates routes
- Add TDD tests in render-timing.test.ts
- Update existing test mocks for new return shape
When renderUrlPdf() sets up request interception for SSRF DNS pinning,
the interceptor and event listener were never cleaned up in recyclePage().
This could cause subsequent HTML-to-PDF conversions on the same pooled
page to have external resources blocked by the stale interceptor.
- Export recyclePage for testability
- Add removeAllListeners('request') + setRequestInterception(false)
- Add browser-recycle.test.ts with TDD (red→green verified)
Tests: 443 passing (was 442)
- Add scale, pageRanges, preferCSSPageSize, width, height to PdfOptions
- Add headerTemplate, footerTemplate, displayHeaderFooter to docs
- Pass all options through routes to browser service for HTML, Markdown, and URL endpoints
- Export PdfRenderOptions interface for type reuse
- Bump version to 0.4.5
- Add partial unique index on api_keys(stripe_customer_id) WHERE NOT NULL
- Use INSERT ... ON CONFLICT in createProKey for cross-pod dedup
- Add findKeyByCustomerId() to query DB directly
- Success page checks DB before creating key (survives pod restarts)
- Refresh in-memory cache after UPSERT
- queryWithRetry now uses explicit client checkout; on transient error,
calls client.release(true) to DESTROY the dead connection instead of
returning it to pool. Fresh connections are created on retry.
- connectWithRetry validates connections with SELECT 1 before returning
- Health check destroys bad connections on failure
- Reduced idleTimeoutMillis from 30s to 10s for faster stale connection eviction
- Fixes BUG-075: pool kept reusing dead TCP sockets after PgBouncer pod restart
- Enable TCP keepalive on pg.Pool to detect dead connections
- Add connectionTimeoutMillis (5s) to prevent hanging on stale connections
- Add queryWithRetry() with exponential backoff for transient DB errors
- Add connectWithRetry() for transaction-based operations
- Detect PgBouncer "no available server" and other transient errors
- Health check has 3s timeout and returns 503 on DB failure
- All DB operations in keys, verification, usage use retry logic
Fixes BUG-075: PgBouncer failover causes permanent pod failures
- Support SMTP_USER/SMTP_PASS env vars for authenticated SMTP
- Support SMTP_FROM env var for configurable sender address
- Auto-detect secure mode for port 465
- Backwards compatible: falls back to unauthenticated local relay
- Add /change-email as a proper standalone page (public/src/change-email.html)
with API key input, new email input, verification code flow, and success state
- Update footer partial: change "/#change-email" link to "/change-email" on all pages
- Remove email change modal HTML and hash-handler JS from index page source
- Add /change-email to sitemap.xml
- Rebuild all HTML files via build-html.cjs
- Add updateEmailByCustomer() to src/services/keys.ts
- Add customer.updated webhook handler in src/routes/billing.ts
to sync email changes made via Stripe dashboard back to DocFast
- Replace revokeByCustomer with downgradeByCustomer in keys.ts
- Sets tier='free' in cache and DB (UPDATE, not DELETE)
- Add isDocFastSubscription() product filter helper in billing.ts
- Filters all subscription events by prod_TygeG8tQPtEAdE
- Handle customer.subscription.updated event
- Downgrades on status=canceled/past_due/unpaid or cancel_at_period_end=true
- Handle customer.subscription.deleted with product filter
- Downgrades to free (was incorrectly deleting the key)
Fixes revenue integrity bug: cancelled Pro subscribers kept Pro access.
- Replace JSON file storage with PostgreSQL (pg package)
- Add db.ts service for connection pool and schema init
- Rewrite keys.ts, verification.ts, usage.ts for async PostgreSQL
- Update all routes for async function signatures
- Add migration script (scripts/migrate-to-postgres.mjs)
- Update docker-compose.yml with DATABASE_* env vars
- Change SLA from 99.9% to 99.5% in landing page
- Fix swagger-ui symlink in Dockerfile (was pointing to /opt/docfast instead of /app)
- Add CSP directives to allow inline scripts/styles and Google Fonts
- Add email-change.ts route with rate limiting (3/hr) and verification
- Add updateKeyEmail to keys service
- Add email-change route to index.ts with CORS support
- Launch BROWSER_COUNT separate Chromium instances (default: 2)
- Each with PAGES_PER_BROWSER pages (default: 8, 16 total)
- Round-robin distribution across browser instances
- Independent restart scheduling per browser
- Updated health endpoint to show per-browser stats
- docker-compose: added BROWSER_COUNT and PAGES_PER_BROWSER env vars
Investor Directive 1: Key recovery now shows key in browser after email verification code.
- Removed sendRecoveryEmail function entirely
- Recovery endpoint returns apiKey in JSON response (shown once in browser)
- Added full recovery modal UI (email → code → key displayed)
- Added "Lost your API key?" links throughout signup flow
Investor Directive 3: Rate limits adjusted to match server capacity.
- Global rate limit: 100/min → 30/min (server handles ~28 PDFs/min)
- CORS: recover routes now restricted to docfast.dev origin
- POST /v1/recover: request recovery code
- POST /v1/recover/verify: verify code, receive key via email
- Key sent via email only (not in API response) for security
- Rate limited to 3 attempts per hour
- Non-enumerable: same response whether email exists or not
- DKIM-signed emails via postfix/opendkim
- Replace Resend email service with nodemailer via local postfix relay
- Remove code field from POST /v1/signup/free response
- Send 6-digit verification code via email only (noreply@docfast.dev)
- Add extra_hosts for Docker-to-host SMTP relay
- Fire-and-forget email sending to avoid blocking API response
- POST /v1/signup/free now returns verification code (temp in response)
- New POST /v1/signup/verify endpoint to verify code and get API key
- Codes expire after 15 minutes, max 3 attempts
- Frontend updated with 2-step signup modal (email → code → key)
- Legacy token verification kept for existing links
- Signup now requires email verification before API key is revealed
- Verification token sent via email (Resend) with console fallback
- GET /verify?token=xxx shows API key in styled HTML page
- Handles expired (24h), invalid, and already-verified tokens
- Frontend modal shows 'check your email' instead of key
- Keeps existing rate limiting
BUG-007: Unwrap req.body.data for template endpoint (docs show wrapped format)
BUG-008: Default PDF margins set to 0 (user can override via request body)
BUG-006: Copy button shows Copied! for 2s then reverts
- Added /v1/signup/free endpoint for instant API key provisioning
- Built unified key store (services/keys.ts) with file-based persistence
- Refactored auth middleware to use key store (no more hardcoded env keys)
- Refactored usage middleware to check key tier from store
- Updated billing to use key store for Pro key provisioning
- Landing page: replaced mailto: link with signup modal
- Landing page: Pro checkout button now properly calls /v1/billing/checkout
- Added Docker volume for persistent key storage
- Success page now renders HTML instead of raw JSON
- Tested: signup → key → PDF generation works end-to-end