Files
zpan/docs/architecture.md
Jasper Van 4ab0b0922a fix(openapi): whitelist Better Auth downloader Device Flow (#552)
Publish only explicitly registered Better Auth operations in the ZPan product OpenAPI contract while preserving runtime auth routes and discovery.
2026-08-04 16:39:54 -04:00

17 KiB

Architecture & Technical Decisions

Technical reference for the current ZPan implementation. Product sequencing and future scope live in V2_ROADMAP.md and docs/roadmap/, not in this document.

System Shape

ZPan is a single-package full-stack TypeScript application. The server is the control plane for auth, metadata, quotas, sharing, WebDAV, background jobs, and paid-tier entitlement checks. File bytes live in S3-compatible object storage and are uploaded or downloaded through presigned URLs whenever possible.

Browser SPA / WebDAV / API clients / downloader agents
                    |
                    v
             Hono application
                    |
        middleware + route modules
                    |
                    v
                 usecases
                    |
             ports / Deps object
                    |
   +----------------+----------------+
   |                |                |
 repos          gateways         providers
   |                |                |
Drizzle DB    S3 / Cloud / mail   CF APIs / changelog

Primary deployment target is Cloudflare Workers. Node.js/Docker is the backup runtime, and the same Hono app is adapted for other serverless targets.

Core Decisions

Area Decision Reason
Server framework Hono Runs cleanly on Workers and Node with web-standard Request/Response
API typing Hono RPC for frontend, OpenAPI for external clients Frontend gets compile-time path/type safety; non-TS clients get a stable REST contract
Runtime database D1 on Workers, SQLite or Turso/libSQL elsewhere One SQLite-family schema across every deployment target
ORM Drizzle Type-safe SQLite/D1 schema and migrations with low runtime overhead
Auth Better Auth Email/password, social/OIDC, organizations, API keys, device flow
Object storage S3-compatible protocol Works with R2, S3, MinIO, B2, Tigris, RustFS, and similar providers
Frontend React 19 + Vite + TanStack Router SPA with typed file routes and no server-rendering framework lock-in
UI shadcn/ui-style components, Radix UI, Tailwind CSS 4, Lucide Local component ownership with consistent interaction primitives
List pagination Signed opaque keyset tokens Stable infinite loading without offset drift or exposing storage keys
Realtime updates One global SSE stream over a durable change feed Resumable cross-runtime invalidation without per-page connections
Tests Vitest projects + Playwright Unit, integration, Workers runtime, libSQL, and browser-level coverage

Repository Layout

zpan/
├── server/
│   ├── adapters/       # Concrete repos, gateways, and providers
│   ├── db/             # Drizzle schemas and DB helpers
│   ├── domain/         # Pure domain helpers and policy logic
│   ├── http/           # Hono route modules
│   ├── middleware/     # Hono middleware and principal resolution
│   ├── platform/       # Runtime adapters: Workers, Node, libSQL
│   ├── usecases/       # Business workflows and port interfaces
│   ├── app.ts          # Hono app composition and route mounting
│   ├── auth.ts         # Better Auth configuration
│   ├── bootstrap.ts    # Runtime-independent app bootstrap
│   └── entry-*.ts      # Node/serverless entry points
├── src/
│   ├── components/     # UI components and feature views
│   ├── hooks/          # Frontend hooks
│   ├── i18n/           # Frontend translations
│   ├── lib/            # RPC clients, API wrappers, formatters, utilities
│   └── routes/         # TanStack Router file routes
├── shared/             # Shared schemas, constants, feature registry, types
├── workers/            # Cloudflare Worker fetch/scheduled/queue entry
├── migrations/         # drizzle-kit generated SQL migrations
├── e2e/                # Playwright tests
├── wrangler.toml       # Cloudflare Workers configuration
└── package.json        # Single pnpm package

There is no native desktop or mobile client workspace in this repository. Native client implementations are expected to live in separate projects. This repository can still own server-side API contracts and isolated automation clients when they are part of the ZPan server/web product boundary.

Runtime Entry Points

Cloudflare Workers

workers/bootstrap.ts exports the Worker handlers:

  • fetch creates a Cloudflare platform from the request environment, reuses a cached Better Auth instance per isolate, injects Open Graph tags for share pages, then delegates to createApp.
  • scheduled delegates to workers/scheduled.ts for licensing refresh, traffic sync, quota reset, trash purge, and telemetry.
  • queue runs archive-job messages through the archive jobs gateway.

Node / Docker / Cloud Run

server/entry-node.ts serves the same Hono app plus the built SPA from dist/. It chooses the platform at boot:

  • TURSO_DATABASE_URL set: libSQL/Turso via createLibsqlPlatform
  • otherwise: local SQLite via createNodePlatform

Because Node has no platform scheduler, the entry starts interval-based jobs for license refresh, traffic sync, quota reset, trash purge, and telemetry.

Other Serverless Targets

server/entry-lambda.ts, server/entry-vercel.ts, server/entry-netlify.ts, and server/entry-azure.ts are thin adapters around the same app and platform model. Business logic does not fork by deployment target.

Platform Abstraction

server/platform/interface.ts is the runtime boundary:

interface Platform {
  db: Database
  getEnv(key: string): string | undefined
  getBinding<T = unknown>(key: string): T | undefined
}

Concrete implementations:

  • cloudflare.ts wraps D1 and platform bindings.
  • node.ts wraps better-sqlite3.
  • libsql.ts wraps @libsql/client and runs migrations against Turso/libSQL.
  • context.ts provides an AsyncLocalStorage proxy so shared objects can read the active request platform safely.

Only runtime infrastructure belongs in platform/. Business decisions stay in usecases and domain modules.

Server Layering

The server follows a ports-and-adapters shape.

server/app.ts

Creates the Hono app, installs global middleware, exposes OpenAPI/Scalar docs, mounts WebDAV, and mounts each API resource. /api/openapi.json includes ZPan's explicit resource contracts plus operations admitted by the declarative Better Auth OpenAPI registry. The registry currently contains only the two Downloader Device Flow operations. Better Auth's full runtime schema remains available only from its own reference endpoints.

server/http/

Route modules own HTTP concerns:

  • path shape and Hono mounting
  • request validation and response serialization
  • route-level auth guards
  • OpenAPI route metadata

Routes should call usecases or narrow domain helpers. They should not reimplement business workflows.

server/usecases/

Usecases own business workflows and coordination:

  • file object create/upload/complete/trash/restore/transfer
  • quota checks and traffic metering
  • sharing and save-to-drive behavior
  • team and user administration
  • site settings, licensing, announcements, branding, audit
  • archive processing and remote download task orchestration

Usecases receive deps as their first argument and reach the outside world only through ports.

server/usecases/ports*.ts

Ports define the contracts usecases need from persistence and external systems: repos, S3, email, zip, Cloud licensing, image upload, download tokens, and similar dependencies.

server/adapters/

Adapters implement ports:

  • repos/ persists to Drizzle tables.
  • gateways/ talks to S3, email, ZPan Cloud, archive queues, and zip handling.
  • providers/ wraps platform/provider-specific APIs such as Cloudflare custom hostnames or changelog fetching.

server/composition.ts is the composition root and the only place concrete adapters are assembled into the Deps object.

server/domain/

Pure policy and transformation helpers live here when they do not need I/O: licensing checks, path templating, WebDAV XML helpers, share rules, quota math, name-conflict planning, and similar logic.

Authentication And Principals

Better Auth owns the core auth tables and session lifecycle. ZPan adds route guards and principal normalization in server/middleware/auth.ts.

The auth middleware is intentionally soft-fail: it tries to identify the caller and sets context variables, while each route decides whether anonymous access is allowed.

Supported principals:

  • user — browser cookie session or bearer session
  • api-key — Better Auth API key plugin, including org-scoped keys
  • downloader — remote downloader agent token
  • download-task-upload — scoped token that lets a downloader upload one task's completed output

Route guards then enforce requireAuth, requireAdmin, requireDownloader, or requireTeamRole('viewer' | 'editor' | 'owner').

Data Model

Drizzle schemas live in server/db/schema.ts and server/db/auth-schema.ts. Migrations live at the repo root in migrations/ and are generated with pnpm db:generate.

Major table groups:

  • Auth and organizations: user, session, account, organization, member, invitation, apikey, deviceCode
  • Objects and storage: matters, storages, object_upload_sessions
  • Quota and billing: org_quotas, org_quota_entitlements, cloud_traffic_reports, webhook_events
  • Sharing and notifications: shares, share_recipients, notifications
  • Site administration: system_options, license_bindings, announcements, activity_events, invite tables
  • WebDAV: webdav_dead_properties, webdav_locks
  • Jobs and downloaders: background_jobs, downloaders, download_tasks, remote_download_usage_reports
  • Image hosting: image_hosting_configs, image_hostings

matters is the file tree. Every object belongs to an organization (orgId), not directly to a user. Storage object keys are implementation details hidden behind ZPan's object and storage abstractions.

File And Object Flow

Browser Upload

  1. Frontend calls POST /api/objects through the Hono RPC wrapper.
  2. Server creates a draft matters row plus an object_upload_sessions row.
  3. Server returns presigned upload instructions.
  4. Browser uploads bytes directly to S3/R2 with raw fetch to the presigned URL.
  5. Frontend calls the completion endpoint.
  6. Server verifies and activates the matters row, updates quota/usage, and records activity.

Abort paths discard the draft and try to clean up storage-side multipart state where possible.

Download

Authenticated object downloads and public share downloads resolve metadata in ZPan, enforce access/quota/credit rules, and return or redirect to a presigned object-storage URL. ZPan does not proxy file bytes unless a feature explicitly requires it.

Remote Download

  1. A user creates a download_tasks row.
  2. A registered downloader agent heartbeats to /api/downloads/downloaders.
  3. The server assigns queued tasks based on availability/capabilities.
  4. The downloader fetches source bytes outside the main ZPan runtime.
  5. The downloader uploads completed output back through a scoped task-upload token and the normal object upload path.
  6. ZPan records status, activity, and optional remote-download usage reports.

WebDAV

WebDAV paths resolve to matters records through the WebDAV usecases and repos. Object keys remain private implementation details. Browser UI, API, and WebDAV all share the same authorization and storage model.

Archive Jobs

Archive operations use background_jobs for durable status and progress. On Workers, queue messages execute archive work. On Node, archive work runs through the gateway path available to the current runtime.

Frontend Architecture

The frontend is a Vite React SPA.

  • File routes live in src/routes/ via TanStack Router.
  • Data fetching uses TanStack Query.
  • API wrappers live in src/lib/api.ts; each wrapper is tested in src/lib/api.test.ts.
  • RPC clients live in src/lib/rpc.ts and use Hono RPC types exported from server/app.ts.
  • Shared types and schemas must come from shared/.
  • UI components live under src/components/, grouped by feature and common UI primitives.
  • i18n strings live in src/i18n/locales/en.json and zh.json.

Frontend code must call ZPan APIs through Hono RPC wrappers. The exception is external URLs that are not ZPan APIs, such as presigned S3 upload URLs.

API Surfaces

  • /api/* — primary JSON API, mounted from route modules in server/http/
  • /api/auth/* — Better Auth routes
  • /api/openapi.json — public ZPan product contract; Better Auth operations are denied by default and currently only the registered Downloader Device Flow operations are included
  • /api/auth/reference and /api/auth/open-api/generate-schema — Better Auth's complete reference UI and generated runtime schema
  • /api/docs — Scalar API reference
  • /dav/* — WebDAV endpoint
  • /api/events — one resumable server-sent event stream for scoped durable resource changes
  • /r/* and /s/* — public redirect/share surfaces
  • /api/store/* — quota store and Cloud webhook endpoints

Public, authenticated, admin, downloader, and webhook routes can share a path prefix. Authorization is per route or per mounted sub-app, so mount order matters when a public/user router and admin router share a prefix.

Unbounded list APIs use signed opaque keyset tokens and the SPA consumes them with infinite queries. Realtime mutations append an atomic resource_changes row; the global SSE stream delivers those invalidation facts using the change sequence as Last-Event-ID. See List Pagination and Realtime Changes for the contract, replay, retention, and indexing rules.

Background Work

Cloudflare Workers:

  • scheduled() handles license refresh, traffic report sync, quota reset, trash purge, and telemetry.
  • queue() handles archive job messages.

Node/Docker:

  • server/entry-node.ts runs equivalent recurring work with setInterval.

Background jobs must be idempotent where possible. External delivery and Cloud reporting tables use stable event ids to prevent duplicate effects.

Paid-Tier Architecture

Paid-tier availability is certificate based.

  • ZPan Cloud is the source of truth for subscriptions and bound instances.
  • ZPan stores the active binding in license_bindings.
  • ZPan verifies signed entitlement certificates locally.
  • shared/feature-registry.ts is the feature-comparison and gate-key source of truth.
  • server/middleware/require-feature.ts gates API routes that require a paid feature.

The quota store uses Cloud for checkout/payment and ZPan for local catalog, entitlement delivery, quota calculation, and usage enforcement.

Testing And Quality Gates

Configured test projects in vitest.config.ts:

  • unit — pure utilities, schemas, domain helpers, and frontend components in jsdom
  • integration — Hono route/usecase integration against SQLite
  • cloudflare — Workers runtime tests with D1 migrations applied through @cloudflare/vitest-pool-workers
  • libsql — platform smoke tests for the libSQL/Turso path

Other gates:

  • pnpm lint — Biome lint and format check
  • pnpm typecheck — server and frontend TypeScript projects
  • pnpm test — unit + integration
  • pnpm test:cf — Workers runtime tests
  • pnpm test:libsql — libSQL platform tests
  • pnpm e2e — Playwright browser flows

Every PR that changes API or UI behavior also needs preview-environment verification per CONTRIBUTING.md.

Architectural Rules

  • Keep the server S3-native: no provider-specific storage SDKs unless the abstraction genuinely requires it.
  • Keep platform-specific code in server/platform/, Worker entry files, or deployment adapters.
  • Keep business workflows in usecases; HTTP routes should stay thin.
  • Use ports/adapters for persistence and external services.
  • Put shared contracts in shared/; do not duplicate types in frontend/server folders.
  • Generate migrations with drizzle-kit; do not hand-author migration journal entries.
  • Preserve direct-to-object-storage upload/download paths unless a feature has a concrete reason to proxy bytes.
  • Treat personal spaces and team spaces as organization-owned data containers; quota, sharing, WebDAV, and future clients must preserve that ownership model.