Publish only explicitly registered Better Auth operations in the ZPan product OpenAPI contract while preserving runtime auth routes and discovery.
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:
fetchcreates a Cloudflare platform from the request environment, reuses a cached Better Auth instance per isolate, injects Open Graph tags for share pages, then delegates tocreateApp.scheduleddelegates toworkers/scheduled.tsfor licensing refresh, traffic sync, quota reset, trash purge, and telemetry.queueruns 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_URLset: libSQL/Turso viacreateLibsqlPlatform- 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.tswraps D1 and platform bindings.node.tswrapsbetter-sqlite3.libsql.tswraps@libsql/clientand runs migrations against Turso/libSQL.context.tsprovides anAsyncLocalStorageproxy 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 sessionapi-key— Better Auth API key plugin, including org-scoped keysdownloader— remote downloader agent tokendownload-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
- Frontend calls
POST /api/objectsthrough the Hono RPC wrapper. - Server creates a draft
mattersrow plus anobject_upload_sessionsrow. - Server returns presigned upload instructions.
- Browser uploads bytes directly to S3/R2 with raw
fetchto the presigned URL. - Frontend calls the completion endpoint.
- Server verifies and activates the
mattersrow, 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
- A user creates a
download_tasksrow. - A registered downloader agent heartbeats to
/api/downloads/downloaders. - The server assigns queued tasks based on availability/capabilities.
- The downloader fetches source bytes outside the main ZPan runtime.
- The downloader uploads completed output back through a scoped task-upload token and the normal object upload path.
- 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 insrc/lib/api.test.ts. - RPC clients live in
src/lib/rpc.tsand use Hono RPC types exported fromserver/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.jsonandzh.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 inserver/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/referenceand/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.tsruns equivalent recurring work withsetInterval.
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.tsis the feature-comparison and gate-key source of truth.server/middleware/require-feature.tsgates 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 jsdomintegration— Hono route/usecase integration against SQLitecloudflare— Workers runtime tests with D1 migrations applied through@cloudflare/vitest-pool-workerslibsql— platform smoke tests for the libSQL/Turso path
Other gates:
pnpm lint— Biome lint and format checkpnpm typecheck— server and frontend TypeScript projectspnpm test— unit + integrationpnpm test:cf— Workers runtime testspnpm test:libsql— libSQL platform testspnpm 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.