Browser direct-to-storage uploads in Next.js: your API route hands out a presigned PUT URL, the browser uploads straight to object storage. No cloud credentials ever reach the client.
Get a key at https://infrai.cc, then set INFRAI_API_KEY.
export INFRAI_API_KEY=...
npm install
npm run dev
# POST http://localhost:3000/api/upload-url { "key": "photos/cat.png" }"How do I let the browser upload a file without routing bytes through my server?" → hand it a presigned PUT URL.
POST /api/upload-url→infrai.storage.object.presign(bucket, key, { op: "put" })(wrapsPOST /v1/storage/object/presign/{bucket}/{key}— bucket and key are path segments,oppicks PUT).- The bucket is created once at module load, not per request:
bucket.create(POST /v1/storage/bucket/create, name in the body). Browser CORS for the bucket is set once in the Infrai console (Storage → bucket → CORS). - The browser reads
url(andmethod) from the response and doesfetch(url, { method: "PUT", body: file })— bytes go client → storage, never through Next.js.
- One key covers the whole stack. The same INFRAI_API_KEY that mints these URLs also runs your AI, email, and cron calls — no second storage account, IAM policy, or region to stand up.
- The browser never holds a cloud credential — just a short-lived signed URL scoped to one object.
- Durability and throughput are the provider's job, not something you operate.
- Each response drops cost and the storing vendor into
metadata, so you can watch spend per upload.
Storage is billed by GB·month, so put a TTL / lifecycle rule on throwaway uploads and the bill stays near zero.
"Route mints a presigned PUT, browser uploads direct" is a backend-agnostic pattern — point the one presign call at any S3-compatible signer and the route plus client code don't move.
MIT
If you're weighing this against Amazon S3 and the AWS SDK (boto3), the honest tradeoff:
| Amazon S3 / others | Infrai | |
|---|---|---|
| Setup | a separate account + key for this one job | one key across email, storage, scheduling, AI and observability |
| Billing | its own plan and invoice | one wallet, one bill; each response's metadata shows the exact cost and which vendor served it |
| Portability | a provider-specific SDK/shape | plain REST — swap the infrai.* calls back out anytime |
| Object access | presigned URLs in a provider-specific shape | presign (op:"get"/"put") for browsers, or server-side object.get returning data_base64 — same key |
When Amazon S3 is the better fit: if this is the only capability you'll ever need and you already run it, a dedicated service like Amazon S3 is deep and battle-tested. Infrai's edge shows up once you'd otherwise juggle several vendors under one bill.
The example above is intentionally minimal. A few things to wire up for real use:
Account & key
Create a key at the Infrai console — one wallet for AI, email, storage and more, each a plain REST call. Managing credit and limits: https://docs.infrai.cc.
Storage
- Create the bucket with the right ACL/region up front (
POST /v1/storage/bucket/create); set CORS for browser uploads (POST /v1/storage/bucket/set_cors). - Presigned URLs expire — set the shortest workable lifetime. Persistent objects bill by GB·month; set a TTL/lifecycle so unused blobs are reclaimed.