Overview of the KitePdf app architecture.
Overview of the KitePdf app architecture.
00

Why this guide exists

This is not a README pasted into Benchlog. It explains the design problems KitePDF solves, the boundaries between components, and why the stack looks the way it does. Deployment commands and env file names live in later parts; here we focus on mental models you can reuse when you extend the product.

01

The problem we are solving

KitePDF turns PDF work into durable, billable jobs: upload once, process asynchronously, download when ready. That shape forces three non-negotiables: a public edge that stays thin, a policy layer that owns identity and credits, and stateless workers that can scale independently. Everything in the architecture serves one of those three.

02

Trust zones at a glance

Three planes: edge, application policy, async executionDiagram

Three planes: edge, application policy, async execution

ZoneWho calls itWhat must never happen
Public edgeBrowsersExposing api-gateway or worker ports directly on the internet
Application planeBrowsers (UI + JWT)Workers or anonymous clients mutating billing state without policy checks
Async planeSQS consumersWorkers trusting user cookies; they use internal secrets only
Internal hooksFrontend server, workersAny route without X-Internal-Secret on /internal/*
03

Authentication: design choice

We split identity into two stores on purpose. Better Auth in the Next.js app owns login sessions, email verification, 2FA, and admin plugins against the frontend database. The Go api-gateway never sees passwords—it only accepts cryptographically verifiable JWTs and maps them to rows in the backend database. That separation keeps the high-churn auth surface in one codebase while the job and credit domain stays in Go.

04

Better Auth on the frontend

Better Auth handles email/password, verification, password reset, bearer support, optional two-factor, and admin capabilities. User records live in frontend Postgres. On signup (or guest conversion), a database hook calls the api-gateway internal API to create or link a backend user keyed by auth_id—the same identifier embedded in JWTs.

  • Frontend DB: sessions, accounts, verification tokens, Better Auth tables.
  • Backend DB: users.id used in jobs and credits; users.auth_id links to IdP user id.
  • Failed backend sync on signup rolls back the frontend user to avoid split-brain accounts.
05

JWT issuance and JWKS verification

The Better Auth JWT plugin signs access tokens (payload includes id, email, name, role). The browser obtains a fresh JWT via getSession—the Set-Auth-Jwt response header—and sends Authorization: Bearer on api-gateway requests. The gateway does not share a static secret with the frontend; it bootstraps a JWKS client (AUTH_JWKS_URL, typically /api/auth/jwks on the app origin) and validates signature, issuer, and audience on every protected request.

Member and flex routes: JWT verified via JWKS, then mapped to backend principalDiagram

Member and flex routes: JWT verified via JWKS, then mapped to backend principal

06

Guests vs members

Anonymous users can still run a subset of tools. POST /guest/session creates a backend anonymous user; a signed httpOnly cookie (and optional X-Guest-Token) proves guest identity on flex routes. Flex middleware tries JWT first (AuthenticateJWTOptional), then guest cookie (AuthenticateGuest), then RequirePrincipal ensures someone is present. Member-only routes stack RequireMemberJWT with RejectAnonymous so dashboards, purchases, and paid tools require a real account.

07

Authorization: what happens after auth

Authentication answers who is calling. Authorization answers what they may do. KitePDF layers policy in middleware rather than scattering checks in handlers.

LayerMechanismDesign intent
Tool accessRequireCredits per toolAtomic balance check + deduction before handler runs; guests blocked on paid tools
Plan limitsUser loaded with subscription plan on JWT resolveOne JOIN per request; rate limits and quotas read plan from context
Rate limitsRedis-backed limiter on uploads and jobsProtect shared workers and storage from abuse
AdminJWT role + RequireAdminSame JWKS path; admin role from token and backend user
InternalX-Internal-SecretWorkers and trusted server hooks only; never exposed to browsers
08

Route families (policy, not a path list)

  • Public: health, share links, guest session create/revoke—no principal required.
  • Flex: tools, files, job poll—JWT or guest, then credits and rate limits.
  • Member: billing, library, notifications—JWT only, no anonymous principals.
  • Admin: operations dashboard—JWT + admin role.
  • Internal: job complete/fail, guest conversion—shared secret, no JWT.
09

Async processing architecture

Tool handlers are thin: validate input, enqueue work, return a job id. Workers own CPU and IO-heavy PDF operations. The gateway remains the system of record for job state; workers report outcomes through internal callbacks. S3 holds bytes; the gateway issues short-lived presigned URLs so browsers never receive long-lived object credentials.

Reference async path—compress on Node worker; other tools route to Python queueDiagram

Reference async path—compress on Node worker; other tools route to Python queue

Queue routing by tool type (Node vs Python) is a scaling knob: add consumers per queue without forking the API. The pattern is identical; only the worker implementation changes.

10

Data ownership

Frontend Postgres holds auth and UX-adjacent state. Backend Postgres holds jobs, credits, files metadata, and anonymous users. Neon or local containers are deployment details; the split is logical. Redis supports rate limiting and ephemeral coordination. This boundary lets you patch auth plugins without migrating job history, and vice versa.

11

Deployment shapes as tradeoffs

ShapeWhat you optimize forAuth implication
Local devFast feedbackJWKS URL points at local Next.js; same JWT flow as prod
Prod-localstackIntegration fidelity on one machinenginx single host; cookies and JWT issuer must match public URL
EC2 + real AWSCost-controlled productionBETTER_AUTH_URL and AUTH_* env vars must align with public hostname
12

What to read next

Part 3 (planned) walks local setup. Part 6+ will cover horizontal worker scaling and observability. Keep this page as the map: edge → policy (authn/authz) → queue → workers → storage.