DEV Community

Cover image for Pasting the same Sentry DSN into every project? Put it in Global Keys once and bind it
uehara
uehara

Posted on Edited on

Pasting the same Sentry DSN into every project? Put it in Global Keys once and bind it

This is part 6 of the series "dotvault, Start to Finish: One Command from Your Laptop to CI, Your Team, and Your AI Agents." dotvault will be released free of charge when this series ends.

A key you put in dotvault's Global Keys pool reaches no project until a project asks for it. The pool is org-wide and belongs to no project. A project receives a key from it only after it binds that key by name, from its own Global keys tab or with dotvault global use <KEY>. Bound keys arrive through the ordinary pull and run at the lowest precedence. In my test, a value the project set for the same name in development replaced the pool's value in the pulled .env.

This is part 6 of 12. Screens are from the production console and CLI 0.15.7, continuing with the demo-app project from parts 2 to 5. Two results worth knowing before you start: pull -E production succeeds on an environment where the project has no production keys of its own, and writes 3 keys (the two bound pool keys with their production overrides, plus API_BASE_URL from the project's Common (_shared) layer); and global describe on a name that is not in the pool, like global drop on a key that was never bound, prints a success line and exits 0, so check names with global keys and global status.

The short version

  • A credential shared across the org goes in Global Keys, not in a project. Use the Global Keys entry in the sidebar or dotvault global set. The value you put there is the common value (Common in the console, _all in the CLI) and applies to every environment. You can add a per-environment override on top.
  • A project receives only the keys it binds. Nothing is broadcast. Bind from the project's Global keys tab, or run dotvault global use <KEY> in the project directory. The name the project receives can differ from the pool name (--as in the CLI, alias in the console).
  • Receiving is the usual pull and run. No extra flag. Bound keys are written first inside the managed block in .env (that is only their position; they still lose to a project key of the same name). Pulling production gives you the production override. Because pull rewrites .env where you run it, check production from a separate directory. An environment with no production keys of its own still pulls: it gets the bound keys' production overrides plus the project's Common (_shared) values.
  • The project's own value wins when names collide. The pool is the weakest layer, common value and overrides alike. global drop (the Unbind button on the project's Global keys tab) stops receiving a key. From the CLI, global delete KEY removes the common value and global delete KEY -E <env> removes that environment's override; removing the whole key, overrides included, is the console's Delete key.
  • Pool commands need --org <id>. Without it, global keys and global set stopped with ORG_CONTEXT_REQUIRED (exit 1). Even when you belong to only one org, the global commands that work on the pool do not pick the org for you. Run describe and delete with --org too. global use, global status and global drop, run inside the project directory, work without --org (all exit 0). The id is in dotvault whoami. A plain dotvault set without global is the exception: in a directory with no project-config.json, it writes to the pool's common value with no --org and exits 0. Run set for project keys from inside the project directory.
  • Binding state lives on the Global keys tab and in global status. A project's Keys tab lists the keys set in that project; bound pool keys show up in the BOUND AS column of its Global keys tab.

Before you read on

  • Where we start. Part 5 left demo-app with .env (5 keys) and .env.local (1 key) in development. The .npmrc and .kube/config pushed in part 5 are still in development too; pushed files are not keys, so they are not in this part's key counts. You can follow this part without having read it.
  • Environment. Production console, CLI 0.15.7, the same Node 20 Linux container as part 2.
  • The org is Personal. The one-person org from part 2. It has no permission settings; everything, Global Keys included, is allowed from the start. How the pool looks in an org with invited members is part 9.
  • Terminal screens are recomposed. The output is reproduced verbatim in a cleaner frame. The raw output is kept as is. Where commands were run separately, they are sometimes joined into one line with cd … &&, and exit codes, which the raw output records as [exit=N], are shown as echo $? lines.
  • Demo account. Project names, values and the org id were created for this series. https://examplePublicKey@o4500000000000000.ingest.sentry.io/… and cf_demo_… are demo values that connect to nothing.
  • Vocabulary. The "pool" is the org-wide store (Global Keys in the console). The "common value" is the value set without an environment: Common in the console, _all in the CLI. An "override" is a value set for one environment. "Binding" is a project declaring that it receives a pool key; an "alias" is the name it receives it under. An "exit code" is the number a command returns when it ends: 0 for success, anything else for failure. The "managed block" is the part of .env that dotvault writes, between the # BEGIN and # END lines; lines you wrote by hand stay outside it. Unbinding (Unbind in the console, global drop in the CLI) stops a project receiving a pool key.

