Skip to content
CelPlume
select · Enteropen · Escclose Powered by Pagefind

System Architecture

Services, data model, security design, and provider capability matrix.

Picumet is a multi-cloud object storage management platform that runs entirely on Cloudflare. A React single-page application served by Cloudflare Pages talks to a Hono API on Cloudflare Workers. The API stores relational metadata in D1, caches short-lived state in KV, and keeps objects in R2 or any S3-compatible provider such as AWS S3 or Oracle Cloud.

The backend follows a service-oriented layout: code is split by business domain, not by technical layer. Each service owns its handlers, validation schemas, types, and domain logic. A central permissions service evaluates every request against path rules, and a storage service hides object operations behind one interface. This design keeps cross-service edges explicit and avoids cyclic dependencies.

This document covers the component topology, service dependencies, key design decisions, the data model, provider support, and the security model. The source code in workers/src/ and frontend/src/ remains the authoritative reference.

  • Familiarity with Cloudflare Workers, D1, KV, and R2 bindings.
  • Basic understanding of the S3 protocol and presigned URLs.
  • Working knowledge of TypeScript, React, and the Hono framework.

The service dependency graph shows how business domains relate:

ComponentPurpose
Cloudflare PagesServes the React single-page application and its static assets.
Cloudflare WorkersHosts the Hono API that implements all business logic.
D1Stores relational metadata: users, mounts, files, rules, sessions, shares, quotas, and logs.
KVCaches short-lived state: CSRF tokens, rate-limit counters, and free-mode credential sessions.
R2Stores objects for the primary provider through the Workers binding.
S3-compatible providersStores objects for AWS S3 and Oracle Cloud through the S3 protocol.
Hono + ZodProvides the HTTP framework and runtime validation for every API request.
React + TanStack QueryRenders the UI and manages server state, caching, and mutations.
services/Business-domain modules: auth, permissions, files, uploads, shares, storage, webdav, and more.
middleware/Cross-cutting concerns: authentication, CSRF, rate limiting, security headers, and the free-mode guard.
db/Data access layer with a unified Db interface over D1 and node:sqlite.
shared/Common types, Zod schemas, errors, and the uniform response envelope.
utils/Path, crypto, SSRF, SMTP, and base64 helpers.

The dependency graph above follows three rules:

  • Every service that touches file data depends on the Permissions service.
  • Every service that touches object bytes depends on the Storage service.
  • The Auth service is a leaf: other services call it, and it depends on no other service.

