Hey everyone,
I see a lot of questions pop up here about how to manage credentials for multiple clients without either A) paying for the Enterprise tier to isolate user environments, or B) cloning the same workflow 50 times and creating a DevOps nightmare.
I recently hit this exact wall. I was trying to build a automation setup, but n8n’s native service nodes (like Google Sheets, Gmail, OpenAI) kept forcing me to select a static credential at design time.
After a lot of trial and error, I ended up building a completely “headless” architecture to bypass this limitation. I wanted to share the exact setup here in case anyone else is stuck in the workflow-cloning trap.
The Solution: Headless n8n + Dynamic HTTP Nodes
Instead of forcing multi-tenancy inside n8n, I moved all the user management to a custom frontend and external database.
The workflow starts with the Client Dashboard: Clients log into a custom web app and input their API keys or complete an OAuth flow (like Google SSO). My app stores their access_token and refresh_token securely in an external database mapped to their Client ID.
When an action is triggered, the frontend fires a webhook to n8n, passing both the execution instructions and the specific client’s authentication tokens directly in the JSON payload.
Bypassing Native Nodes
To make this work, you have to completely ditch native platform nodes (like the “OpenAI” or “Google Sheets” nodes) because they lock you into static credentials. Instead, you use standard HTTP Request nodes.
Here is how you handle the credentials dynamically using **The Golden Rule of Absolute Referencing:**If you just use standard n8n $json expressions, your workflow will break if you add formatting nodes or timers between the webhook and the HTTP node. To make it bulletproof, you must use absolute referencing to point directly back to the Webhook trigger.
**Example 1: OpenAI (Bearer Token)**Instead of the native node, you use an HTTP Request pointing to https://api.openai.com/v1/chat/completions. Under the Headers section, you set:
-
Name:
Authorization -
Value:
=Bearer {{ $('Webhook').item.json.body.openAiKey }}(This pulls the exact client’s key sent from your frontend webhook).
**Example 2: Google Sheets (OAuth Token & Custom Body)**Google is slightly trickier because it requires an OAuth token and specific body formatting.
-
URL: https://sheets.googleapis.com/v4/spreadsheets/{{
$('Webhook').item.json.body.spreadsheetId }}/values/Sheet1!A1:append?valueInputOption=USER_ENTERED -
Headers: Pass
Authorization: Bearer {{ $('Webhook').item.json.body.googleToken }}andContent-Type: application/json. -
Body: Send the data payload dynamically (e.g., pulling lead data from an earlier step into Google’s required “array of arrays” format).
Why this matters
By decoupling the credential data from n8n’s vault, I now have one single master workflow blueprint per task.
Whether I have 5 clients or 5,000, they all trigger the exact same n8n canvas. The HTTP nodes dynamically adapt and execute using whatever credentials the webhook just handed them. If a bug needs fixing, I update one master workflow, and every single client instantly benefits. It keeps infrastructure costs totally flat.
Since this took a bit of head-scratching to map out initially, I put together a full YouTube video showing exactly how I built it. It covers the database schema and walks through setting up the dynamic HTTP modules for Google Sheets and OpenRouter as real-world examples.
If you are trying to build your own SaaS wrapper around n8n, you can check out my video walkthrough here: https://www.youtube.com/watch?v=u1MJ06N46uQ
Update: I just published a companion blog post as well. If you want to grab the exact expressions and JSON examples to copy-paste into your own setup: Dynamic Credential Management in n8n: One Workflow, Many Clients | Five