Module 4: Dev, Staging, and Prod Environments in Self-Hosted

5. Credentials per environment: test vs. production

Description

By the end of this lesson you will be able to configure order-triage's credentials so that dev and staging use test accounts and sandbox keys, and only prod uses Cumbre's real credentials for the CRM and the AI model. You're going to understand the idea that makes this possible: how the same workflow references the "same" credential —by its name and its type— even though its value changes completely between environments. And you're going to know how to map credentials correctly when you import a workflow into another environment, without falling into the classic mismatched-identifier mistake.

This matters because it's where environment isolation stops being infrastructure and becomes concrete operational security. The encryption key and the separate volumes from the previous lessons were the wall; credentials are what that wall protects. If dev used the CRM's real key, every test would touch Cumbre's 400 real customers' data, and the whole effort of separating environments would be wasted. This lesson makes sure the real thing only lives where it should: in prod, and nowhere else.

Connection to the module: this lesson is the direct continuation of Module 3, lesson 3 ("Separating credentials from workflows"). There you learned a credential is a secret living outside the repository, and you told apart the reference (which travels in the JSON) from the value (which never does). Here that value multiplies by three: one reference, three values, one per environment. And it builds on lesson 4: since each environment encrypts with its own N8N_ENCRYPTION_KEY, a credential can't be copied encrypted from one environment to another —it has to be recreated— and this lesson teaches you to do it right. Lesson 6 is going to take the next step: getting even the credential's value out of n8n, into environment variables.

The flight simulator and the real cockpit

Let's start with the image that organizes the whole lesson.

A pilot doesn't learn to fly a passenger plane by flying a passenger plane full of people. They learn in a flight simulator: a cabin faithfully reproducing the real controls —the same lever, the same instruments, the same panel— but connected to a computer, not a real plane. In the simulator, the pilot can get it wrong, crash the virtual plane ten times, try risky maneuvers. The controls are identical to the real ones; what changes is what they're connected to. The simulator's lever moves a fake plane; the real cockpit's lever moves three hundred tons with passengers inside.

That's exactly the relationship between dev's credentials and prod's. The credential is the lever. In both environments, order-triage has a credential called "Cumbre CRM key," of the same type, connected to the same node, used the same way. But in dev, that lever is connected to a test CRM —a simulator, no consequences; in prod, the same lever is connected to the real CRM, with customers' real data. The workflow doesn't know the difference: it moves the lever the same way. What changes isn't the lever, it's what it's connected to. And that connection is decided by the credential's value, different per environment.

The practical consequence is the rule governing the lesson:

In dev and staging, test credentials (the simulator). In prod, real credentials (the cockpit). Cumbre's real keys only touch prod.

So, a bug in order-triage tested in dev crashes a virtual plane: it queries a toy CRM, spends an AI key with a low limit, hurts nobody. The same bug in prod would move real data. Environments exist so you learn to fly in the simulator before touching the cockpit.

Reference versus value, now multiplied by environment

Recover the distinction from Module 3, because it's the key to everything. When you open order-triage's JSON and look at the node querying the CRM, you find something like this:

{
  "name": "Query CRM",
  "type": "n8n-nodes-base.httpRequest",
  "credentials": {
    "httpHeaderAuth": {
      "id": "5",
      "name": "Cumbre CRM key"
    }
  }
}

What you see there is the reference: a pointer saying "this node uses a credential of type httpHeaderAuth, named Cumbre CRM key." There's no secret at all. The CRM's key doesn't appear, only its name and type. That reference travels in the workflow's JSON, gets versioned in the repository, and is identical across the three environments, because it's logic, not secret.

The value —the real key— is what lives inside each n8n instance, encrypted with that environment's key, and never in the repository. Here's this lesson's new idea: the reference is one, but the value is three, one per environment.

Reference (in the JSON, versioned)Value (in the instance, encrypted, secret)
devCumbre CRM key, Header Auth typeThe sandbox CRM's key
stagingCumbre CRM key, Header Auth typeThe staging CRM's key
prodCumbre CRM key, Header Auth typeThe real CRM's key

Read the table slowly, because it's the central concept. The reference column is identical across the three rows: same name, same type. That's why the same order-triage.json works across all three environments without changing a line. The value column is different in each row: each environment has its own key, pointing at its own CRM. The workflow references "the credential called Cumbre CRM key," and each environment resolves that name to its key. One name, three values.

This is what enables the dream of "one workflow, many environments." The workflow doesn't say "use the key sk-crm-live-9f2c..."; that would tie it to one environment. It says "use the credential called Cumbre CRM key," and lets each environment provide its own value. The reference is the standard outlet; the value is what's on the other side of the outlet, different in each house.

