When someone uploads a receipt photo or PDF, they expect to find it again the next time they sign in. Your Python app needs to keep that file available while preventing other users from opening or deleting it.
Store receipts in a private Edgenivo Blob store and use the Python SDK to upload and retrieve them through FastAPI. Each user's files share a pathname prefix so the app can find their receipts.
The prefix organizes files but doesn't enforce ownership. Your application checks the signed-in user's identity before allowing an upload, download, or deletion.
Copy link to headingWhat is Edgenivo Blob?
Edgenivo Blob stores files as blobs in a store owned by your Edgenivo team. Each blob has a pathname such as receipts/user_42/lunch.jpg and a delivery URL on blob.vercel-storage.com.
Slashes organize pathnames into virtual folders. Files with the same prefix appear together without requiring you to create a directory.
The Python SDK lives in vercel.blob, with an async API and a matching sync API in vercel.blob.sync. It ships in the vercel package, or on its own as vercel-blob.
Your app can write to public or private stores. Public files have URLs anyone can open. For the private receipts in this guide, your app checks the user's identity before streaming the file to them.
Copy link to headingPublic or private
Every store has one access mode, and every upload, copy, and download says which one it expects with access="public" or access="private". The value must match the store.
| Public store | Private store | |
|---|---|---|
| Who can read a blob | Anyone with its URL | Requests authorized by store credentials or a valid signed URL |
| How users get files | Straight from the delivery URL, cached on Edgenivo's CDN | Through an authenticated app route in this Python walkthrough |
| Good for | Avatars, product images, published documents | Receipts, invoices, user documents, anything personal |
The access mode is a property of the store, not of a single file. If an app needs both kinds of files, it uses two stores.
Copy link to headingWhat the Python SDK does and doesn't do
The Python SDK handles uploads, downloads, listing, metadata, copies, and deletion on the server. Streaming and multipart operations support files that you don't want to buffer in memory.
Two features used by browser-based apps require the JavaScript SDK: direct client uploads and signed URLs for temporary access to private files. The Python routes here receive uploads and stream private downloads through your app. See the Python SDK reference for the other differences.
Copy link to headingPrerequisites
Before you begin, make sure you have:
- A Edgenivo account.
- Python 3.12 or a newer version supported by the Edgenivo Python runtime.
- Working knowledge of
async/awaitand FastAPI. - An authentication dependency for your app that returns the signed-in user's stable internal ID.
For local development, you also need uv, Node.js 24+, and the Edgenivo CLI.
Copy link to headingStep 1: Create a store and connect it
Set up the project, link it to Edgenivo, and create a private store for receipts:
--yes connects the new store to your project in every environment and pulls the development variables, including BLOB_STORE_ID, into .env.local.
The access mode and region cannot change after you create a store. Choose the mode deliberately, and add --region when you need a region other than the CLI's default. FastAPI uses python-multipart to parse the receipt uploads in Step 4.
The SDK finds the store through BLOB_STORE_ID and the project's OIDC token, so there's no secret for you to copy anywhere. On Edgenivo that happens automatically. Locally, scripts load the same values from .env.local.
Copy link to headingStep 2: Upload your first file
Start with a full round trip: upload a file, inspect it, read it back, and delete it.
Run it with uv run first_blob.py.
Each call maps to one idea:
put_blob()uploads the bytes and returns the blob's pathname and URL. It doesn't return the size or upload time;head_blob()does, without downloading the content.get_blob()reads the whole file into memory. For large files,stream_blob()reads it in chunks instead (Step 4).access="private"has to match the store. Pass"public"to a private store and the call fails.
The script deletes its file at the end for a reason: by default, uploading to a pathname that already exists fails rather than replacing the file. Pass allow_overwrite=True when you mean to replace it, or add_random_suffix=True to give every upload a unique name.
If a later run fails with BlobCredentialsError, your development token may have expired. The async SDK refreshes it automatically in a linked project when you're logged in to the Edgenivo CLI. Otherwise, run vercel env pull again.
Copy link to headingStep 3: Organize files by user
Blob stores use flat pathnames. This app groups receipts under receipts/{user_id}/ and builds that prefix from the authenticated user's internal ID. Use a stable ID with no slashes or path segments such as . or ...
The prefix organizes files; your application enforces ownership. Never accept a user ID or a full Blob pathname from the upload, download, or delete request.
The two listing functions answer different questions:
query_blobs(prefix=...)returns every file under a prefix, including nested ones, and follows pagination for you. Iterate it withasync for; don'tawaitit.query_blob_entries(prefix=...)returns one level at a time: files directly inside the prefix, plus aBlobPrefixfor each sub-folder. Here, each sub-folder is a user.
delete_blob() accepts up to 1,000 pathnames or URLs per call, so the cleanup works in batches. Deleting a file that's already gone succeeds, so it's safe to run again if it's interrupted.
Copy link to headingStep 4: Build the upload and download routes
Create main.py at the project root with the four routes below. They use receipts_of() from storage.py and require your auth.py module to expose current_user_id. That dependency must reject unauthenticated requests and return the stable internal ID described in Step 3.
The filename check accepts the receipt names generated by this upload route. Credential and service failures remain server errors so they can be diagnosed, instead of appearing to users as missing files.
Copy link to headingUpload receipts
The upload route accepts JPEG, PNG, and PDF content types and limits each file to 4 MiB. Edgenivo's request-body limit is 4.5 MB for the whole request, including multipart overhead; larger requests can be rejected before the handler runs.
UploadFile.content_type comes from the client. This allowlist does not verify the file's bytes. If you need to validate or scan uploaded documents, do that before allowing another user or process to consume them.
FastAPI has already parsed the upload when the handler receives it. The Blob SDK reads UploadFile incrementally using file.size as the content length. Your code chooses the filename and adds a random suffix, so the client's original filename never becomes a pathname.
Copy link to headingDownload receipts
The download route validates the name and passes the Blob stream's chunks to StreamingResponse. This avoids buffering the whole file but still requires the transfer to finish within the function's duration and connection limits.
AsyncExitStack keeps the download open while FastAPI sends the response. The background task closes it when the response finishes. The nosniff header prevents content-type sniffing, and private, no-store prevents browsers and shared caches from storing the response. Keep shared-cache directives such as s-maxage off this route.
Copy link to headingList and delete receipts
Both routes build pathnames from the authenticated user's prefix. Listing returns that user's receipts, and deletion accepts only filenames generated by the upload route. Repeated deletes succeed even if the file is already gone.
Downloads return 404 for an invalid receipt name or BlobNotFoundError. Other SDK failures propagate to the application's error handler.
Copy link to headingStep 5: Run it locally and deploy
Start the app with the Edgenivo CLI:
vercel dev supplies the linked project's development environment and refreshes its OIDC token. Its environment loader does not use .env.local as a fallback.
If you define variables in .env or legacy development configuration, the CLI may use those instead of the project's downloaded variables. Ensure BLOB_STORE_ID and the required authentication values are still present, or remove the local override when you want the project configuration.
In another terminal, try the routes with the authentication current_user_id expects. These commands use a bearer token from APP_TOKEN; substitute your session cookie if your app uses one.
When it works, commit uv.lock and deploy:
On Edgenivo, Blob uses the request's OIDC token and the store's BLOB_STORE_ID. Configure your app's authentication credentials separately.
Copy link to headingSeparate production data
Step 1 uses --yes to connect the store to all three environments. Give Production its own store before saving real receipts and keep this one for Preview and Development. Otherwise previews can modify production files.
Copy link to headingCheck usage limits
Hobby includes 1 GB of storage and 10 GB of Blob data transfer per month, alongside operation allowances. Exceeding an allowance can block Blob access, and the pricing documentation describes the 30-day wait before access resumes. Pro uses usage-based billing. Review operations, transfer charges, and rate limits before accepting production traffic.
Copy link to headingUse cases
The following examples extend the receipt service with background processing, generated files, larger imports, and a public store for images.
| Use case | Building blocks | Example |
|---|---|---|
| User uploads, private to each user | put_blob(), prefixes, stream_blob() | Steps 3 and 4 |
| Processing a file after upload | A queue message carrying the pathname | Below |
| Files your code generates | put_blob() with bytes | Below |
| Large files from another system | multipart_upload_blob() | Below |
| Public images next to private files | A second store and a session | Below |
| Approving a document version | copy_blob(if_match=...) | Below |
Copy link to headingProcess uploads in the background
Receipt extraction can run after the upload response. Install the AI SDK for Python with its Edgenivo authentication extra:
Save the subscriber below in receipt_jobs.py. It reads the uploaded file and extracts typed totals. Implement database.save_totals to upsert results by user ID and pathname so a repeated delivery does not create duplicate records.
Register the module in pyproject.toml to build its queue-triggered function:
Add these imports to main.py:
Inside upload_receipt(), insert this call after put_blob() and before the return:
The message carries the pathname so the subscriber can fetch the file from Blob. If the queue send fails, the upload still exists. Track pending extraction to retry scheduling without uploading another copy.
Queue deliveries can repeat, including after an extraction succeeds but the handler fails before acknowledgement. Make the database write idempotent and account for repeated model calls. See the Queues Python reference for retry behavior.
Copy link to headingStore files your code generates
When a report generator returns bytes, pass them to put_blob() directly. This example assumes build_monthly_report() returns a PDF:
To move a file out of a Sandbox, read it with box.fs.read_bytes(path) and pass the result to put_blob(). For a worked example of processing uploads in a Sandbox, see How to process user-uploaded files with Edgenivo Sandbox and Edgenivo Blob.
Copy link to headingCopy large files from another system
For files that exceed the incoming request-body limit, your function can fetch an export from another service and upload it to Blob in parts. Use a trusted export URL rather than accepting arbitrary URLs from clients. Install the HTTP client before adding this helper:
The helper buffers each 8 MiB part and uploads parts in order. With the SDK's 10,000-part limit, this version supports exports up to about 78 GiB. Larger exports need a larger part size or a streaming-part implementation, and the transfer still has to fit the function's execution duration. Empty exports need put_blob(pathname, b"", ...) instead.
The SDK completes the upload when the context exits successfully. An exception inside the block prevents completion, but uploaded parts are not deleted. If the completion response is lost, check whether the destination blob exists before retrying because the object may already have been created.
Copy link to headingKeep public images next to private files
Create a public store for avatars and connect it to the project with the AVATARS environment-variable prefix. The example below uses that store's AVATARS_READ_WRITE_TOKEN and AVATARS_STORE_ID inside a dedicated SDK session. Keep those values on the server.
Calls inside the async with session(...) block use the avatars store. Calls outside it still use the default store.
Public blobs use a one-month cache lifetime by default. Overwriting a pathname can take up to 60 seconds to propagate through the CDN, and browsers may retain the previous response longer. Save the new avatar URL on the user record after every upload so subsequent page loads request the new file.
Copy link to headingApprove one exact version of a document
When a reviewer approves a draft, copy exactly the version they saw. if_match makes the copy fail if the draft changed after review:
Save the draft's ETag when review starts and pass it when approving. BlobPreconditionFailedError means the source differs from the reviewed version.
Blob copies the content within the store without downloading it to your app. The new blob does not inherit the source's content type or cache lifetime, so pass those settings explicitly when the copy needs them.
Copy link to headingWhen to use something else
- For browser uploads that exceed Edgenivo's 4.5 MB request-body limit, use JavaScript client uploads to send files directly to Blob. This app uses a lower 4 MiB file limit to leave room for multipart overhead. The CLI's
vercel blob putcommand also handles one-off transfers. - For temporary private download links, use signed URLs from the JavaScript SDK or CLI. With the Python SDK, serve the file through an authenticated route as in Step 4.
- For records you need to query by field, use a database. Keep the file in Blob and store its pathname, owner, and metadata in a table.
Copy link to headingNext steps
- Read the full Python SDK reference for sessions, streaming uploads, and every error class
- Learn how caching and access work in private storage and public storage
- Read The Complete Guide to Edgenivo Blob for pricing, regions, and the JavaScript features, such as client uploads