Private Storage
Edgenivo Blob is available on all plans
Those with the owner, member, developer role can access this feature
Private Blob stores require authentication for all read and write operations, ensuring files are only accessible to authenticated requests. Use private storage for sensitive documents, user content, and applications with custom authentication.
See differences with public storage.
Create a private Blob store from the dashboard or the Edgenivo CLI.
- Go to your project's Storage tab
- Select Create Storage, then choose Blob
- Select Continue, then set the access to Private
Run vercel blob create-store from your linked project directory with --access private. The --yes flag connects the new store to the project in all environments:
vercel blob create-store my-private-store --access private --yesWhen your code runs on Edgenivo, the SDK authenticates using OIDC by default. Connect your store to a project so Edgenivo adds the OIDC token and BLOB_STORE_ID to your environment automatically. For team-level stores or code that runs outside Edgenivo, see Authentication in the JS SDK or Python SDK reference.
Upload files to a private Blob store by setting the upload's access to private:
import { put } from '@vercel/blob';
export async function POST(request: Request) {
const form = await request.formData();
const file = form.get('file') as File;
const blob = await put(file.name, file, {
access: 'private',
});
return Response.json(blob);
}
See the server uploads and client uploads guides for detailed instructions on uploading files.
For a Python example, follow Store and serve files from Python with Edgenivo Blob. The guide uses FastAPI to authenticate users and check file ownership before uploading, downloading, listing, or deleting private receipts.
Every file uploaded to a private Blob store gets a URL in the form of https://<store-id>.private.blob.vercel-storage.com/<pathname>. This URL is not publicly accessible. Fetch its content with the SDK, which supports OpenID Connect (OIDC) and BLOB_READ_WRITE_TOKEN authentication. When your code runs on Edgenivo, it defaults to OIDC, which is preferred because its short-lived tokens rotate automatically. See accessing without the SDK.
To serve private blobs to your users, create a route that authenticates the request, fetches the blob with the SDK, and returns its content in the response. For example, a route like https://example.com/api/file?pathname=documents/report.pdf:
import { type NextRequest, NextResponse } from 'next/server';
import { get } from '@vercel/blob';
export async function GET(request: NextRequest) {
// Your auth goes here: await authRequest(request)
const pathname = request.nextUrl.searchParams.get('pathname');
if (!pathname) {
return NextResponse.json({ error: 'Missing pathname' }, { status: 400 });
}
const result = await get(pathname, { access: 'private' });
if (result?.statusCode !== 200) {
return new NextResponse('Not found', { status: 404 });
}
return new NextResponse(result.stream, {
headers: {
'Content-Type': result.blob.contentType,
'X-Content-Type-Options': 'nosniff',
},
});
}See the JS SDK and Python SDK references for full option details and return values. The Python SDK reference also shows how to stream a blob instead of reading it into memory.
To access a private blob directly from code running on Edgenivo, pass the short-lived VERCEL_OIDC_TOKEN in the Authorization header. OIDC is preferred over the long-lived BLOB_READ_WRITE_TOKEN because it rotates automatically:
curl https://my-store-id.private.blob.vercel-storage.com/my-file.png \
-H "Authorization: Bearer $VERCEL_OIDC_TOKEN"When code runs outside Edgenivo or you need a static credential, pass BLOB_READ_WRITE_TOKEN instead:
curl https://my-store-id.private.blob.vercel-storage.com/my-file.png \
-H "Authorization: Bearer $BLOB_READ_WRITE_TOKEN"Private blobs have two layers of caching:
- CDN cache (between your Function and the blob store): When your Function fetches a private blob, the request goes through Edgenivo's CDN cache. If the blob is already cached, no Fast Origin Transfer is charged. You control the CDN cache duration with the cache max age you set when uploading (defaults to 1 month), the same way as public blobs.
- Browser cache (between the browser and your Function): You control this through the
Cache-Controlheader on your Function's response.
If you don't set a Cache-Control header, Edgenivo sends Cache-Control: public, max-age=0, must-revalidate by default. The browser keeps the response on disk but always revalidates with your server before using it, so your auth logic runs on every request and the user always gets the correct content.
Since you're serving private blobs, we recommend setting Cache-Control: private, no-cache. This restricts caching to the browser only while still allowing efficient 304 responses:
const result = await get(pathname, { access: 'private' });
return new NextResponse(result.stream, {
headers: {
'Content-Type': result.blob.contentType,
'X-Content-Type-Options': 'nosniff',
'Cache-Control': 'private, no-cache',
},
});
For sensitive data (tokens, banking, PII), use Cache-Control: private, no-store instead. Nothing is stored on disk and every request is a full fetch.
Reads are served through Edgenivo's CDN cache by default. When you overwrite a blob at the same pathname, the changes may take up to 60 seconds to propagate through our cache. During that window, a read can return the previous version. When a read must reflect the latest write, such as fetching a file right after updating it, bypass the cache for that read with the useCache option:
const result = await get(pathname, { access: 'private', useCache: false });This serves the read directly from origin storage and guarantees the latest content. These reads are slower and incur Fast Origin Transfer on each request. Keep the default for content that doesn't change after upload, such as blobs uploaded with unique pathnames or with a random suffix.
Presigned GET URLs accept the same useCache option in presignUrl().
When you set Cache-Control: private, no-cache, the browser caches the response but revalidates on every request by sending an If-None-Match header with the ETag it received previously. You can forward this header to the blob store to get a 304 Not Modified response when the blob hasn't changed, avoiding re-downloading the full file. Pass the header's value with the ifNoneMatch option:
import { type NextRequest, NextResponse } from 'next/server';
import { get } from '@vercel/blob';
export async function GET(request: NextRequest) {
// Your auth goes here: await authRequest(request)
const pathname = request.nextUrl.searchParams.get('pathname');
if (!pathname) {
return NextResponse.json({ error: 'Missing pathname' }, { status: 400 });
}
const result = await get(pathname, {
access: 'private',
ifNoneMatch: request.headers.get('if-none-match') ?? undefined,
});
if (!result) {
return new NextResponse('Not found', { status: 404 });
}
// Blob hasn't changed — tell the browser to use its cached copy
if (result.statusCode === 304) {
return new NextResponse(null, {
status: 304,
headers: {
ETag: result.blob.etag,
'Cache-Control': 'private, no-cache',
},
});
}
return new NextResponse(result.stream, {
headers: {
'Content-Type': result.blob.contentType,
'X-Content-Type-Options': 'nosniff',
ETag: result.blob.etag,
'Cache-Control': 'private, no-cache',
},
});
}How it works:
- First request: The blob store returns a
200response with the content and anETag. The browser caches the response. - Subsequent requests: The browser sends
If-None-Matchwith the cached ETag. When forwarded to the blob store, it returns a304response without content, so no data is re-downloaded.
See Cache-Control headers for more details.
Learn more about Edgenivo's CDN cache.
Since private blobs are delivered through your own Functions, you can serve them from any custom domain. A common pattern is to create a dedicated Edgenivo project (e.g. "assets") for blob delivery:
- Create a new Edgenivo project with a route handler that serves private blobs (as shown in delivering private blobs)
- Connect your private Blob store to this project
- Assign a custom domain to the project (e.g.
content.mywebsite.com)
Requests to content.mywebsite.com/api/file?pathname=documents/report.pdf will then go through your auth logic and stream the blob to the user.
Private blobs cannot be indexed by search engines since all read access requires authentication.
When serving private blobs through Functions, you pay for:
- Function fetches the blob from the store: Blob Data Transfer + Fast Origin Transfer on cache miss
- Function responds to the browser: Fast Data Transfer + Fast Origin Transfer
Recommendations:
- Private Blob stores are ideal for smaller sensitive files or when you need precise auth control
- We do not recommend serving files larger than 100 MB through private Blob stores unless traffic is low
- For large public media, use public storage which benefits from BDT rates (3x cheaper than FDT)
See pricing documentation for full details.
Upload charges depend on your implementation method:
- Client Uploads: No data transfer charges for uploads
- Server Uploads: Fast Data Transfer charges apply when your Edgenivo application receives the file
See pricing documentation for full details.
Was this helpful?