Backblaze B2 Cloud Storage is an S3-compatible object storage service known for its affordability and ease of use. It's suitable for storing large amounts of unstructured data like backups, archives, images, videos, and application assets.
This guide demonstrates how to integrate Backblaze B2 with Neon by storing file metadata (like the file id, name and URL) in your Neon database, while using B2 for file storage.
Prerequisites
Create a Neon project
- Navigate to pg.new to create a new Neon project.
- Copy the connection string by clicking the Connect button on your Project Dashboard. For more information, see Connect from any application.
Create a Backblaze account and B2 bucket
- Sign up for or log in to your Backblaze account.
- Navigate to B2 Cloud Storage > Buckets in the left sidebar.
- Click Create a Bucket. Provide a globally unique bucket name (e.g.,
my-neon-app-b2-files
), choose whether files should be Private or Public. For this guide, we'll use Public for simplicity, but Private is recommended for production applications where you want to control access to files. - Create application key:
- Navigate to B2 Cloud Storage > Application Keys in the left sidebar.
- Click + Add a New Application Key.
- Give the key a name (e.g.,
neon-app-b2-key
). - Crucially, restrict the key's access: Select Allow access to Bucket(s) and choose the bucket you just created (e.g.,
my-neon-app-b2-files
). - Select Read and Write for the Type of Access.
- Leave other fields blank unless needed (e.g., File name prefix).
- Click Create New Key.
- Copy the Key ID and Application Key. These will be used in your application to authenticate with B2.
- Find S3 endpoint:
- Navigate back to B2 Cloud Storage > Buckets.
- Find your bucket and note the Endpoint URL listed (e.g.,
s3.us-west-000.backblazeb2.com
). You'll need this S3-compatible endpoint for the SDK configuration.
Configure CORS for client-side uploads
If your application involves uploading files directly from a web browser using the generated presigned URLs, you must configure Cross-Origin Resource Sharing (CORS) rules for your B2 bucket. CORS rules tell B2 which web domains are allowed to make requests (like
PUT
requests for uploads) to your bucket. Without proper CORS rules, browser security restrictions will block these direct uploads.Follow Backblaze's guide to Cross-Origin Resource Sharing Rules. You configure CORS rules in the B2 Bucket Settings page in the Backblaze web UI.
Here’s an example CORS configuration allowing
http://localhost:3000
to view and upload files:In a production environment, replace
http://localhost:3000
with your actual domainCreate a table in Neon for file metadata
We need a table in Neon to store metadata about the objects uploaded to B2.
-
Connect to your Neon database using the Neon SQL Editor or a client like psql. Create a table including the B2 file name (object key), file URL, user ID, and timestamp:
CREATE TABLE IF NOT EXISTS b2_files ( id SERIAL PRIMARY KEY, object_key TEXT NOT NULL UNIQUE, -- Key (path/filename) in B2 file_url TEXT, -- Base public URL user_id TEXT NOT NULL, -- User associated with the file upload_timestamp TIMESTAMPTZ DEFAULT NOW() );
Storing the full public
file_url
is only useful if the bucket is public. For private buckets, you'll typically only store theobject_key
and generate presigned download URLs on demand. -
Run the SQL statement. Add other relevant columns as needed (e.g.,
content_type
,size
if needed).
Securing metadata with RLS
If you use Neon's Row Level Security (RLS), remember to apply appropriate access policies to the
b2_files
table. This controls who can view or modify the object references stored in Neon based on your RLS rules.Note that these policies apply only to the metadata in Neon. Access control for the objects within the B2 bucket itself is managed via B2 bucket settings (public/private), Application Key permissions, and presigned URL settings.
-
Upload files to B2 and store metadata in Neon
Leveraging B2's S3 compatibility, the recommended pattern for client-side uploads involves presigned upload URLs. Your backend generates a temporary URL that the client uses to upload the file directly to B2. Afterwards, your backend saves the file's metadata to Neon.
This requires two backend endpoints:
/presign-b2-upload
: Generates the temporary presigned URL./save-b2-metadata
: Records the metadata in Neon after the client confirms successful upload.
We'll use Hono for the server,
@aws-sdk/client-s3
and@aws-sdk/s3-request-presigner
for B2 interaction (due to S3 compatibility), and@neondatabase/serverless
for Neon.First, install the necessary dependencies:
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner @neondatabase/serverless @hono/node-server hono dotenv
Create a
.env
file:# Backblaze B2 Credentials & Config B2_APPLICATION_KEY_ID=your_b2_key_id B2_APPLICATION_KEY=your_b2_application_key B2_BUCKET_NAME=your_b2_bucket_name B2_ENDPOINT_URL=https://your_b2_s3_endpoint # Neon Connection String DATABASE_URL=your_neon_database_connection_string
The following code snippet demonstrates this workflow:
import { serve } from '@hono/node-server'; import { Hono } from 'hono'; import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { neon } from '@neondatabase/serverless'; import 'dotenv/config'; import { randomUUID } from 'crypto'; const B2_BUCKET = process.env.B2_BUCKET_NAME; const B2_ENDPOINT = process.env.B2_ENDPOINT_URL; const endpointUrl = new URL(B2_ENDPOINT); const region = endpointUrl.hostname.split('.')[1]; const s3 = new S3Client({ endpoint: B2_ENDPOINT, region: region, credentials: { accessKeyId: process.env.B2_APPLICATION_KEY_ID, secretAccessKey: process.env.B2_APPLICATION_KEY, }, }); const sql = neon(process.env.DATABASE_URL); const app = new Hono(); // Replace this with your actual user authentication logic, by validating JWTs/Headers, etc. const authMiddleware = async (c, next) => { c.set('userId', 'user_123'); await next(); }; // 1. Generate presigned URL for upload app.post('/presign-b2-upload', authMiddleware, async (c) => { try { const { fileName, contentType } = await c.req.json(); if (!fileName || !contentType) throw new Error('fileName and contentType required'); const objectKey = `${randomUUID()}-${fileName}`; const publicFileUrl = `${B2_ENDPOINT}/${B2_BUCKET}/${objectKey}`; const command = new PutObjectCommand({ Bucket: B2_BUCKET, Key: objectKey, ContentType: contentType, }); const presignedUrl = await getSignedUrl(s3, command, { expiresIn: 300 }); // 5 min expiry return c.json({ success: true, presignedUrl, objectKey, publicFileUrl }); } catch (error) { console.error('Presign Error:', error.message); return c.json({ success: false, error: 'Failed to prepare upload' }, 500); } }); // 2. Save metadata after client upload confirmation app.post('/save-b2-metadata', authMiddleware, async (c) => { try { const { objectKey, publicFileUrl } = await c.req.json(); const userId = c.get('userId'); if (!objectKey) throw new Error('objectKey required'); await sql` INSERT INTO b2_files (object_key, file_url, user_id) VALUES (${objectKey}, ${publicFileUrl}, ${userId}) `; console.log(`Metadata saved for B2 object: ${objectKey}`); return c.json({ success: true }); } catch (error) { console.error('Metadata Save Error:', error.message); return c.json({ success: false, error: 'Failed to save metadata' }, 500); } }); const port = 3000; serve({ fetch: app.fetch, port }, (info) => { console.log(`Server running at http://localhost:${info.port}`); });
Explanation
- Setup: Initializes Neon (
sql
), Hono (app
), and the AWS S3 client (s3
) configured with the B2 endpoint, region (extracted from endpoint), and B2 Application Key credentials. - Authentication: A placeholder
authMiddleware
is included. Replace this with real authentication logic. It currently just sets a staticuserId
for demonstration. - Upload endpoints:
/presign-b2-upload
: Generates a temporary secure URL (presignedUrl
) using@aws-sdk/s3-request-presigner
that allows uploading a file directly to B2. It returns the URL, the generatedobjectKey
, and the standard S3 public URL./save-b2-metadata
: Called by the client after successful upload. Saves theobjectKey
,file_url
, anduserId
into theb2_files
table in Neon using@neondatabase/serverless
.
Testing the upload workflow
Testing the presigned URL flow involves multiple steps:
-
Get presigned URL: Send a
POST
request to your/presign-b2-upload
endpoint with a JSON body containingfileName
andcontentType
. Using cURL:curl -X POST http://localhost:3000/presign-b2-upload \ -H "Content-Type: application/json" \ -d '{"fileName": "test-b2.png", "contentType": "image/png"}'
You should receive a JSON response with a
presignedUrl
,objectKey
, andpublicFileUrl
:{ "success": true, "presignedUrl": "https://s3.<REGION>.backblazeb2.com/<BUCKET>/<OBJECT_KEY>?...", "objectKey": "<OBJECT_KEY>", "publicFileUrl": "https://s3.<REGION>.backblazeb2.com/<BUCKET>/<OBJECT_KEY>" }
Note the
presignedUrl
,objectKey
, andpublicFileUrl
from the response. You will use these in the next steps -
Upload file to B2: Use the received
presignedUrl
to upload the actual file using an HTTPPUT
request. TheContent-Type
header must match the one used to generate the URL. Using cURL:curl -X PUT "<PRESIGNED_URL>" \ --upload-file /path/to/your/test-b2.png \ -H "Content-Type: image/png"
Replace
<PRESIGNED_URL>
with the actual URL from step 1. A successful upload typically returns HTTP200 OK
. -
Save metadata: Send a
POST
request to your/save-b2-metadata
endpoint with theobjectKey
and optionallypublicFileUrl
from step 1. Using cURL:curl -X POST http://localhost:3000/save-b2-metadata \ -H "Content-Type: application/json" \ -d '{"objectKey": "<OBJECT_KEY>", "publicFileUrl": "<PUBLIC_URL>"}'
You should receive a JSON response indicating success:
{ "success": true }
Expected outcome:
- The file appears in your B2 bucket (check the Backblaze B2 web UI).
- A new row appears in your
b2_files
table in Neon.
-
Accessing file metadata and files
Storing metadata in Neon allows your application to easily retrieve references to the files hosted on B2.
Query the
b2_files
table from your application's backend when needed.Example SQL query:
Retrieve files for user 'user_123':
SELECT id, object_key, -- Key (path/filename) in B2 file_url, -- Base public URL (only useful if bucket is Public) user_id, -- User associated with the file upload_timestamp FROM b2_files WHERE user_id = 'user_123'; -- Use actual authenticated user ID
Using the data:
-
The query returns metadata stored in Neon.
-
Accessing the file:
- If your bucket is Public, you can use the
file_url
directly in your application (e.g.,<img>
tags, download links). - If your bucket is Private, the stored
file_url
is likely irrelevant. You must generate a presigned download URL (a GET URL) on demand using your backend. This involves a similar process to generating the upload URL but usingGetObjectCommand
(JS) orgenerate_presigned_url('get_object', ...)
(Python) with read permissions. This provides secure, temporary read access.
- If your bucket is Public, you can use the
This pattern effectively separates file storage and delivery concerns (handled by Backblaze B2) from structured metadata management (handled by Neon), leveraging the strengths of both services.
-
Resources
- Backblaze B2 Cloud Storage documentation
- Backblaze B2 S3 Compatible API
- Backblaze B2 Application Keys
- Neon documentation
- Neon RLS
Need help?
Join our Discord Server to ask questions or see what others are doing with Neon. Users on paid plans can open a support ticket from the console. For more details, see Getting Support.