> This page location: Tools & Workflows > Integrations (3rd party) > File & media storage > Cloudinary
> Full Neon documentation index: https://neon.com/docs/llms.txt

# Media storage with Cloudinary

Store files via Cloudinary and track metadata in Neon

[Cloudinary](https://cloudinary.com/) is a cloud-based platform for image and video management, offering upload, storage, real-time manipulation, optimization, and delivery via CDN.

This guide demonstrates how to integrate Cloudinary with Neon. You'll learn how to securely upload files directly from the client-side to Cloudinary using signatures generated by your backend, and then store the resulting asset metadata (like the Cloudinary Public ID and secure URL) in your Neon database.

## Setup steps

## Create a Neon project

1. Navigate to the [Neon Console](https://console.neon.tech) 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](https://neon.com/docs/connect/connect-from-any-app).

## Create a Cloudinary account and get credentials

1. Sign up for a free or paid account at [Cloudinary.com](https://cloudinary.com/users/register/free).
2. Once logged in, navigate to your **Account settings**.
3. Find your **Product Environment Credentials** which include:

   - **Cloud Name**
   - **API Key**
   - **API Secret**

   Create a new **API Key** if you do not have one. This key is used to authenticate your application with Cloudinary.
   ![Cloudinary API Key](https://neon.com/docs/guides/cloudinary-api-key.png)

## Create a table in Neon for file metadata

We need a table in Neon to store metadata about the assets uploaded to Cloudinary.

1. Connect to your Neon database using the [Neon SQL Editor](https://neon.com/docs/get-started/query-with-neon-sql-editor) or a client like [psql](https://neon.com/docs/connect/query-with-psql-editor). Create a table to store relevant details:

   ```sql
   CREATE TABLE IF NOT EXISTS cloudinary_files (
       id SERIAL PRIMARY KEY,
       public_id TEXT NOT NULL UNIQUE,  -- Cloudinary's unique identifier for the asset
       media_url TEXT NOT NULL,         -- Media URL for the asset on Cloudinary's CDN
       resource_type TEXT NOT NULL,     -- Type of asset (for example, 'image', 'video', 'raw')
       user_id TEXT NOT NULL,           -- User associated with the file
       upload_timestamp TIMESTAMPTZ DEFAULT NOW()
   );
   ```

2. Run the SQL statement. You can customize this table by adding other useful columns returned by Cloudinary (for example, `version`, `format`, `width`, `height`, `tags`).

**Note: Securing metadata with RLS**

If you use [Neon's Row Level Security (RLS)](https://neon.com/blog/introducing-neon-authorize), apply appropriate policies to the `cloudinary_files` table to control access to the metadata stored in Neon based on your rules.

Note that these policies apply _only_ to the metadata in Neon. Access control for the assets themselves is managed within Cloudinary (for example, via asset types, delivery types). By default, uploaded assets are typically accessible via their CDN URL.

## Upload files to Cloudinary and store metadata in Neon

The recommended secure approach for client-side uploads to Cloudinary involves **signed uploads**. Your backend generates a unique signature using your API Secret and specific upload parameters (like timestamp). The client uses this signature, along with your API Key and the parameters, to authenticate the direct upload request to Cloudinary. After a successful upload, the client sends the returned asset metadata back to your backend to save in Neon.

This requires two backend endpoints:

1. `/generate-signature`: Generates a signature, timestamp, and provides the API key for the client upload.
2. `/save-metadata`: Receives asset metadata from the client after a successful Cloudinary upload and saves it to the Neon database.

**JavaScript**

We'll use [Hono](https://hono.dev/) for the server, the official [`cloudinary`](https://www.npmjs.com/package/cloudinary) Node.js SDK for signature generation, and [`@neondatabase/serverless`](https://www.npmjs.com/package/@neondatabase/serverless) for Neon.

First, install the necessary dependencies:

```bash
npm install cloudinary @neondatabase/serverless @hono/node-server hono dotenv
```

Create a `.env` file with your credentials:

```env
# Cloudinary Credentials
CLOUDINARY_CLOUD_NAME=your_cloudinary_cloud_name
CLOUDINARY_API_KEY=your_cloudinary_api_key
CLOUDINARY_API_SECRET=your_cloudinary_api_secret

# Neon Connection String
DATABASE_URL=your_neon_database_connection_string
```

The following code snippet demonstrates this workflow:

```javascript
import { serve } from '@hono/node-server';
import { Hono } from 'hono';
import { v2 as cloudinary } from 'cloudinary';
import { neon } from '@neondatabase/serverless';
import 'dotenv/config';

cloudinary.config({
  cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
  api_key: process.env.CLOUDINARY_API_KEY,
  api_secret: process.env.CLOUDINARY_API_SECRET,
  secure: true,
});

const sql = neon(process.env.DATABASE_URL);
const app = new Hono();

// Replace this with your actual user authentication logic
const authMiddleware = async (c, next) => {
  // Example: Validate JWT, session, etc. and set user ID
  c.set('userId', 'user_123'); // Static ID for demonstration
  await next();
};

// 1. Generate signature for client-side upload
app.get('/generate-signature', authMiddleware, (c) => {
  try {
    const timestamp = Math.round(new Date().getTime() / 1000);
    const paramsToSign = { timestamp: timestamp };

    const signature = cloudinary.utils.api_sign_request(
      paramsToSign,
      process.env.CLOUDINARY_API_SECRET
    );

    return c.json({
      success: true,
      signature: signature,
      timestamp: timestamp,
      api_key: process.env.CLOUDINARY_API_KEY,
    });
  } catch (error) {
    console.error('Signature Generation Error:', error);
    return c.json({ success: false, error: 'Failed to generate signature' }, 500);
  }
});

// 2. Save metadata after client confirms successful upload to Cloudinary
app.post('/save-metadata', authMiddleware, async (c) => {
  try {
    const userId = c.get('userId');
    // Client sends metadata received from Cloudinary after upload
    const { public_id, secure_url, resource_type } = await c.req.json();

    if (!public_id || !secure_url || !resource_type) {
      throw new Error('public_id, secure_url, and resource_type are required');
    }

    // Insert metadata into Neon database
    await sql`
      INSERT INTO cloudinary_files (public_id, media_url, resource_type, user_id)
      VALUES (${public_id}, ${secure_url}, ${resource_type}, ${userId})
    `;

    console.log(`Metadata saved for Cloudinary asset: ${public_id}`);
    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 the Neon client (`sql`), Hono (`app`), and configures the Cloudinary Node.js SDK using environment variables.
2. **Authentication:** Includes a placeholder `authMiddleware`. **Replace this with your actual user authentication logic.**
3. **API endpoints:**
   - **`/generate-signature` (GET):** Creates a current `timestamp`. Uses `cloudinary.utils.api_sign_request` with the parameters to sign (at minimum, the timestamp) and your `API Secret` to generate a `signature`. It returns the `signature`, `timestamp`, and your `API Key` to the client. These are needed for the client's direct upload request to Cloudinary.
   - **`/save-metadata` (POST):** Called by the client _after_ a successful direct upload to Cloudinary. The client sends the relevant asset metadata received from Cloudinary (`public_id`, `secure_url`, `resource_type`). The endpoint saves this information, along with the `userId`, into the `cloudinary_files` table in Neon.

**Python**

We'll use [Flask](https://flask.palletsprojects.com/en/stable/), the official [`cloudinary`](https://pypi.org/project/cloudinary/) Python SDK, and [`psycopg2`](https://pypi.org/project/psycopg2/).

First, install the necessary dependencies:

```bash
pip install Flask cloudinary psycopg2-binary python-dotenv
```

Create a `.env` file with your credentials:

```env
# Cloudinary Credentials
CLOUDINARY_CLOUD_NAME=your_cloud_name
CLOUDINARY_API_KEY=your_api_key
CLOUDINARY_API_SECRET=your_api_secret

# Neon Connection String
DATABASE_URL=your_neon_database_connection_string
```

The following code snippet demonstrates this workflow:

```python
import os
import time

import cloudinary
import cloudinary.utils
import psycopg2
from dotenv import load_dotenv
from flask import Flask, jsonify, request

load_dotenv()

cloudinary.config(
    cloud_name=os.getenv("CLOUDINARY_CLOUD_NAME"),
    api_key=os.getenv("CLOUDINARY_API_KEY"),
    api_secret=os.getenv("CLOUDINARY_API_SECRET"),
    secure=True,
)

app = Flask(__name__)


# Use a global PostgreSQL connection pool in production instead of connecting per request
def get_db_connection():
    return psycopg2.connect(os.getenv("DATABASE_URL"))


# Replace this with your actual user authentication logic
def get_authenticated_user_id(request):
    # Example: Validate Authorization header, session cookie, etc.
    return "user_123"  # Static ID for demonstration


# 1. Generate signature for client-side upload
@app.route("/generate-signature", methods=["GET"])
def generate_signature_route():
    try:
        user_id = get_authenticated_user_id(request)
        if not user_id:
            return jsonify({"success": False, "error": "Unauthorized"}), 401

        timestamp = int(time.time())
        params_to_sign = {"timestamp": timestamp}

        signature = cloudinary.utils.api_sign_request(
            params_to_sign, os.getenv("CLOUDINARY_API_SECRET")
        )

        return (
            jsonify(
                {
                    "success": True,
                    "signature": signature,
                    "timestamp": timestamp,
                    "api_key": os.getenv("CLOUDINARY_API_KEY"),
                }
            ),
            200,
        )

    except Exception as e:
        print(f"Signature Generation Error: {e}")
        return jsonify({"success": False, "error": "Failed to generate signature"}), 500


# 2. Save metadata after client confirms successful upload to Cloudinary
@app.route("/save-metadata", methods=["POST"])
def save_metadata_route():
    conn = None
    cursor = None
    try:
        user_id = get_authenticated_user_id(request)
        if not user_id:
            return jsonify({"success": False, "error": "Unauthorized"}), 401

        # Client sends metadata received from Cloudinary after upload
        data = request.get_json()
        public_id = data.get("public_id")
        secure_url = data.get("secure_url")
        resource_type = data.get("resource_type")

        if not public_id or not secure_url or not resource_type:
            raise ValueError("public_id, secure_url, and resource_type are required")

        # Insert metadata into Neon database
        conn = get_db_connection()
        cursor = conn.cursor()
        cursor.execute(
            """
            INSERT INTO cloudinary_files (public_id, media_url, resource_type, user_id)
            VALUES (%s, %s, %s, %s)
            """,
            (public_id, secure_url, resource_type, user_id),
        )
        conn.commit()
        print(f"Metadata saved for Cloudinary asset: {public_id}")
        return jsonify({"success": True}), 201

    except (psycopg2.Error, ValueError) as e:
        print(f"Metadata Save Error: {e}")
        return (
            jsonify({"success": False, "error": "Failed to save metadata"}),
            500,
        )
    except Exception as e:
        print(f"Unexpected Metadata Save Error: {e}")
        return jsonify({"success": False, "error": "Server error"}), 500
    finally:
        if cursor:
            cursor.close()
        if conn:
            conn.close()


if __name__ == "__main__":
    app.run(port=3000, debug=True)
```

**Explanation**

1. **Setup:** Initializes Flask (`app`), the database connection function, and configures the Cloudinary Python SDK using environment variables.
2. **Authentication:** Includes a placeholder `get_authenticated_user_id` function. **Replace this with your actual user authentication logic.**
3. **API endpoints:**
   - **`/generate-signature` (GET):** Gets the current `timestamp`. Uses `cloudinary.utils.api_sign_request` with the parameters to sign and your `API Secret` to generate the `signature`. Returns the `signature`, `timestamp`, and `API Key` to the client.
   - **`/save-metadata` (POST):** Called by the client _after_ a successful direct upload to Cloudinary. It receives asset metadata from the client, validates required fields (`public_id`, `secure_url`, `resource_type`), and saves this along with the `userId` into the `cloudinary_files` table using `psycopg2`.
4. **Database Connection:** The example shows creating a new connection per request. In production, use a global connection pool for better performance.

## Testing the upload workflow

This workflow involves getting a signature from your backend, using it to upload directly to Cloudinary, and then notifying your backend.

1. **Get signature and parameters:** Send a `GET` request to your backend's `/generate-signature` endpoint.

   ```bash
   curl -X GET http://localhost:3000/generate-signature
   ```

   **Expected response:** A JSON object with the `signature`, `timestamp`, and `api_key`.

   ```json
   {
     "success": true,
     "signature": "a1b2c3d4e5f6...",
     "timestamp": 1713999600,
     "api_key": "YOUR_CLOUDINARY_API_KEY"
   }
   ```

2. **Upload file directly to Cloudinary:** Use the obtained `signature`, `timestamp`, `api_key`, and the file path to send a `POST` request with `multipart/form-data` directly to the Cloudinary Upload API. The URL includes your **Cloud Name**.

   ```bash
   curl -X POST https://api.cloudinary.com/v1_1/<YOUR_CLOUD_NAME>/image/upload \
        -F "file=@/path/to/your/test-image.jpg" \
        -F "api_key=<API_KEY_FROM_STEP_1>" \
        -F "timestamp=<TIMESTAMP_FROM_STEP_1>" \
        -F "signature=<SIGNATURE_FROM_STEP_1>"
   ```

   > If uploading a video, change the endpoint in the URL from `/image/upload` to `/video/upload`.

   **Expected response (from Cloudinary):** A successful upload returns a JSON object with metadata about the uploaded asset.

   ```json
   {
     "asset_id": "...",
     "public_id": "<PUBLIC_ID>",
     "version": 1713999601,
     "version_id": "...",
     "signature": "...",
     "width": 800,
     "height": 600,
     "format": "jpg",
     "resource_type": "image",
     "created_at": "2025-04-24T05:37:06Z",
     "tags": [],
     "bytes": 123456,
     "type": "upload",
     "etag": "...",
     "placeholder": false,
     "url": "http://res.cloudinary.com/<YOUR_CLOUD_NAME>/image/upload/v1713999601/sample_image_123.jpg",
     "secure_url": "https://res.cloudinary.com/<YOUR_CLOUD_NAME>/image/upload/v1713999601/sample_image_123.jpg",
     "folder": "",
     "original_filename": "test-image",
     "api_key": "YOUR_CLOUDINARY_API_KEY"
   }
   ```

   > Note the `public_id`, `secure_url`, and `resource_type` in the response. These are needed for the next step.

3. **Save metadata:** Send a `POST` request to your backend's `/save-metadata` endpoint with the key details received from Cloudinary in Step 2.

   ```bash
   curl -X POST http://localhost:3000/save-metadata \
        -H "Content-Type: application/json" \
        -d '{
             "public_id": "<PUBLIC_ID_FROM_STEP_2>",
             "secure_url": "<SECURE_URL_FROM_STEP_2>",
             "resource_type": "<RESOURCE_TYPE_FROM_STEP_2>"
           }'
   ```

   **Expected response (from your backend):**

   ```json
   { "success": true }
   ```

**Expected outcome:**

- The file is successfully uploaded to your Cloudinary account (visible in the Media Library).
- A new row corresponding to the uploaded asset exists in your `cloudinary_files` table in Neon.

## Accessing file metadata and files

With metadata stored in Neon, your application can retrieve references to the media hosted on Cloudinary.

Query the `cloudinary_files` table from your application's backend whenever you need to display or link to uploaded files.

**Example SQL query:**

Retrieve media files associated with a specific user:

```sql
SELECT
    id,
    public_id,      -- Cloudinary Public ID
    media_url,      -- HTTPS URL for the asset
    resource_type,
    user_id,
    upload_timestamp
FROM
    cloudinary_files
WHERE
    user_id = 'user_123' AND resource_type = 'image'; -- Use actual user ID & desired type
```

**Using the data:**

- The query returns metadata stored in Neon.
- The `media_url` is the direct CDN link to the asset.
- **Cloudinary transformations:** Cloudinary excels at on-the-fly transformations. You can manipulate the asset by modifying the `media_url`. Parameters are inserted between the `/upload/` part and the version/public_id part of the URL. For example, to get a 300px wide, cropped version: `https://res.cloudinary.com/<CLOUD_NAME>/image/upload/w_300,c_fill/v<VERSION>/<PUBLIC_ID>.<FORMAT>`. Explore the extensive [Cloudinary transformation documentation](https://cloudinary.com/documentation/image_transformations).

This pattern separates media storage, processing, and delivery (handled by Cloudinary) from structured metadata management (handled by Neon).

## Resources

- [Cloudinary documentation](https://cloudinary.com/documentation)
- [Cloudinary Upload API reference](https://cloudinary.com/documentation/image_upload_api_reference)
- [Neon Documentation](https://neon.com/docs/introduction)

---

## Related docs (File & media storage)

- [File storage](https://neon.com/docs/guides/file-storage)
- [AWS S3](https://neon.com/docs/guides/aws-s3)
- [Azure Blob Storage](https://neon.com/docs/guides/azure-blob-storage)
- [Backblaze B2](https://neon.com/docs/guides/backblaze-b2)
- [Cloudflare R2](https://neon.com/docs/guides/cloudflare-r2)
- [ImageKit](https://neon.com/docs/guides/imagekit)
- [Uploadcare](https://neon.com/docs/guides/uploadcare)