The layout avoids cyclic dependencies. index.ts only assembles routes and middleware; it contains no business logic. Public routes mount before the catch-all path server, which serves file objects at GET /*.

DecisionChosenAlternativeReason
Code organizationService-oriented by business domainLayered by technical tierEach domain stays self-contained and testable; cross-domain edges stay explicit and cyclic dependencies stay impossible.
Compute platformCloudflare Workers + PagesOther edge runtimes or self-hosted serversThe Workers bindings (D1, R2, KV) remove operational overhead; the free tier fits the target scale; a single runtime avoids building a portable platform layer.
Database backendD1 in production, node:sqlite in testsExternal PostgreSQL or SupabaseD1 gives zero-config relational storage with low latency; the Db abstraction lets the test suite run on node:sqlite without network access.
Delete orderDelete metadata first, then objectsDelete object and metadata togetherMetadata deletion happens in one atomic D1 transaction; object deletion runs best-effort after commit and records failures as orphan objects for reconciliation.
Recycle binNone: hard delete with confirmationTrash or quarantine with a retention periodS3-compatible providers lack a portable trash; a recycle bin would need tombstone logic and deferred reclamation for little benefit at this scale.
Path boundarySegment-based boundary checkString prefix matchisPathWithinBoundary requires the boundary plus /, which prevents prefix attacks such as /users/alice2 matching /users/alice.
Storage protocolOne S3-protocol credential modelNative SDK per providerR2, AWS S3, and Oracle all speak S3, so one model covers them; rejecting non-S3 providers (OSS, COS) avoids parallel driver maintenance.
Download authorizationOne-time tokens consumed atomically in D1Long-lived signed URLsDELETE ... RETURNING consumes each token exactly once, so concurrent requests cannot reuse it; counting at the gateway gives exact download numbers.

Every service directory follows the same shape: handlers.ts for API handlers, schemas.ts for Zod validation, types.ts for TypeScript types, and optional domain files. All API input passes through Zod validation.

Responsibilities: user registration, login, logout, JWT issuance and verification, session management, email verification, and password reset.

services/auth/
├── handlers.ts // login, register, logout handlers
├── schemas.ts // RegisterSchema, LoginSchema
└── types.ts // JwtPayload and request types
MethodPathDescription
POST/api/auth/registerCreates a user account.
POST/api/auth/loginAuthenticates the user and sets an HttpOnly JWT cookie.
POST/api/auth/logoutClears the session.
GET/api/auth/meReturns the current user.
GET/api/auth/csrf-tokenIssues a CSRF token.
GET/api/auth/verify-email, /api/auth/verifyVerifies an email address (aliases).
POST/api/auth/forgot-passwordStarts password recovery.
POST/api/auth/reset-passwordResets the password.

Dependencies: middleware/auth.ts, middleware/rate-limit.ts, middleware/csrf.ts, utils/crypto.ts, utils/smtp.ts, and the user, quota, settings, and log repositories.

Responsibilities: the core checkPermission algorithm, path-rule query and matching, principal construction, and permission enforcement.

services/permissions/
├── check.ts // core algorithm, rule ordering, rule loading
├── principal.ts // builds Principal from the Hono context, requirePermission, can
└── types.ts // permission types

The evaluation order runs from highest to lowest priority:

  1. Admin override.
  2. Mount boundary check.
  3. User root-path limit.
  4. API-key permission scope, applied as the intersection of key permissions and rule permissions.
  5. Path rules, ordered by subject specificity, path specificity, explicit priority, then effect.
  6. Owner-permission fallback.
  7. Default deny.

Two security boundaries matter:

  • isPathWithinBoundary compares path segments rather than using startsWith.
  • An API key with no matching rule receives a deny; the system never defaults to allow.

Dependencies: utils/path.ts, the rule repository, and shared/errors.ts.

Responsibilities: file and folder listing, folder creation, file details, metadata updates (rename), delete, move (Saga), batch operations, password verification, download links, copy links in multiple formats, and public path serving at GET /*.

services/files/
├── handlers.ts // list, folder, detail, update, password verify, download link, copy links
├── operations.ts // delete, move, batch operations, job status
├── move.ts // move Saga (copy, verify, atomic switch, async source cleanup)
├── path-serve.ts // public path serving at {origin}{virtual path}
├── schemas.ts // Update, CreateFolder, Move, Batch, VerifyPassword
└── types.ts

Copy links: GET /api/files/:id/copy-links returns formats: { direct, html, markdown, bbcode }. The direct value defaults to {origin}{virtual path}, a public direct link that path-serve.ts serves. Pass ?signed=true&expiresIn={seconds} to request a provider presigned URL, with the gateway token as the fallback.

Public path serving: GET /* streams an object by its virtual path. A public mount serves directly without login; a private mount requires an authenticated user with download permission; a password-protected file returns 403.

Move Saga: moveWithSaga validates permissions, conflicts, and cycles, creates a job, copies and verifies the object, switches the metadata atomically, and cleans up the source asynchronously. The main file API and WebDAV MOVE share this path.

Dependencies: permissions/principal.ts, storage/providers.ts, shares/tokens.ts, and the file, mount, provider, log, and job repositories.

Responsibilities: upload session management, single-file direct upload, multipart upload, Worker-proxied upload, quota reservation and release, completion verification (a HEAD check against spoofing), and idempotency. The service stays compatible with PicGo and PicList.

services/uploads/
├── handlers.ts // session, proxy upload, multipart, complete, abort
├── compat.ts // PicGo-compatible upload (Bearer API key / multipart)
├── schemas.ts // InitUploadSchema
└── types.ts

State machines:

  • Single file: pending → uploading → verifying → completed, with failed, expired, and aborted as terminal states.
  • Multipart: pending → uploading → parts_uploaded → completing → completed.

Dependencies: permissions/principal.ts, storage/providers.ts, the session, quota, file, mount, and reconciliation repositories, utils/path.ts, and utils/crypto.ts.

Responsibilities: share link creation, listing, and revocation; public access; password verification; download tokens consumed atomically in D1; share access logs; and the download gateway.

services/shares/
├── handlers.ts // create, list, public info, download, preview, revoke
├── gateway.ts // /api/gateway/download/:token streaming proxy
├── tokens.ts // download tokens (atomic D1 consumption)
├── schemas.ts // CreateShareSchema
└── types.ts

Download tokens: consumeDownloadToken uses DELETE ... RETURNING for a single atomic consumption, so concurrent requests cannot reuse a one-time token.

Dependencies: permissions/principal.ts, storage/providers.ts, the share, file, mount, provider, and log repositories, and utils/crypto.ts.

Responsibilities: the storage-provider abstraction (R2 binding or S3 protocol), object operations (HEAD, GET, PUT, DELETE, COPY, List), multipart upload, presigned URLs, connectivity tests, and switching across providers.

services/storage/
├── providers.ts // provider factory (getProvider, getProviderForMount)
├── r2.ts // R2BindingProvider based on the env.R2 binding
├── s3.ts // S3Provider (R2 S3 API, AWS S3, Oracle)
└── types.ts // StorageProviderInterface

Provider selection:

  • A provider with type=r2 and no endpoint uses R2BindingProvider (the local or production R2 binding).
  • Everything else uses S3Provider, an S3-protocol client.

Dependencies: the provider and mount repositories and utils/crypto.ts for secret decryption.

Responsibilities: the WebDAV protocol for PicGo and PicList compatibility, including PROPFIND, GET/HEAD, PUT, DELETE, MKCOL, MOVE, and OPTIONS, with Basic authentication using API keys.

services/webdav/
├── handlers.ts // WebDAV method handlers
└── types.ts // WebDAVResource, PropfindRequest

Security notes:

  • Every method runs through permissions/principal.ts for path-level authorization (PROPFIND read, PUT write, DELETE delete).
  • MOVE reuses the files/move.ts Saga instead of editing file_metadata directly.
  • Write targets must stay inside the key’s upload root (assertWithinUploadRoot).
  • XML hrefs pass through escapeXml to prevent injection and malformed XML.
  • Basic auth uses base64(keyId:secret).

Dependencies: middleware/auth.ts (API key auth), permissions/principal.ts, storage/providers.ts, files/move.ts, utils/path.ts, and utils/crypto.ts.

Responsibilities: temporary sessions with user-supplied object-storage credentials, file listing, upload, delete, and logout. The service encrypts credentials, writes them to KV with a short TTL, and cleans them up automatically.

services/free-mode/
├── handlers.ts // init, files, upload, object, logout
├── schemas.ts // FreeModeInitSchema
└── types.ts

Security notes:

  • middleware/free-mode.ts enforces cross-site protections (Origin and Sec-Fetch-Site), session-level CSRF, and fail-closed IP-plus-user rate limiting.
  • Path and file-name checks reject .., ~, control characters, and backslashes.
  • validateEndpoint runs SSRF checks on the supplied endpoint.
  • Credentials use AES-GCM encryption in KV; the API never returns auth_token, only the fm_token session id.

Dependencies: middleware/free-mode.ts, storage/providers.ts, utils/crypto.ts, utils/ssrf.ts, and utils/path.ts.

Responsibilities: dashboard and statistics, user management, global shares, all files, access logs, system settings, announcements, storage providers, mount points, and permission rules.

services/admin/
├── handlers.ts // dashboard, users, shares, files, logs, settings, announcements
├── storage.ts // storage providers, mount points, permission rules
├── schemas.ts // UserUpdate, Settings, Announcement
├── storage-schemas.ts // Provider, Mount, Rule
└── types.ts

Dependencies: the user, share, log, settings, announcement, provider, mount, and rule repositories, storage/providers.ts, and utils/ssrf.ts.

Responsibilities: profile, appearance preferences, default path, and password change.

services/users/
├── handlers.ts // /me/settings GET/PUT, /me/password PUT
├── schemas.ts // ProfileSchema, PasswordSchema
└── types.ts

Dependencies: the user and quota repositories and utils/crypto.ts.

Responsibilities: API key creation (the secret shows once), listing, revocation, and permission-rule queries.

services/keys/
├── handlers.ts // POST/GET/DELETE /api/keys, GET /api/keys/rules
├── schemas.ts // CreateKeySchema
└── types.ts

Security notes:

  • uploadPath normalization rejects .. and ~ escapes and uses the result as the key’s upload root boundary.
  • The key token displays once at creation; the store keeps a sha256Hex hash.

Dependencies: the API key and rule repositories, utils/crypto.ts, and utils/path.ts.

Responsibilities: site settings, announcements, and health checks without authentication.

services/public/
├── handlers.ts // GET /api/public/settings, /announcements, /health
└── types.ts

Dependencies: the settings and announcement repositories.

Responsibilities: expired quota release, move-source cleanup, expired share marking, and quota reconciliation, run by a scheduled task.

  • releaseExpiredReservations releases quota reservations held by expired upload sessions.
  • cleanupOldObjects deletes source objects left by moves (source_cleanup_pending flag).
  • expireDueShares marks shares as expired.
  • reconcileQuotas corrects the used_storage and used_files counters.
  • runScheduledTasks runs all four tasks and returns a per-task count.

Dependencies: the session, share, file, mount, provider, and quota repositories and storage/providers.ts.

Cross-service code lives in four places:

  • shared/schemas.ts provides common Zod schemas: PathSchema, FileNameSchema, PaginationSchema, UUIDSchema, and PasswordSchema.
  • shared/types.ts defines Principal, Mount, FileMetadata, Conditions, the Env runtime bindings, and the Hono context variables.
  • shared/errors.ts defines ApiError with static constructors: badRequest, unauthorized, forbidden, notFound, conflict, tooManyRequests, and internal.
  • shared/response.ts provides the ok and error helpers that shape the { success, data | error, timestamp } envelope.

The middleware layer handles cross-cutting concerns:

MiddlewarePurpose
middleware/auth.tsauthMiddleware, optionalAuthMiddleware, apiKeyAuthMiddleware, and adminMiddleware; also exposes getDb and getClientIp.
middleware/csrf.tsValidates the CSRF token for cookie-authenticated write operations.
middleware/rate-limit.tsApplies KV fixed-window rate limiting.
middleware/free-mode.tsEnforces free-mode cross-site, CSRF, and rate-limit guards.
middleware/global.tsInitializes the request context, CORS, and security headers.

The db/ directory is the single data-access layer. The Db class wraps either a D1 backend (production on Workers) or a node:sqlite backend (local tests), so business code does not know which one runs. Domain repositories live under db/repos/: users, quotas, providers, mounts, files, sessions, jobs, rules, API keys, shares, logs, settings, announcements, and reconciliation.

The utils/ directory holds path.ts (normalization, boundary checks, pattern matching), crypto.ts (JWT, bcrypt, AES-GCM), ssrf.ts (endpoint validation), smtp.ts, and base64.ts.

D1 stores the following core tables:

TablePurpose
usersUser accounts, roles, status, default path, and locale preferences.
user_quotasUsed and reserved storage, file counts, and limits.
storage_providersS3-protocol provider configuration with encrypted credentials.
mountsMaps a provider to a virtual path with sorting preferences.
file_metadataFiles and folders: object key, path, size, etag, owner, and custom attributes.
upload_sessionsTracks upload progress, parts, and reserved quota.
operation_jobsAsynchronous move, copy, and delete jobs.
path_rulesPermission rules scoped to a mount.
api_keysAPI keys with permissions, protocols, and upload root.
sharesShare links with password, expiry, and access limits.
download_tokensOne-time download tokens consumed atomically.
access_logsAudit log of upload, download, delete, share, and verify actions.
system_settingsKey-value site settings.
announcementsSite announcements and per-user dismissal records.
reconciliation_reportsReports from object-to-database reconciliation.
orphan_objectsObjects whose deletion failed, pending retry.

Migrations live in workers/migrations/:

  • 0001_initial.sql creates the base schema.
  • 0002_add_parts_and_download_tokens.sql adds multipart parts and the download_tokens table.
  • 0003_mount_id_and_session_version.sql adds mount isolation and session revocation fields.
  • 0004_smtp_and_otp.sql adds SMTP and one-time password (OTP) email verification.
TermDefinition
PrincipalThe entity that performs an action: a user, a role, or an API key.
MountMaps a storage provider to a virtual path such as / or /backup.
ProviderA storage provider such as R2, AWS S3, or Oracle Cloud.
Path RuleA permission policy for a specific path pattern, scoped to a mount.
Object KeyThe actual storage path of an object inside the provider bucket.
Canonical PathThe normalized virtual path after standardization.
Upload SessionTracks upload state and the reserved quota for one upload.
Quota ReservedStorage locked at upload start to guarantee the quota fits.
Idempotency KeyA client-generated identifier that prevents duplicate operations.
Download TokenA short-lived token that authorizes one download.
Share LinkA short link that exposes a file to public or password-protected access.

Supported providers:

ProviderStatusProtocolNotes
Cloudflare R2SupportedS3Free tier of 10 GB per month plus 1 million class-A operations; no egress fees.
AWS S3SupportedS3Standard S3 protocol.
Oracle CloudSupportedS3S3-compatible API; custom domains need a CDN such as CloudFront.
Alibaba Cloud OSSNot supportedOSSProtocol incompatible.
Tencent Cloud COSNot supportedCOSProtocol incompatible.

Capability matrix:

CapabilityR2S3Oracle
List objectsYesYesYes
Head objectYesYesYes
Get objectYesYesYes
Range getYesYesYes
Put objectYesYesYes
Multipart uploadYesYesYes
Abort multipartYesYesYes
Copy objectYesYesYes
Delete objectYesYesYes
Presigned URLYesYesYes
Public URLYesYesYes
Custom domainYesYesYes (via CDN)
Checksum (MD5)YesYesYes

Credential model: all providers share one S3-protocol configuration instead of a per-provider discriminated union. The config holds type, name, endpoint, region, bucket, accessKeyId, and secretAccessKey, plus optional publicDomain, uploadDomain, and pathPrefix. The store encrypts credentials with AES-GCM and decrypts them only when the provider client needs them. A provider with type=r2 and no endpoint uses the Workers R2 binding directly.

The security model applies defense in depth across the request lifecycle:

  • Authentication: JWT lives in an HttpOnly cookie with SameSite=Strict; the client never stores it in localStorage.
  • Session revocation: users.session_version feeds into the JWT. Logout, password change, password reset, and admin account disabling bump the version, which invalidates older JWTs immediately.
  • Mount isolation: path_rules.mount_id scopes each rule to a mount (NULL means global); permission queries filter by mount so identical paths on different mounts never interfere.
  • Initial credentials: production requires an ADMIN_PASSWORD of at least 12 characters; without it, the system fails closed and creates no admin. No hardcoded default credentials exist.
  • CSRF: write operations require an X-CSRF-Token verified against KV; API-key authentication bypasses this check.
  • Rate limiting: KV fixed-window counters with fail-closed behavior on authentication and sensitive write endpoints.
  • Path traversal: normalizePath plus isPathWithinBoundary compare path segments.
  • SSRF: validateEndpoint enforces scheme and port allowlists and blocks private IPv4/IPv6 ranges; deployment should pair it with an egress allowlist.
  • Object poisoning: completing an upload forces a HEAD check that verifies ETag and size.
  • XSS: strict file-name validation, React auto-escaping, and pre-escaped highlighting for code previews.
  • SQL injection: every query uses parameterized statements.
  • Download tokens: D1 consumes each token atomically, so concurrent requests cannot reuse a one-time token.
  • Share download counting: issuing a token does not count; the gateway counts once when it consumes the token and returns 410 over the limit.
  • Share passwords: POST /api/shares/:id/verify sets a short-lived authorization cookie; the password never appears in a URL.
  • Large-file memory: PicGo-compatible upload, WebDAV, and free-mode paths stream request bodies instead of buffering them.
  • Readiness: /api/public/health/live and /ready probes gate the API; before initialization, business endpoints return 503.
  • CSP: script-src 'self' https://challenges.cloudflare.com with no unsafe-inline.