1. Project keys versus pool keys

Every key up to part 4 lived inside a project, inside an environment. A pool key lives in neither.

Project key Pool key (Global Keys)
Where project × environment (the Common (_shared) layer from part 3 is still inside the project) one pool per org, outside every project
Written with the project's Keys tab, set / import Global Keys in the sidebar, global set
Value layers environment value and the project's Common (_shared) value common value (Common / _all) and per-environment overrides
Delivered to that project only projects that bind it; nothing is delivered on its own
Received with pull / run the same pull / run, no extra step
Same name on both sides — the project's value wins; the pool is the weakest layer
Seeing the value Reveal on the Keys tab, pull never in the console; only in the pulled file

The notice at the top of the Global Keys page says the same thing: "This is an org-shared secret pool that isn't tied to any project. … Only the ones a project explicitly binds from its own Global keys tab are included in that project's pull. … This page never displays the values themselves — check with dotvault global keys or dotvault pull."

2. Putting a key in the pool from the console

Open Global Keys in the sidebar. It starts empty.

The empty Global Keys page (English UI). The notice at the top, the Add a key heading with "The value you add is registered as the common value (_all). Per-env overrides are set afterwards from "Update value" on the row.", the Key, Value and Description fields with an Add key button, and "No keys yet. Add one from the form above." under Keys in the pool.

Fill in the name, the value and a description under Add a key. Names may use letters, digits and _, and must start with a letter or _ (e.g. SENTRY_DSN).

Add a key filled in with SENTRY_DSN, a value and the description "Sentry DSN shared by every product". The Add key button is enabled.

Add key shows "Added SENTRY_DSN (common value)." and one row appears under Keys in the pool with a Common tag in the COMMON / OVERRIDE column. This is the common value: development, staging and production all receive it.

To give production a different value, press Update value on the row.

Update value opened: the target select (Common (_all) / development / staging / production) set to production, a new value typed in, and the Update button.

The choices are Common (_all) and the three environments. Choose production, type the value and press Update: "Updated the value of SENTRY_DSN (production)."

The Keys in the pool table (English UI). The SENTRY_DSN row with the description "Sentry DSN shared by every product", Common and production tags in the COMMON / OVERRIDE column, Update value / Delete override / Delete key buttons under ACTIONS and the notice "Updated the value of SENTRY_DSN (production)."

Two tags now: Common and production. It means "production gets the override, every other environment gets the common value". A Delete override button appears once an override exists.

The page never shows a value. There is no equivalent of the Reveal button from part 4. The way to check what you typed is pull, in section 5.

3. Putting a key in the pool from the CLI

The same thing from the terminal. Pool commands live under dotvault global.

Terminal screen of the pool commands (recomposed). dotvault whoami prints the email and "org: org_personal_f4785468-… Personal (personal, org_admin)"; dotvault global keys fails with "ORG_CONTEXT_REQUIRED: X-Org-Context header required to resolve an org for this session"; with --org it lists SENTRY_DSN with envs common,production; global set CLOUDFLARE_API_TOKEN=… prints "set 1 key(s) in the global pool [_all]"; with -E production it prints "[production]"; global describe prints "described CLOUDFLARE_API_TOKEN in the global pool [_all]"; the final global keys lists two rows.

The first dotvault global keys fails. ORG_CONTEXT_REQUIRED means the CLI cannot tell which org's pool you mean: the global commands that work on the pool do not pick an org from your login, even when you belong to only one, and since the pool belongs to no project there is no project to derive it from. Add --org <id>. The id is on the org: line of dotvault whoami. A Personal org has a long org_personal_… id; you type it once and recall it from shell history.

global set KEY=VALUE writes the common value ([_all]) without -E, and a production override with -E production. global describe KEY "text" attaches a description. The envs column of global keys reads common,production: whether a common value exists, and which environments carry an override.

At this point the pool holds SENTRY_DSN, added in the console, and CLOUDFLARE_API_TOKEN, added from the CLI, each with a common value and a production override. The console shows the CLI's key and the CLI lists the console's key, as the same rows.

4. Binding a key to a project

Putting a key in the pool delivers nothing to demo-app. The project has to opt in.