Why credentials get recreated, not copied

Here's where this lesson joins with lesson 4's encryption key, and it answers a question that naturally comes up: "if order-triage needs the credential in all three environments, can't I just export it from dev and import it into staging and prod?"

The short answer is not the way you're imagining, and the reason is the encryption key. Remember: each environment encrypts its credentials with its own N8N_ENCRYPTION_KEY, deliberately different. A credential exported from dev comes out encrypted with dev's key. If you import it into prod, whose engine uses a different key, prod can't decrypt it: to it, it's unreadable garbage. The wall we built in lesson 4 —isolating secrets between environments— is exactly what prevents copying the encrypted credential.

And even if you could copy it, you wouldn't want to: dev's credential has the sandbox value. Copying it to prod would put a toy key into production, where order-triage needs the real one. The value has to be different per environment; copying the same value to all three defeats the purpose.

That's why credentials get recreated in each environment, not copied:

  1. In dev, you create the Cumbre CRM key credential with the sandbox value.
  2. In staging, you create a credential with the same name and type, but with staging's value.
  3. In prod, you create a credential with the same name and type, with the real value.

Three separate credentials, one per instance, sharing name and type but not value. Since they share name and type, the same order-triage.json references them without a problem in all three. Since they don't share value, each environment connects to its own thing. The discipline that makes this work is a single one: the credential's name and type are identical across the three environments. If in dev it's called Cumbre CRM key and in prod you name it CRM Prod, the workflow imported into prod won't find the credential it references, and the node will be left unconnected. The name is the outlet; it has to be the same in all three houses.

(There's a legitimate case for migrating a credential in the clear between instances with the --decrypted flag you saw in Module 3; but that's for moving a specific value through a secure channel, not for "copying dev to prod," and it would also mean carrying over the wrong value. For environments, the way is to recreate.)

order-triage's credentials, per environment

order-triage uses two credentials, and both follow the pattern. Let's look at them concretely:

The CRM's key (Cumbre CRM key, Header Auth type). The HTTP Request node uses it to query the customer's data.

  • In dev: it points at a sandbox CRM —a test CRM account, or an imitation server— with made-up data. Querying there touches nothing real.
  • In prod: it points at Cumbre's real CRM, with the 400 customers' data.

The AI model's key (Cumbre LLM key). The AI Agent node uses it to classify the order.

  • In dev: a key with a low spending limit, or —even better, and it's what this stack does— a local Ollama model, which costs nothing. Remember the Starter Kit brings Ollama exactly for this; Module 5 exploits it fully for testing AI workflows at zero cost.
  • In prod: the key for whichever model provider Cumbre really uses, with the production-quality model.

Notice the advantage of having local models in dev: not only do you avoid touching real data, you avoid spending real money testing. A test of order-triage in dev that classifies a hundred made-up orders with a local Ollama model costs zero. The same test against a paid provider would cost money per order. Environment isolation, here, is also cost isolation.

staging's special case: test, but resembling the real thing

Between dev and prod sits staging, and its credentials deserve a note, because they're neither one's nor the other's. The general rule —"test in dev and staging, real only in prod"— is correct, but staging has a nuance: its test credentials should point at something as close to production as possible, without being production.

Think of it this way. In dev, the CRM credential can point at any toy CRM, or even an imitation server that returns made-up responses; there, only the workflow's logic running matters. In staging, however, you want the credential to point at a sandbox CRM from the same provider as the real one, with the same response shape, the same error codes, the same quirks. Many services offer exactly this: an account or a sandbox mode that behaves like the real one but with no real effects. That's the ideal credential for staging.

Why this distinction? Because staging's job is to catch the bugs that only show up against a system that behaves like the real one. If staging used a toy CRM too different from the real one, it would let those bugs through to prod, and lose its reason for existing. The more staging's credential resembles prod's —same provider, sandbox mode— the better it fulfills its role as the last filter.

So, order-triage's full credential scale looks like this:

  • dev: as cheap and isolated as possible —toy CRM or imitation, local Ollama model. Goal: for the logic to run.
  • staging: test, but of the same type as production —real provider's sandbox CRM, a similar-quality model. Goal: trust it's going to work in prod.
  • prod: the real thing —real CRM, production model. Goal: run for real.

You're not always going to be able to have a perfect sandbox for staging, and that's fine: it's an ideal you approach based on what the provider offers. What stays non-negotiable is the same as always —real keys only in prod; staging's nuance is how to make its test as faithful as possible without crossing that line.

