Cloudinary 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
- 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 Cloudinary account and get credentials
-
Sign up for a free or paid account at Cloudinary.com.
-
Once logged in, navigate to your Account settings.
-
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.
-
Create a table in Neon for file metadata
We need a table in Neon to store metadata about the assets uploaded to Cloudinary.
-
Connect to your Neon database using the Neon SQL Editor or a client like psql. Create a table to store relevant details:
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 (e.g., 'image', 'video', 'raw') user_id TEXT NOT NULL, -- User associated with the file upload_timestamp TIMESTAMPTZ DEFAULT NOW() );
-
Run the SQL statement. You can customize this table by adding other useful columns returned by Cloudinary (e.g.,
version
,format
,width
,height
,tags
).
Securing metadata with RLS
If you use Neon's Row Level Security (RLS), 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 (e.g., 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:
/generate-signature
: Generates a signature, timestamp, and provides the API key for the client upload./save-metadata
: Receives asset metadata from the client after a successful Cloudinary upload and saves it to the Neon database.
We'll use Hono for the server, the official
cloudinary
Node.js SDK for signature generation, and@neondatabase/serverless
for Neon.First, install the necessary dependencies:
npm install cloudinary @neondatabase/serverless @hono/node-server hono dotenv
Create a
.env
file with your credentials:# 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:
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
- Setup: Initializes the Neon client (
sql
), Hono (app
), and configures the Cloudinary Node.js SDK using environment variables. - Authentication: Includes a placeholder
authMiddleware
. Replace this with your actual user authentication logic. - API endpoints:
/generate-signature
(GET): Creates a currenttimestamp
. Usescloudinary.utils.api_sign_request
with the parameters to sign (at minimum, the timestamp) and yourAPI Secret
to generate asignature
. It returns thesignature
,timestamp
, and yourAPI 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 theuserId
, into thecloudinary_files
table in Neon.
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.
-
Get signature and parameters: Send a
GET
request to your backend's/generate-signature
endpoint.curl -X GET http://localhost:3000/generate-signature
Expected response: A JSON object with the
signature
,timestamp
, andapi_key
.{ "success": true, "signature": "a1b2c3d4e5f6...", "timestamp": 1713999600, "api_key": "YOUR_CLOUDINARY_API_KEY" }
-
Upload file directly to Cloudinary: Use the obtained
signature
,timestamp
,api_key
, and the file path to send aPOST
request withmultipart/form-data
directly to the Cloudinary Upload API. The URL includes your Cloud Name.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.
{ "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
, andresource_type
in the response. These are needed for the next step. -
Save metadata: Send a
POST
request to your backend's/save-metadata
endpoint with the key details received from Cloudinary in Step 2.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):
{ "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:
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.
This pattern separates media storage, processing, and delivery (handled by Cloudinary) from structured metadata management (handled by Neon).
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.