From the CLI, in the project directory: dotvault global use <KEY>.

Terminal screen of binding (recomposed). dotvault global status prints "project=demo-app: no global keys bound."; global use SENTRY_DSN prints "Bound SENTRY_DSN to demo-app."; global use CLOUDFLARE_API_TOKEN --as CF_API_TOKEN prints "Bound CLOUDFLARE_API_TOKEN (as CF_API_TOKEN) to demo-app."; global use GH_TOKEN prints "Bound GH_TOKEN to demo-app." followed by "Warning: the global project has no value yet for: GH_TOKEN"; global status lists three lines (CLOUDFLARE_API_TOKEN -> CF_API_TOKEN, GH_TOKEN, SENTRY_DSN).

--as CF_API_TOKEN renames the key on the way in. The pool still calls it CLOUDFLARE_API_TOKEN; demo-app's .env gets CF_API_TOKEN=…. Use it when the application already reads a different name.

The third command binds GH_TOKEN, a name that is not in the pool. It says "Bound" and then warns "the global project has no value yet for: GH_TOKEN". The binding is accepted; with no value behind it, nothing is pulled (section 5 confirms that).

In the console, binding state is the BOUND AS column of the project's Global keys tab (section 6b binds one from there, with an alias).

global status, global use and global drop run in the project directory or take -p <project>. In a directory with no project-config.json and no -p, you get the VALIDATION_ERROR in section 9.

5. Pulling

In the demo-app directory, dotvault pull. No extra flag.

Terminal screen of dotvault pull (recomposed). dotvault pull prints "Wrote 7 keys to /work/demo-app/.env", "Wrote 1 keys to /work/demo-app/.env.local" and "2 files synced, 0 excluded, 8 keys total"; cat .env shows the hand-written PORT=3000, then the managed block with CF_API_TOKEN=cf_demo_…, SENTRY_DSN=https://examplePublicKey@…, API_BASE_URL, DATABASE_URL, FEATURE_FLAGS, REDIS_URL and SMTP_PASSWORD, seven lines. cd ../prod-check && dotvault pull -E production prints "Wrote 3 keys" and "1 file synced, 1 excluded, 3 keys total"; cat .env shows CF_API_TOKEN=cf_demo_prod_…, SENTRY_DSN=https://prodPublicKey@… and API_BASE_URL.

