diff --git a/storage/blob-starter/app/api/blob/route.ts b/storage/blob-starter/app/api/blob/route.ts
new file mode 100644
index 0000000000..2ff6437577
--- /dev/null
+++ b/storage/blob-starter/app/api/blob/route.ts
@@ -0,0 +1,30 @@
+import { type NextRequest, NextResponse } from 'next/server'
+import { get } from '@vercel/blob'
+
+// Delivery route for private blobs.
+// Private blob URLs are not directly accessible in the browser.
+// This route streams the blob content after authenticating the request.
+// See: https://vercel.com/docs/vercel-blob/private-storage
+export async function GET(request: NextRequest) {
+ // ⚠️ Add your own authentication here.
+ // For example: 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',
+ },
+ })
+}
diff --git a/storage/blob-starter/components/uploader.tsx b/storage/blob-starter/components/uploader.tsx
index f79e271f03..a0dd07bd1c 100644
--- a/storage/blob-starter/components/uploader.tsx
+++ b/storage/blob-starter/components/uploader.tsx
@@ -28,27 +28,32 @@ export default function Uploader() {
if (file) {
try {
const blob = await upload(file.name, file, {
- access: 'public',
+ access: 'private',
handleUploadUrl: '/api/upload',
onUploadProgress: (progressEvent) => {
setProgress(progressEvent.percentage)
},
})
+ // Private blob URLs are not directly accessible in the browser.
+ // Serve them through a delivery route that streams the content.
+ // See: https://vercel.com/docs/vercel-blob/private-storage
+ const deliveryUrl = `/api/blob?pathname=${encodeURIComponent(blob.pathname)}`
+
toast(
(t: { id: string }) => (
diff --git a/storage/blob-starter/package.json b/storage/blob-starter/package.json
index 3f54a9c9e8..6eaab905e9 100644
--- a/storage/blob-starter/package.json
+++ b/storage/blob-starter/package.json
@@ -15,7 +15,7 @@
"lint": "next lint"
},
"dependencies": {
- "@vercel/blob": "^1.0.0",
+ "@vercel/blob": "^2.3.0",
"next": "^16.0.10",
"react": "^19.2.1",
"react-dom": "^19.2.1",
diff --git a/storage/blob-sveltekit/package.json b/storage/blob-sveltekit/package.json
index a28c2bc154..4c916ea968 100644
--- a/storage/blob-sveltekit/package.json
+++ b/storage/blob-sveltekit/package.json
@@ -26,7 +26,7 @@
"vite": "^5.4.4"
},
"dependencies": {
- "@vercel/blob": "^1.0.0",
+ "@vercel/blob": "^2.3.0",
"dotenv-expand": "^10.0.0"
}
}
diff --git a/storage/blob-sveltekit/src/routes/+page.server.ts b/storage/blob-sveltekit/src/routes/+page.server.ts
index e1ceb69a3d..a40d2f4b6f 100644
--- a/storage/blob-sveltekit/src/routes/+page.server.ts
+++ b/storage/blob-sveltekit/src/routes/+page.server.ts
@@ -11,11 +11,13 @@ export const actions = {
error(400, { message: 'No file to upload.' })
}
- const { url } = await put(file.name, file, {
- access: 'public',
+ const { pathname } = await put(file.name, file, {
+ access: 'private',
addRandomSuffix: true,
token: env.BLOB_READ_WRITE_TOKEN,
})
- return { uploaded: url }
+ // Private blob URLs are not directly accessible in the browser.
+ // Return a delivery route URL that streams the content.
+ return { uploaded: `/api/blob?pathname=${encodeURIComponent(pathname)}` }
},
}
diff --git a/storage/blob-sveltekit/src/routes/api/blob/+server.ts b/storage/blob-sveltekit/src/routes/api/blob/+server.ts
new file mode 100644
index 0000000000..766e2b3ff2
--- /dev/null
+++ b/storage/blob-sveltekit/src/routes/api/blob/+server.ts
@@ -0,0 +1,33 @@
+import { error } from '@sveltejs/kit'
+import { get } from '@vercel/blob'
+import { env } from '$env/dynamic/private'
+
+// Delivery route for private blobs.
+// Private blob URLs are not directly accessible in the browser.
+// This route streams the blob content after authenticating the request.
+// See: https://vercel.com/docs/vercel-blob/private-storage
+export async function GET({ url }) {
+ // ⚠️ Add your own authentication here.
+
+ const pathname = url.searchParams.get('pathname')
+
+ if (!pathname) {
+ error(400, { message: 'Missing pathname' })
+ }
+
+ const result = await get(pathname, {
+ access: 'private',
+ token: env.BLOB_READ_WRITE_TOKEN,
+ })
+
+ if (result?.statusCode !== 200) {
+ error(404, { message: 'Not found' })
+ }
+
+ return new Response(result.stream, {
+ headers: {
+ 'Content-Type': result.blob.contentType,
+ 'X-Content-Type-Options': 'nosniff',
+ },
+ })
+}