Introducing APULODI: file infrastructure without the complexity
Almost every application you will ever ship has a file problem. Avatars, invoices, CSV imports, PDF exports, product photos, chat attachments. And in our experience the story always starts the same boring way: you create a bucket, wire up the SDK, write an upload endpoint, and ship it. Eighteen months later you own all of this:
- presigned-URL logic duplicated across three services, with three different expiry times and three opinions about key naming;
- orphaned objects from uploads that never completed, which nobody reaps because "the sweeper job is on the backlog";
- metadata that quietly drifted from reality — the database says 2.4 MB, the stored object is 2.1 MB, and nobody knows which one is true;
- a delete button that deletes the database row and leaves the bytes in storage forever, billed to you monthly.
None of these problems is hard on its own. That is exactly the trap. Each one gets rebuilt by every team, slightly worse, under deadline pressure. We did it ourselves, several times, at several companies. APULODI is us extracting that plumbing into a platform, once, properly.
What APULODI is
APULODI is file infrastructure for developers. One API — and an official TypeScript SDK on top of it — covering the entire life of a file: upload, organize, version, describe, deliver, observe, delete. You create a project, generate an API key, and everything you do with that key is isolated from every other customer. There is no infrastructure to run, no buckets to name, no sweeper jobs to remember.
It is not a wrapper around "an S3-compatible thing" that we expose for you. APULODI owns the developer experience: identity, authorization, folders, versioning, metadata, events, quotas. The storage layer is ours to evolve — you never see it, and none of our public contract depends on it.
How an upload actually works
The most important engineering decision in APULODI is that file bytes never touch our API. Here is the full life of an upload:
- Your server asks APULODI for an upload: filename, content type, size
(
POST /v1/files/upload). We validate everything, check your project's quotas, generate the file ID and the storage key server-side, and record the file aspending. - We hand you back a presigned upload URL bound to that exact key and content type, with a short expiry.
- You
PUTthe bytes straight to storage. Your application servers are not in the path at all. - You call the complete endpoint. We do not take your word for it — we
verify the object in storage, check the actual size and content type
against what you declared, and only then mark the file
uploaded. A mismatch marks the uploadfailedwith a specific error code.
The payoff: a two-gigabyte video upload never holds open a connection to your API, never lands in a memory buffer, and never gets rate-limited by the same process that serves your login page. Your API tier stays small and cheap; the storage layer does what storage layers are good at.
For files too large for a single request, the same idea extends into multipart sessions: you open a session, receive presigned URLs for each part, upload parts in parallel, retry any individual part that failed, and complete the session. Parts are 8 MiB, sessions are tracked server-side, and the SDK does the part bookkeeping for you — it switches from a single PUT to multipart automatically once a file crosses the 8 MiB threshold.
Everything that comes with it
| Capability | What you get |
|---|---|
| Folders | Logical paths like users/avatars. Created automatically by path-aware uploads. Renames and moves never touch the physical layout. |
| Versioning | Replacing a file bumps its version and retains the previous bytes. file.version tells you where you are. |
| Metadata | Arbitrary JSON key/value pairs per file — userId, documentType, source — validated and size-bounded. |
| Copy | Server-side copies between folders without bytes crossing your network. |
| Search | Name search, path/content-type/status filters, validated sorting, cursor pagination. |
| Webhooks | A signed event for every state change, with retries and redelivery. |
| Usage & quotas | Daily metered rollups and enforceable per-project limits with descriptive 403 errors. |
Folders deserve a special mention because they are where most DIY setups go
wrong. In APULODI, users/avatars is a real, addressable thing — you can list
a folder's children — but it is deliberately decoupled from how bytes are
laid out internally. You name things the way your product thinks; we handle
the way storage thinks.
What it looks like in code
Install the SDK and upload your first file:
import { Apulodi } from "@apulodi/sdk";
const apulodi = new Apulodi({
apiKey: process.env.APULODI_API_KEY!,
});
// 1. Upload: the SDK gets a presigned URL, PUTs the bytes, and completes
// the upload — returning the verified file record.
const file = await apulodi.files.upload({
file: buffer,
fileName: "passport.jpg",
contentType: "image/jpeg",
path: "kyc/documents",
metadata: { userId: "usr_1042", documentType: "passport" },
});
console.log(file.id, file.version, file.size); // file_8f92… 1 48291
// 2. List everything in that folder, newest first.
const { data } = await apulodi.files.list({ path: "kyc/documents" });
// 3. Hand your user a short-lived download URL.
const { url } = await apulodi.files.downloadUrl(file.id);
If you prefer raw HTTP, the same flow is three calls — request, PUT,
complete — and every endpoint is documented with request/response examples in
the API reference.
Tenancy: organizations, projects, keys
An organization owns members and billing. Inside it, projects are the
isolation boundary — one per app or environment, like production-api and
mobile-app. API keys belong to a project, never to a person. Object keys are
derived server-side from the organization, project and file IDs, so a client
cannot influence where its bytes land, and a key from project A gets a plain
404 — not a 403 — when it reaches for project B's files. We do not confirm
that the file exists.
This matters more than it sounds. When you eventually store customer documents, the question "could a leaked test key read production data?" must have exactly one answer, and it must be no.
Choices we made, and what they cost
We would rather document our trade-offs than pretend they do not exist:
- Download bandwidth is metered when the download URL is issued, not when bytes move. After we hand you a presigned GET, the transfer happens directly between storage and your user — which is exactly why it is fast — but it also means we cannot count the bytes after the fact. Proxying every download to make the graph prettier would be the wrong trade.
- Deletion is a two-stage thing. Deleting a file removes it from listings
and API access immediately, but the bytes survive for a 7-day grace window
so
restoreis real, not theoretical. After the window, a purge sweep removes the object and emitsfile.purged. - There are no public buckets and there never will be. Delivery happens through short-lived signed URLs. If you need public delivery, that is a CDN layer we will build properly — not a bucket policy you toggle at 2am.
- Upload creation accepts an
Idempotency-Keyheader. Mobile clients retry. Without idempotency you get duplicates; with it, the same key returns the same pending file instead of a new one.
What is next
The event pipeline that powers webhooks today is the same one that will drive media processing — thumbnails, transcoding, transformations — without you having to run workers. A CDN layer comes after that. And then the part we are personally most invested in: local-currency billing. We are starting with Malawi — PayChangu, Airtel Money, TNM Mpamba, pricing in Kwacha — because we have watched talented developers abandon a perfectly good platform at the checkout simply because they do not hold a card that talks to Stripe. That is a product requirement here, not a nice-to-have.
Come build
The Quickstart takes you from zero to a verified upload in a few minutes, and the free plan is genuinely free — no card, no trial clock. If something feels off, or a file API question keeps you up at night, we want to hear about it.