The development .env went from 5 keys to 7. The two new ones, CF_API_TOKEN and SENTRY_DSN, sit at the top of the managed block (between # BEGIN and # END); the project's own keys follow in name order. GH_TOKEN, bound in section 4 with no value in the pool, is not written and there is no warning about it.

The "7 keys" in the output includes the two from the pool. The keys from the pool sit at the top of the managed block, and global status shows which ones are bound.

The lower half is production. pull rewrites the managed block of .env in the directory you run it from, with the values of the environment you name (that is why the managed block in prod-check below holds only the 3 production keys). Running -E production inside demo-app would drop the development values such as DATABASE_URL from the block and put production values in your local .env. So I checked from a separate directory (prod-check) holding only a copy of project-config.json, and ran pull -E production there. demo-app has never had a key in production. pull still succeeds and writes 3 keys: CF_API_TOKEN and SENTRY_DSN with their production override values (cf_demo_prod_… and prodPublicKey), and API_BASE_URL from the project's Common (_shared) layer set in part 3. "1 excluded" means .env.local, which has no production keys, was not written.

I pulled staging too. Staging has no override, so CF_API_TOKEN and SENTRY_DSN arrive with their common values (image omitted; the raw output is kept). An override applies to its environment and the common value applies everywhere else, and the pulled files bear that out.

run behaves the same way: the last screen in section 6b shows dotvault run -- sh -c 'printenv CF_API_TOKEN SENTRY_DSN_WEB' printing both values.

6. When the project has a key of the same name

With the pool's SENTRY_DSN still bound, put a key of the same name in demo-app's development.

Terminal screen of the name collision (recomposed). dotvault set SENTRY_DSN=https://projectOwnKey@… prints "set SENTRY_DSN (v1) in .env [demo-app/development]"; dotvault pull writes 7 keys; grep -n SENTRY .env shows line 6 with the projectOwnKey value; dotvault global drop GH_TOKEN and global drop SENTRY_DSN print "Dropped … from demo-app."; global status lists only CLOUDFLARE_API_TOKEN -> CF_API_TOKEN; dotvault delete SENTRY_DSN prints "deleted 1 keys from .env"; dotvault pull writes 6 keys; grep shows only line 5, CF_API_TOKEN.

After pull, the SENTRY_DSN line carries the project's projectOwnKey value. The key count stays at 7: the pool's copy was replaced, not added to. The line keeps its bound position near the top of the managed block; only the value is the project's.

The pool is the weakest layer. A project environment value beats it.

global drop stops receiving. In the console, the Unbind button on the project's Global keys tab is the same operation (the CLI help describes drop as "Unbind an org global key from this project."). After dropping GH_TOKEN and SENTRY_DSN, global status is down to the one CLOUDFLARE_API_TOKEN line. I then removed the project's own SENTRY_DSN with dotvault delete, and pull is back to 6 keys (the 5 from part 5 plus CF_API_TOKEN). Dropping only removes the project's binding; the key stays in the pool.

Before 6b, remove the production override of CLOUDFLARE_API_TOKEN from the CLI: dotvault global delete CLOUDFLARE_API_TOKEN -E production --org <id> prints "deleted 1 key(s) from the global pool [production]", and the next global keys shows CLOUDFLARE_API_TOKEN with envs common only (both outputs are on the last terminal screen of section 7). That is why the CLOUDFLARE_API_TOKEN row on the 6b screens shows Common only.

6b. Rebinding from the console, with an alias

Now bind the dropped SENTRY_DSN again, this time from the console and under a different name. In the console, it is the project's Global keys tab. Every pool key is listed, and the BOUND AS column says whether this project is bound to it.

The Global keys tab of project demo-app (English UI). Under the notice, "Keys available in the org's shared pool, and whether demo-app is bound to each one." and a table with CLOUDFLARE_API_TOKEN (Common, BOUND AS CF_API_TOKEN, an Unbind button) and SENTRY_DSN (Common and production, the alias field being filled with SENTRY_DSN_WEB, a Bind button).

SENTRY_DSN was just dropped, so its row shows a Bind button. The CLOUDFLARE_API_TOKEN binding made from the CLI shows up here as CF_API_TOKEN. Typing SENTRY_DSN_WEB into the alias (optional) field of the SENTRY_DSN row and pressing Bind gives "Bound SENTRY_DSN." and fills BOUND AS with SENTRY_DSN_WEB.

The Global keys tab after binding. The "Bound SENTRY_DSN." notice, both rows showing a name under BOUND AS (CF_API_TOKEN, SENTRY_DSN_WEB) and Unbind buttons.

A project's Keys tab lists the keys set in that project: in development, the same 4 keys as at the end of part 5 (the ones set as development values). In part 5, .env had 5 because the fifth, API_BASE_URL, is the project's Common (_shared) value from part 3. The places that show binding state are the Global keys tab and global status.

A binding made in the console shows up in global status too.

Terminal screen after binding in the console (recomposed). dotvault global status prints "CLOUDFLARE_API_TOKEN -> CF_API_TOKEN" and "SENTRY_DSN -> SENTRY_DSN_WEB"; dotvault pull writes 7 keys; grep shows line 5 CF_API_TOKEN=… and line 6 SENTRY_DSN_WEB=…; dotvault run -- sh -c 'printenv CF_API_TOKEN SENTRY_DSN_WEB' prints the two values; in another directory dotvault global status -p demo-app works.

global status shows two lines, CLOUDFLARE_API_TOKEN -> CF_API_TOKEN and SENTRY_DSN -> SENTRY_DSN_WEB; pull writes 7 keys, and .env gets SENTRY_DSN_WEB=…. Part 7 starts from this state.

7. Removing from the pool

Removal has two levels: one override, or the whole key.

In the console, Delete override on the row opens an environment select and a Delete override button. The CLI equivalent, removing only the production override, is global delete … -E production below.

Delete override opened under the SENTRY_DSN row, before pressing its button: an environment select set to production and a Delete override button.

Delete key removes the common value and every override. Pressing it shows "This deletes the common value and every override (1)." under the row, with the number of overrides, and a second Delete button.

Delete key opened under the SENTRY_DSN row: "This deletes the common value and every override (1)." and a red Delete button.

Pressing Delete then raises the browser's own confirmation, which names the key being deleted. On a throwaway key created for this, TMP_DELETE_ME, it read "Delete TMP_DELETE_ME entirely (the common value and every override)? This cannot be undone." (not capturable as an image, so quoted).

From the CLI, global delete KEY removes the common value and global delete KEY -E production removes only the production override. The screen below captures the CLOUDFLARE_API_TOKEN override removal from the end of section 6. Just before it, I also tried a bare set, which is why the three-row global keys in the middle still shows a production override on CLOUDFLARE_API_TOKEN.

Terminal screen of a bare set and the delete commands (recomposed). In /work/nowhere, a directory with no project-config.json, dotvault set BARE_TEST=1 prints "set 1 key(s) in the global pool [_all]"; global keys lists three rows including BARE_TEST; global delete BARE_TEST prints "deleted 1 key(s) from the global pool [_all]"; global delete CLOUDFLARE_API_TOKEN -E production prints "[production]"; the final global keys shows CLOUDFLARE_API_TOKEN with envs common only.

The first two commands on that screen are a separate point. In a directory with no project-config.json, a plain dotvault set BARE_TEST=1, without global, prints "set 1 key(s) in the global pool [_all]". A set that cannot resolve a project writes to the pool, and it does not need --org: with a single org membership, the bare set resolves the org on its own. The global commands take --org, as in section 3. The set from part 4, typed somewhere without a project-config.json, lands in the pool instead of a project. If that happens by accident, global keys shows it and global delete removes it.

The last global keys shows CLOUDFLARE_API_TOKEN with common only: deleting the production override leaves the common value in place.

8. Permissions and service tokens

This part runs in a Personal org (an org of one), so there are no permission settings. Service tokens for CI come in part 7, and who can write to the pool in an org with invited members comes in part 9, both with console screens.

9. When it fails, and when a wrong name still prints success

The failures from this part, and the commands that print a success line even when the name is wrong.

Terminal screen of failures (recomposed). dotvault global keys prints ORG_CONTEXT_REQUIRED and exit 1; global describe NO_SUCH_KEY "does not exist" --org … prints "described NO_SUCH_KEY in the global pool [_all]" and exit 0; global drop NOT_BOUND prints "Dropped NOT_BOUND from demo-app." and exit 0; global status in a directory without a config prints VALIDATION_ERROR (No project-config.json found …) and exit 6; global set --org … prints "error: missing required argument 'pairs'" and exit 1.

Command Output Exit code
global keys / global set without --org ORG_CONTEXT_REQUIRED: X-Org-Context header required to resolve an org for this session 1
global describe NO_SUCH_KEY "…" (name not in the pool) described NO_SUCH_KEY in the global pool _all 0
global use GH_TOKEN (name not in the pool) Bound GH_TOKEN … + Warning: the global project has no value yet for: GH_TOKEN 0
global drop NOT_BOUND (never bound) Dropped NOT_BOUND from demo-app. 0
global status with no project-config.json and no -p VALIDATION_ERROR: No project-config.json found … For the org-wide pool use dotvault global … 6
global set --org … with no KEY=VALUE error: missing required argument 'pairs' 1

The first message means the CLI could not tell which org's pool you meant. Run it again with --org <id>, as in the global keys --org … runs in section 3.

In the second and fourth rows, describing a name that is not in the pool, or dropping a key that was never bound, prints a success line and exits 0. Check the names with global keys and global status. The use command (third row) adds a warning.

This table is the output of CLI 0.15.7. To catch a mistyped name or a missing bind, check the global status and pull output as in sections 4 and 5.

Next

Part 7 hands secrets to CI without putting a person's password there. A service token issued in the console lets GitHub Actions pull the production values, and the CF_API_TOKEN and SENTRY_DSN_WEB bound in this part show up in that pull as well.


dotvault stores the secrets in your .env and config files server-side and hands them to any authorised developer, CI job or AI agent with a single command (https://dotvault.io). It will be released free of charge when this series ends, and the steps in these articles are the steps you will use on day one.

If you find this useful, please like and follow.

Our products are listed at https://www.eln.ne.jp/products


EarthLink Network builds its own products to run the whole company on AI — each one born from a need along the way. See the full index of what we build:

→ The 18 products EarthLink Network builds in-house

For more about the company and each product, visit www.eln.ne.jp.

Top comments (0)