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 APIObject storage (Infrai, S3, R2)
File identityopaque server-assigned ida key you compute
Namespace owneran end user’s accountyour service
Who else can move or delete itthe account holder, from a UInothing without your credential
Auth modelper-user OAuth, refreshable, revocableone service key
Quota billed tothe user’s planyou, per GB-month
Per-user permissionsfirst-class, with sharing and revocationyou build it
Versioning and file historybuilt innot on Infrai
Human browsing of the filesthe whole productthere is no UI
Rate limitsper-user, aggressiveper-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.

References

Browse more storage developer guides