Worked example: creating the credential in two environments and importing the workflow

Let's walk through the concrete flow: creating Cumbre CRM key in dev with a sandbox value, importing it into prod too with a real value, and connecting the imported workflow. Remember: you do this on your own n8n instances; the guide doesn't run anything.

Step 1 — Bring up dev and create the sandbox credential. With dev running at http://localhost:5678, go into the editor, go to the credentials section, and create a new one of Header Auth type. Name it exactly Cumbre CRM key. For the value, put the sandbox CRM's key (for example, a test-account token: Bearer sk-crm-sandbox-1a2b3c).

What to expect: n8n encrypts that value with dev's N8N_ENCRYPTION_KEY and saves it in dev's database. The credential now exists in dev, and only in dev.

Step 2 — Import order-triage into dev and connect it. Import the order-triage.json from your repository. When the HTTP Request node looks for the Cumbre CRM key credential, it's going to find it —because you just created it with that exact name— and connect on its own. What to expect: the node shows the credential connected, no warnings. If the name didn't match, the node would show the credential as missing.

Step 3 — Bring up prod and repeat with the real value. With prod running at http://localhost:5680 (another instance, another database, another encryption key), go into its editor and create a Header Auth-type credential, named exactly the same: Cumbre CRM key. But for the value, put the real CRM's key (Bearer sk-crm-live-9f2c8a1b).

What to expect: now two Cumbre CRM key credentials exist —one in dev with the sandbox value, one in prod with the real value— in two separate instances, encrypted with different keys. Neither knows about the other.

Step 4 — Import order-triage into prod and verify the connection. Import the same order-triage.json into prod. The HTTP Request node looks for Cumbre CRM key, finds it in prod —with its real value— and connects. What to expect: the same workflow, without a single change to its JSON, running in prod against the real CRM. The magic wasn't changing the workflow; it was that each environment had its own credential with the same name.

Step 5 — Confirm you didn't cross values. The mental check that closes the flow: does prod's credential have the real value, and dev's the sandbox one? Never the other way around. If by mistake you put the real value in dev, every test in dev would be touching the real CRM —the exact accident all of this exists to prevent. Check that the real thing lives only in prod.

Reading is inconvenient; writing is a disaster

There's a nuance about test credentials worth understanding, because it explains why the "sandbox in dev" rule is so strict. Not every operation against an external system is equally dangerous when you use the wrong credential.

order-triage does two kinds of things against the CRM. It can read —query a customer's data to enrich the order— and, depending on how it's built, it can write —record the order's outcome, update a status, create a record. The difference between the two, when by accident they run against the real system from dev, is huge:

  • A wrong read is inconvenient but reversible. If dev queries the real CRM by mistake, you saw real data you shouldn't have, which is a privacy problem, but you didn't change anything. The CRM stays the same.
  • A wrong write is a disaster and often irreversible. If dev writes to the real CRM —records a test order, changes a real customer's status, sends a real email— you altered production data with test garbage. Cleaning that up ranges from hard to impossible: a sent email can't be un-sent, an order recorded in the billing system already triggered processes.

That's why dev's credential shouldn't just point at a test system: ideally it should be a credential that can't even touch production. A sandbox key that only works against the test CRM is safer than a real key "I promise to use carefully," because the sandbox makes the write accident impossible, instead of leaving it up to your memory.

This is one more argument in favor of local models for the AI part in dev: an Ollama model running on your machine can't, by construction, spend real money or touch the provider's production account. Physical impossibility is a better guarantee than discipline. When you can choose between "a test credential that can't touch the real thing" and "the real credential used carefully," always choose the first. The whole module is about making accidents impossible, not just unlikely.

The mismatched-identifier mistake

There's a technical detail from Module 1 (lesson 5, "what breaks on reimport") that resurfaces here and is worth facing head-on, because it's the number-one source of "missing" credentials when importing between environments.

Remember the reference in the JSON has two parts: an id and a name.

"credentials": {
  "httpHeaderAuth": { "id": "5", "name": "Cumbre CRM key" }
}

The id —here "5"— is the identifier the credential had on the instance where the workflow was exported (say, dev). But when you create the Cumbre CRM key credential in prod, prod assigns it its own id —maybe "2", maybe "11"— because identifiers are local to each instance. So the JSON's id ("5", from dev) doesn't match the credential's id in prod.

