Drive and Dropbox APIs vs object storage for app-generated files
The real split isn't features — it's who owns the namespace. Why a key you compute beats a file id someone else assigns, and when Drive is still the right answer.
The essential difference is who owns the namespace. Google Drive and Dropbox hand you an opaque file id inside a human being’s account — the human can rename it, move it, unshare it or delete it, and your OAuth grant can be revoked on a Tuesday. Object storage like Infrai’s gives you a key you computed inside a bucket your service owns, and that key is a primary key: it resolves tomorrow because nothing with a mouse can touch it.
Everything else — pricing, quotas, upload limits — follows from that one fact. If your app generates the file, manages it, and is the only thing that ever reads it, you want a keyspace, not a filing cabinet.
A key you compute versus an id you’re given
In Drive, creating a file returns an id like 1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms. You store that id, and it’s the only handle you have. Ask “where is customer 42’s July invoice?” and you have to have written the mapping down somewhere, because the id encodes nothing.
In object storage you decide the key up front:
tenants/acme/invoices/2026-07/INV-1042.pdf
That string is derived from data your application already has, which means the lookup is a printf, not a join. Your database row can hold the tenant and the invoice number and reconstruct the location; if the row is lost you can still enumerate what exists.
export INFRAI_API_KEY="your_infrai_api_key"
PDF_B64=$(base64 < ./invoice-1042.pdf | tr -d '\n')
PAYLOAD=$(printf '{"data_base64":"%s","content_type":"application/pdf","metadata":{"tenant-id":"acme","doc-kind":"invoice"}}' "$PDF_B64")
curl -sS -X PUT "https://api.infrai.cc/v1/storage/object/put/kb-appfiles-0726/tenants/acme/invoices/2026-07/INV-1042.pdf" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d "$PAYLOAD"
{
"ok": true,
"data": {
"bucket_id": "bkt_e0e1a2ebc3d74a2a9eb890",
"key": "tenants/acme/invoices/2026-07/INV-1042.pdf",
"size_bytes": 17,
"etag": "a9b8798d8df975340da2e17c1f85ffc7",
"content_type": "application/pdf",
"metadata": { "tenant-id": "acme", "doc-kind": "invoice" },
"created_at": "2026-07-26T00:52:04.735019Z"
}
}
No id came back that you didn’t already know. That’s the point.
Enumeration is structural, not a search index
Drive’s answer to “what files does this app own?” is a query language with q= filters against a shared user drive. Object storage’s answer is a prefix scan, and it’s exact:
curl -sS "https://api.infrai.cc/v1/storage/object/list/kb-appfiles-0726?prefix=tenants/&delimiter=/" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
{
"ok": true,
"data": {
"items": [],
"next_cursor": null,
"common_prefixes": ["tenants/acme/", "tenants/globex/"]
}
}
Note items is empty and everything came back under common_prefixes — with a delimiter set, that call is answering “what folders exist at this level?”, not “what files are here?”. Drop the delimiter and you get objects back, paged by next_cursor.
And confirm a single file the cheap way:
curl -sS "https://api.infrai.cc/v1/storage/object/head/kb-appfiles-0726/tenants/acme/invoices/2026-07/INV-1042.pdf" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
{
"ok": true,
"data": {
"found": true,
"status": "found",
"key": "tenants/acme/invoices/2026-07/INV-1042.pdf",
"size_bytes": 17,
"etag": "a9b8798d8df975340da2e17c1f85ffc7",
"content_type": "application/pdf",
"metadata": { "doc-kind": "invoice", "tenant-id": "acme" },
"last_modified": "2026-07-26T00:52:04Z"
}
}
A caveat we hit while testing this: GET /v1/storage/object/list/{bucket} returns content_type and metadata as null even for objects that demonstrably have both. Use head per object when you need those fields, and don’t build a filter on the listing.
The whole comparison, in one place
| Google Drive / Dropbox API | Object storage (Infrai, S3, R2) | |
|---|---|---|
| File identity | opaque server-assigned id | a key you compute |
| Namespace owner | an end user’s account | your service |
| Who else can move or delete it | the account holder, from a UI | nothing without your credential |
| Auth model | per-user OAuth, refreshable, revocable | one service key |
| Quota billed to | the user’s plan | you, per GB-month |
| Per-user permissions | first-class, with sharing and revocation | you build it |
| Versioning and file history | built in | not on Infrai |
| Human browsing of the files | the whole product | there is no UI |
| Rate limits | per-user, aggressive | per-account |
What Drive and Dropbox do that object storage doesn’t
Real access control, for one. Drive’s sharing model is per-identity, enforced by the platform, revocable, and auditable — and that’s a genuine limitation on the object-storage side that’s easy to gloss over.
Worth flagging specifically for Infrai: a presigned download URL is an expiry mechanism, not an authorisation mechanism. We created a 300-second signed GET and then stripped the query string off it:
SIGNED=$(curl -sS -X POST "https://api.infrai.cc/v1/storage/object/presign/kb-appfiles-0726/tenants/acme/invoices/2026-07/INV-1042.pdf" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"op":"get","expires_seconds":300}' \
| python3 -c "import json,sys; print(json.load(sys.stdin)['data']['url'])")
curl -sS -o /dev/null -w "with signature: %{http_code}\n" "$SIGNED"
curl -sS -o /dev/null -w "signature stripped: %{http_code}\n" "${SIGNED%%\?*}"
Both return 200. The signature governs how long the link is convenient, not who may read the object, and the URL also discloses your account’s path prefix. So keep object keys unguessable and server-derived — a UUID segment rather than INV-1042.pdf if the content is sensitive — and put your real authorisation check in the handler that decides whether to mint the link at all. If you need platform-enforced per-recipient permissions on documents humans will open, Drive or Dropbox genuinely is the better tool and you should use it.
The other honest gap: there’s no versioning and no undelete. Overwrite a key and the previous bytes are gone.
The generate-and-manage loop, end to end
This is the pattern for files your app makes and only your app reads — a deterministic key, a write, and a verification read that proves the write landed:
import { readFile } from "node:fs/promises";
import process from "node:process";
const API = "https://api.infrai.cc";
const KEY = process.env.INFRAI_API_KEY;
if (!KEY) throw new Error("INFRAI_API_KEY is not set");
const BUCKET = "kb-appfiles-0726";
const objectKey = ({ tenant, period, invoiceNo }) =>
`tenants/${tenant}/invoices/${period}/INV-${invoiceNo}.pdf`;
async function request(path, verb, payload) {
const res = await fetch(`${API}${path}`, {
method: verb,
headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
body: payload === undefined ? undefined : JSON.stringify(payload),
});
const json = await res.json();
if (!res.ok || json.ok === false) {
const e = json.error ?? {};
throw new Error(`${verb} ${path} -> HTTP ${res.status} ${e.code ?? ""} ${e.message ?? ""}`);
}
return json.data;
}
export async function storeInvoice(localPath, descriptor) {
const key = objectKey(descriptor);
const bytes = await readFile(localPath);
const written = await request(`/v1/storage/object/put/${BUCKET}/${key}`, "PUT", {
data_base64: bytes.toString("base64"),
content_type: "application/pdf",
metadata: { "tenant-id": descriptor.tenant, "doc-kind": "invoice" },
});
const check = await request(`/v1/storage/object/head/${BUCKET}/${key}`, "GET");
if (!check.found || check.size_bytes !== written.size_bytes) {
throw new Error(`post-write verification failed for ${key}`);
}
return key;
}
const key = await storeInvoice("./invoice-1042.pdf", {
tenant: "acme",
period: "2026-07",
invoiceNo: "1042",
});
console.log(`stored ${key}`);
Two things in there are load-bearing. Metadata keys must be hyphenated — a key containing an underscore breaks the upstream request signing and comes back as a 503, which is a miserable way to spend an afternoon. And POST /v1/storage/object/set_metadata/{bucket}/{key} replaces the metadata map rather than merging into it, so read before you write if you’re adding a single tag.
Cost, and the part that isn’t about storage
Writes cost $0.0001 per call and reads $0.0002; listing, heading and presigning are free and rate-limited. Verified 26 July 2026, with $2 of trial credit on a new account — about 19,999 writes. Pull today’s numbers rather than trusting the paragraph:
curl -sS "https://api.infrai.cc/v1/discovery" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
| python3 -c "import json,sys; [print(c['id'], c['billing'].get('price_usd','free')) for c in json.load(sys.stdin)['capabilities'] if c['id'].startswith('storage.object')]"
Rates in this segment keep drifting downward, so read them as an upper bound. Drive and Dropbox don’t price per call at all — they price per seat, which is the tell that they’re built for people rather than programs.
If your files are genuinely app-owned, the choice among object stores is then a normal one: S3 if you’re already inside AWS, Cloudflare R2 if egress dominates your bill, MinIO if the bytes can’t leave your hardware. Infrai’s argument is narrower and worth stating plainly — the same credential that stores the invoice also renders it, emails it, queues the retry and attributes the cost to the tenant, so the second question after “the file is stored” doesn’t start with another vendor signup.