Our Azure Native Integration is GA — If you're running on Azure, get unified billing, SSO, MACC eligibility, and more
Docs/Getting Started/File storage with Backblaze B2

File storage with Backblaze B2

Store files via Backblaze B2 and track metadata in Neon

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

  1. Create a Neon project

    1. Navigate to pg.new to create a new Neon project.
    2. Copy the connection string by clicking the Connect button on your Project Dashboard. For more information, see Connect from any application.
  2. Create a Backblaze account and B2 bucket

    1. Sign up for or log in to your Backblaze account.
    2. Navigate to B2 Cloud Storage > Buckets in the left sidebar.
    3. 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 B2 Bucket
    4. 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. Create B2 Application Key
    5. 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. B2 Bucket Endpoint
  3. 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: Example CORS configuration

    In a production environment, replace http://localhost:3000 with your actual domain

  4. Create a table in Neon for file metadata

    We need a table in Neon to store metadata about the objects uploaded to B2.

    1. 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 the object_key and generate presigned download URLs on demand.

    2. 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.

  5. 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:

    1. /presign-b2-upload: Generates the temporary presigned URL.
    2. /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

    1. 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.
    2. Authentication: A placeholder authMiddleware is included. Replace this with real authentication logic. It currently just sets a static userId for demonstration.
    3. 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 generated objectKey, and the standard S3 public URL.
      • /save-b2-metadata: Called by the client after successful upload. Saves the objectKey, file_url, and userId into the b2_files table in Neon using @neondatabase/serverless.
  6. Testing the upload workflow

    Testing the presigned URL flow involves multiple steps:

    1. Get presigned URL: Send a POST request to your /presign-b2-upload endpoint with a JSON body containing fileName and contentType. 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, and publicFileUrl:

      {
        "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, and publicFileUrl from the response. You will use these in the next steps

    2. Upload file to B2: Use the received presignedUrl to upload the actual file using an HTTP PUT request. The Content-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 HTTP 200 OK.

    3. Save metadata: Send a POST request to your /save-b2-metadata endpoint with the objectKey and optionally publicFileUrl 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.
  7. 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 using GetObjectCommand (JS) or generate_presigned_url('get_object', ...) (Python) with read permissions. This provides secure, temporary read access.

    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

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.

Last updated on

Was this page helpful?