So what happens on import? It depends on the n8n version, and it's worth knowing and checking:

  • In the best case, n8n reconciles by name: it sees the id doesn't exist in prod but there's a credential with the name Cumbre CRM key, and connects it. That's why the identical-name discipline matters so much: it's what saves the reconnection.
  • In other cases, the node shows up with the credential marked as missing, and you have to open it and manually select the correct credential for the environment. It's a click, not a drama, but you have to do it, and you have to know it can happen.

The practical rule that avoids 90% of the pain: name credentials the same across every environment, so name-based reconciliation works. And when you import a workflow into a new environment, check every node with a credential and confirm it ended up connected to the correct credential for that environment, not marked as missing nor —worse— accidentally connected to another one. That thirty-second review on import is what separates a clean promotion from "it worked in dev and prod gives 401."

A detail that puts your mind at ease about this review: reconnecting a missing credential is a click, not a rebuild. When a node shows its credential blank, you open the node, expand the credential selector, and choose from the list the one you already created in that environment with the correct name. n8n rewrites the reference with the local id, and the node ends up connected. You lose nothing about the workflow; you just tell it again which of this environment's credentials to use. That's why the identical-name discipline pays off twice: when automatic reconciliation fails, you find the right credential in the list instantly, because it's named what you expect. If each environment named its credentials differently, that click would turn into a hunt.

And a workflow note for when you actually promote (Module 6): this credential review is a fixed step in the process of bringing a workflow to a new environment, not an accident. The first time a workflow arrives at an environment, its credentials get mapped; the following times, if the names didn't change, the reconnection is automatic or trivial. It's worth turning into a habit —"I imported, now I check the nodes with credentials"— so it never slips past you.

Common mistakes

Putting the real credential in dev (practical, and it defeats the whole module). What happens: someone, from a rush or from copying, creates the Cumbre CRM key credential in dev with the CRM's real key. From that moment, every test in dev queries the real CRM, and any write operation the workflow does touches real data. The "padded room" stops being one. Why it happens: it's easier to copy the key already at hand than to obtain a sandbox one. How to spot it: look at dev's credential values; if they contain -live- or point at the CRM's real domain, there's a real key where it shouldn't be. How to fix it: the real key lives only in prod. Get a sandbox account or key for dev and staging, or use a local model for the AI part. And if you already tested with the real one, review what it touched and, if the key got exposed, rotate it.

Naming the credential differently in each environment (practical). What happens: someone creates Cumbre CRM key in dev, CRM Staging in staging, and CRM Prod in prod. When importing the same order-triage.json, name-based reconciliation fails in staging and prod, and the nodes show up with missing credentials. Why it happens: it seems natural to "identify" the environment in the credential's name. How to spot it: if your credentials have the environment in the name, and importing a workflow leaves the nodes disconnected, this is it. How to fix it: identical name and type across the three environments; what identifies the environment isn't the credential's name, it's which instance it lives in. The name is the standard outlet; putting the environment in it turns it into a different outlet per house.

Trying to export one environment's credential and import it encrypted into another (conceptual). What happens: someone exports dev's credential (encrypted) and tries to import it into prod, expecting it to "get copied." prod can't decrypt it, because its key is different, and it would also have the wrong value. Why it happens: copying seems faster than recreating. How to spot it: credentials that show up but fail when used, or decryption errors after an import between environments. How to fix it: don't copy, recreate: create the credential in each environment with the same name and type but that environment's value. The different encryption key per environment is what makes copying encrypted data fail, and it's on purpose.

Not checking the nodes after importing (practical). What happens: someone imports order-triage into prod, sees it "imported fine," and activates it without opening the nodes. One of them was left with the credential marked as missing, and the workflow fails on its first real run with a 401. Why it happens: the import "finishes" and it's assumed everything ended up connected. How to spot it: nodes with a warning icon or a blank credential after importing. How to fix it: do the thirty-second review —open every node with a credential and confirm it's connected to that environment's correct credential— before activating. It's the difference between discovering the problem yourself, calmly, and discovering it on the first real run.

Exercises

Exercise 1 — Reference or value. For each element, say whether it's a reference (travels in the JSON, the same across the three environments) or a value (lives in the instance, different per environment, secret): (a) the text "name": "Cumbre CRM key" inside the workflow's JSON; (b) the string Bearer sk-crm-live-9f2c8a1b; (c) the node's httpHeaderAuth type; (d) the sandbox CRM key you created in dev; (e) the id "5" in the JSON's credentials block.

See solution

(a) Reference. The credential's name travels in the JSON and is the same across the three environments; it isn't a secret.

