Fileaway Architecture board
Read only

drag to pan  ·  scroll to move  ·  ctrl+scroll to zoom

Fileaway · architecture · read-only

Receiving files without ever holding them.

0 — Cover

Fileaway hands someone a link. They drop a file on it, and the file lands in your own S3 or R2 bucket — your account, your bill. No uploaded byte is ever in Fileaway's memory, disk, or egress.

That single constraint decides the rest of the system: what the API may do, where the edge sits, what can be cached, what must be reserved before it is real, and what must be reconciled afterwards.

1 — The constraint

Most upload features put the server in the middle of the bytes: the browser POSTs the file to the API, which re-uploads it to storage. That buys 413s, proxy timeouts, buffered memory per concurrent upload, and bandwidth billed twice.

Fileaway keeps the server in the control path only. The browser asks for permission, gets back a presigned URL, and sends the file straight to the bucket. A few hundred bytes of signature cross our servers; the file crosses the internet once.

2 — The map

Six deployables: the marketing site and docs (both Next 16 static exports), the React 19 dashboard and public upload page, the fileaway-sdk npm package, a Cloudflare Worker with KV in front of the public upload routes, and the Express 5 API on EC2 behind nginx and pm2.

State lives in MongoDB. Files live in the customer's own S3 or R2. Stripe handles billing and ZeptoMail transactional email. The API is the only component that can decrypt a customer's bucket credentials, and it does so only to sign.

3 — The upload path

The upload is a two-phase exchange. First the browser asks the edge about the link; the Worker answers from a 30-second KV cache and rejects inactive, expired or exhausted links without troubling the origin.

A presign request that survives the edge reaches the API, which validates it, decrypts the customer's bucket credentials just long enough to sign a 15-minute PUT URL, writes a pending Upload row with an HMAC token and a TTL, and atomically increments the link's upload count to reserve the slot.

The browser then PUTs the bytes directly to S3 or R2 — the only step that carries a file, and one Fileaway is not on the wire for. A complete call carrying the token makes the API HEAD the object to confirm it landed, flips the row to committed, and fans out a signed webhook plus a WebSocket frame to the owner's dashboard.

4 — Big files

Large files use S3/R2 multipart. start opens the upload and persists partSize and partCount on the Upload row; parts re-signs any subset of parts at any time, which is what makes resume cheap; complete assembles them and runs the same HEAD-and-commit as a single-shot upload; abort cancels upstream and refunds the reserved slot.

On the client every part is wrapped in a pause-aware retry, so a paused upload parks rather than fails and resumes against freshly signed parts without re-reading the file from zero.

5 — Inside the API

The API is layered routes → controllers → services → repositories → models, and nothing calls upwards. Zod validates at the request edge, typed errors are caught by one middleware and rendered through one response envelope, pino handles structured logging, and rate limits are per-IP on public uploads and per-key on the programmatic API.

Mount order is load-bearing: /auth, /u, /management and /subscriptions claim their prefixes first, and the API-key-gated router is mounted last at the root, so nginx can map api.fileaway.io onto it and integrators write /links and /uploads/:id with no version prefix. The proxy-download route is registered before the key gate because it accepts its own short-lived token grant.

6 — The data

Link is the contract with the uploader: an unguessable 32-hex slug, optional password and expiry, an upload ceiling, a MIME allowlist and size cap, a destination key prefix, a naming rule that can validate or rewrite filenames, per-link branding, free-form metadata echoed into webhooks, and the webhook config itself.

Upload is the receipt, moving pending → committed with an HMAC token, a TTL on the pending reservation, and multipart bookkeeping. Integration holds the customer's bucket credentials encrypted at rest. Around them sit User, ApiKey, Pack and Subscription, Notification, WebhookDelivery and IdempotencyResponse.

7 — Trust

Customer bucket credentials are encrypted at rest with AES-256-GCM as iv:authTag:ciphertext, decrypted in memory only to mint a signature, and never logged or returned by any endpoint. Production refuses to boot if the JWT secrets, encryption key, Mongo URI or CORS origin are still at their development defaults.

Sessions use short-lived JWT access tokens with refresh tokens, plus Google OAuth. API keys are hashed, scoped per route, entitlement-checked and rate-limited per key. Public links carry an unguessable slug and opt-in password, expiry, ceiling, MIME allowlist, size cap and filename rule. Downloads stream through the API under a short-lived token grant, so bucket URLs are never exposed.

8 — The unhappy path

Upload counts are reserved at presign with an atomic increment, so two uploaders cannot both take the last slot — at the cost of leaving the count inflated when a presign is abandoned. A TTL drops the pending row, and an hourly reconciler recomputes committed plus still-live reservations, writing only when it finds drift and tolerating a one-cycle off-by-one.

Webhooks retry three times with 1s/4s/16s backoff, signed with HMAC-SHA256 over timestamp and body, and every attempt is logged to WebhookDelivery for the dashboard. Side-effectful POSTs accept an Idempotency-Key backed by a unique (key, scope) index, giving exactly one execution per key, with losers returning the winner's cached response.

9 — Where it runs

deploy.sh runs on an Ubuntu EC2 box: it builds the React client to /var/www/html, the landing and docs static exports to /var/www/landing and /var/www/docs, typechecks the server and restarts it under pm2, which runs src/index.ts through node --import tsx. nginx.conf is synced, validated with nginx -t and only then reloaded; a missing TLS certificate aborts the deploy before anything is overwritten.

At the edge, a Cloudflare Worker fronts the public upload routes with a KV cache of link info. The marketing site has a second build target with NEXT_PUBLIC_SHOW_AUTH=false that ships as a static-asset Worker — one codebase, a full funnel on EC2 and a read-only shopfront at the edge. Nothing stateful lives on Cloudflare.

10 — What it buys

What the shape buys: no size ceiling to defend, because 413s and proxy timeouts are designed out rather than tuned down; storage that stays the customer's, in their region under their retention policy; dead links that die at the edge for the price of a KV read; and an upload that is already an event, signed and delivered with the integrator's own metadata attached.

Fileaway · built and operated by Hassen Ben Mbarek