(b) Value. It's the CRM's real key: a secret, living only in prod's instance, encrypted, and never in the repo.

(c) Reference. The credential's type is part of the node's logic; the same across the three environments.

(d) Value. The sandbox key is the secret living in dev's instance, different from prod's.

(e) Reference (with a nuance). The id travels in the JSON, but it's an identifier local to the instance where it was exported, so it may not match in another environment; that's why reconnection relies on the name, not the id.

Why it works: if you correctly separated all five, you're clear on the line that makes "one workflow, many environments" possible —the reference is shared and gets versioned; the value is per environment and is secret. The nuance about the id (e) is exactly what explains why nodes sometimes come out disconnected on import.

Exercise 2 — Design order-triage's credential table. order-triage uses two credentials: Cumbre CRM key (Header Auth) and Cumbre LLM key (AI model). Draw a table with one row per credential and one column per environment (dev, staging, prod), and in each cell write what value it would have (not the literal key, but its nature: sandbox, low limit, real, local, etc.). Then point out which cells contain something that must never leave its instance.

See solution
Credentialdevstagingprod
Cumbre CRM keySandbox CRM (made-up data)Staging CRM (data resembling the real thing, no consequences)Real CRM (400 real customers' data)
Cumbre LLM keyLocal Ollama model (zero cost) or low-limit keyLow-limit key or local modelThe real provider's key, production model

The cells containing something that must never leave its instance are the prod column's: the CRM's real key and the AI provider's real key. Those are the ones that, if leaked, grant access to real data and generate real spend. The dev and staging ones are lower risk by design (sandbox, low limit, local), but they're still secrets and don't go to the repo.

Why it works: filling in the table forces you to decide, credential by credential and environment by environment, what's real and what's for testing. That explicit exercise is what prevents the most expensive mistake —a real key in dev— if you thought it through beforehand, you don't put it there by accident.

Exercise 3 — Diagnose the broken import. A teammate imported order-triage into prod and tells you: "It imported, but when I run it the CRM node gives a 401 unauthorized. In dev it works perfectly." You ask them to open the HTTP Request node in prod. What are the two most likely causes, and how do you tell one from the other?

See solution

The two most likely causes:

  1. The credential was left marked as missing or disconnected. On import, the JSON's id (from dev) didn't match any id in prod, and name-based reconciliation didn't happen (maybe because prod's credential has a different name, or it wasn't created). The node has no credential, and that's why the CRM responds 401. How you tell: opening the node, the credential shows up blank or with a warning.

  2. The credential is connected, but its value is wrong. The Cumbre CRM key credential exists in prod and is connected, but it was created with the wrong value —for example, dev's sandbox token got copied in, which the real CRM doesn't recognize. How you tell: opening the node, the credential shows up connected, but testing the call, the real CRM rejects the token.

How to tell them apart: open the node. If the credential is blank/missing, it's cause 1 —create or select it in prod. If it's connected but fails, it's cause 2 —check its value is prod's real key, not one copied from dev.

Why it works: the 401 has two origins that get fixed differently (missing credential vs. credential with the wrong value), and knowing which one before touching anything saves you from guessing blindly. It's exactly the kind of diagnosis a system owner makes.

Summary and next step

In this lesson you made credentials per environment concrete. With the image of the flight simulator and the real cockpit you locked in the rule governing everything: test credentials in dev and staging —the simulator— real credentials only in prod —the cockpit— and Cumbre's real keys never touch another environment. You understood that a credential's reference (its name and type) travels in the JSON and is identical across the three environments, while its value lives in each instance and is different per environment: one reference, three values. You saw why credentials get recreated in each environment instead of copied —because each environment encrypts with its own key and because the value has to be different— with the single discipline of keeping the name and type identical across the three so the same workflow references them. You walked through the flow of creating the credential with a sandbox value in dev and a real one in prod, importing the same order-triage.json into both, and you faced the mismatched-identifier mistake, solved by reconciling by name and reviewing every node on import.

Before moving on you should be able to: explain the difference between a credential's reference and its value and why that enables one workflow for three environments; say why credentials get recreated instead of copied; and describe what to check on the nodes after importing a workflow into another environment.

Lesson 6 takes one more step in the same direction: getting secrets out even of n8n's interface. You're going to meet the $env expression, which lets a node read an environment variable —from that .env you already configured— and n8n's Variables; you're going to understand why secrets should live in the environment's manager and never written inside the workflow; and you're going to see, with plan honesty, what external secrets (Vault, AWS, GCP) offer as an Enterprise feature and what the equivalent Community alternative is using environment variables.

Resources