This is part 4 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.
Paste the staging DATABASE_URL over the dev one by mistake, and with a plain .env the old value is gone the moment you save. In dotvault it is not: one command, dotvault rollback, brings it back. Writing a key never destroys the previous value. Each write is a new version: DATABASE_URL v1, then v2. dotvault versions DATABASE_URL lists them newest first with a timestamp, the writer's ID and a hash of the value. dotvault rollback DATABASE_URL --to 1 restores v1, and the restore itself is recorded as v3, so v2 stays in the history. Deleting a key removes its current value and keeps the history too. On top of that, keys can be written from the console (one at a time, or a whole .env dropped on the Upload tab) or from the CLI (set for one, import for a file), and dotvault pull --file .env.local fetches one file instead of all of them.
This is part 4 of 12. Screens are from the production console and CLI 0.15.7, continuing with the demo-app project created in part 2 and extended in part 3. One thing worth knowing up front: dotvault versions FOO still shows v1 after dotvault delete FOO.
The short version
-
Keys can be written from the console or the CLI. Console: + Add key on the Keys tab, or drop a
.envon the Upload tab. CLI:dotvault set KEY=VALUEfor one key,dotvault import .envfor a file. Preferimportwhen there is more than one key;setputs the value on the command line, where your shell history keeps it. -
A write is a new version, not an overwrite.
dotvault versions KEYlists every version with its number, timestamp, writer and value hash. -
dotvault rollback KEY --to Nrestores version N as a new version. Rolling back to v1 produces v3; v2 is not removed. History is never rewritten. -
dotvault pullwith no arguments writes every file the project has for the current environment.--file .env.localwrites that one file only. -
Reveal value shows a value in the console; Delete in the console or
dotvault deleteremoves a key. The history stays visible throughversions. The Reveal button says "audit-logged"; the audit screen is covered in part 10.
Before you read on
-
Where we start. Part 3 left
demo-appwith four environments (development, staging, production, preview) and a Common layer. 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.
- Terminal screens are recomposed. The terminal output is reproduced verbatim in a cleaner frame. The raw output is kept as is.
- Demo account. Project names, key names and values were created for this series. Every value is a placeholder made for this article.
-
Vocabulary. "Secret" means any value that must not be exposed, such as an API key or a password. A "key" is a name such as
DATABASE_URL, the "value" is its content, and a "version" is the number that goes up by one every time the value is written.
1. Writing one key at a time in the console
"+ Add key" on the Keys tab opens a name and a value field at the top of the list. Fill both in and press Add.
The list is sorted by name, not by the order you added keys. SESSION_SECRET is deleted again later in this part.
A project can hold more than one file. Besides .env, you can add .env.local or any other name. "+ Add file" opens a file name field, prefilled with .env.local.
Create adds an .env.local block under the .env list and opens the first key row. The file is not saved until it has at least one key.
After Add, the four keys of .env and the one key of .env.local sit in two separate blocks. Later in this part, the CLI pulls .env.local alone, with --file.
2. Registering a whole .env from the console
Ten keys, one at a time, is tedious. The Upload tab takes a .env file and registers its keys in one go.
Pick the environment first; "Uploading into development." above the drop zone tells you where the file will land. Dropping the file shows a preview before anything is registered.
Check the rows, then press Upload. One line confirms the result.
Back on the Keys tab, the .env list has six rows.
The "Whole-file uploads" mentioned in the hint are a different mechanism for files such as .npmrc or a kubeconfig that are distributed as they are; that is part 5. The Upload tab reads the keys and values out of the file and registers them one by one.
3. Writing from the CLI: import and set
Back in the terminal. The terminal screens from here to section 5 were captured before sections 1 and 2, so they show one file (.env) and 5 keys: the four rows of the Keys tab plus API_BASE_URL from Common. MAIL_HOST and MAIL_FROM from section 2 were deleted after the captures with dotvault delete MAIL_HOST MAIL_FROM (not shown).
There are two ways to write keys from the CLI. dotvault import <file> registers every key/value pair in a file. dotvault set KEY=VALUE registers the pairs given as arguments.
Reach for import first. set takes the value on the command line, so the value ends up in your shell history (~/.zsh_history and the like). import reads a file; the only thing the history keeps is the file name.
import reads only the KEY=VALUE lines. Comment lines starting with # are skipped. A quoted value (FEATURE_FLAGS="beta,new-dashboard") is stored without the quotes. A file with no KEY=VALUE line at all stops with VALIDATION_ERROR (section 8).
The third command in that screen lists the versions of DATABASE_URL. It was entered from the console in part 2 and never touched since, so there is exactly one, v1. The next section overwrites it.
4. Overwriting by mistake
This is the accident from the opening, reproduced on purpose: DATABASE_URL gets the name of the staging database.
set answers with (v2). Nothing was overwritten; a second version was added. dotvault pull writes the v2 value (app_staging) into .env. One of the 5 keys is API_BASE_URL, promoted to Common in part 3; Common keys are written on every pull, whichever environment you pull. So far this is what any text file would do.
The difference is dotvault versions DATABASE_URL. v2 and v1 are listed newest first, each with the timestamp, the ID of the person who wrote it and a hash of the value (sha=). The value itself is not printed. Same hash, same value; different hash, different value. That is all it tells you.
5. Rolling back
dotvault rollback DATABASE_URL --to 1 restores the v1 value.
The output reads "rolled back DATABASE_URL to v1 (now v3)". You asked for v1, and the current version is v3. That is how dotvault rolls back: it does not delete v2 and step back to v1, it writes the v1 value again as v3. In versions, v3 and v1 carry the same sha= and only v2 differs. The mistaken v2 and the restoring v3 both stay.
Getting the restored value onto disk is the usual dotvault pull; grep confirms app_dev is back. A rollback changes the value on the dotvault side only. Your local .env does not change until you pull.
You can only roll back to a version that exists. The output for a version number that does not exist is in section 8.
6. Pulling: all files, or one
dotvault pull, as used since part 2, writes every file the project holds for that environment when called without arguments. Section 1 added .env.local, so there are now two. --file restricts it to one.
.env.local is written the same way as .env: wrapped in BEGIN / END markers, with -rw------- permissions so only the owner can read it. In dotvault list demo-app, development now has one row for .env and one for .env.local.
.env shows 5 keys here: the four rows of the Keys tab plus API_BASE_URL from Common. MAIL_HOST and MAIL_FROM from section 2 were deleted after the captures, as noted at the start of section 3.
set, versions and the other key commands also accept --file. Without it they operate on the first entry of files in project-config.json, which is .env after the init from part 2. Working on a key in .env.local means adding --file .env.local every time.
Forget --file and the command looks in .env, reports "0 versions" and exits without an error. When a key seems to be missing, check the file first.
7. Revealing, describing and deleting
Selecting a key in the console opens the Selected key panel on the right, with a description field and a "Reveal value (audit-logged)" button. Reveal value turns the button into a field holding the value, with a Save button. Save was not pressed in this part.
The button says "audit-logged". The audit screen is part 10; who else on a team can reveal a value is part 9, where people are invited.
The description is a short text kept next to the value. The console has a Save description button; in this part it was set from the CLI with dotvault describe KEY "text". Changing the description does not add a version.
Deleting a key is Delete in the console or dotvault delete KEY on the CLI. The console asks first.
The CLI does not ask, and takes several keys at once.
Look at the last command, dotvault versions FOO. The key was just deleted, and v1 is still listed. Delete removes the current value; it does not remove the history.
describe on a key that does not exist prints a warning and exits with code 1. The exit code is the number a command returns when it finishes: 0 means success, anything else means failure. Type echo $? right after the command to see it. It is a yellow warning rather than a red ✖, so a script should check the exit code rather than the colour.
8. When it fails
The failures met in this part, with their exit codes.
| Command | Output | Exit code |
|---|---|---|
rollback --to 9 (no such version) |
PROJECT_NOT_FOUND, server text "version 9 … not found" | 5 |
rollback --to 0 |
VALIDATION_ERROR (positive integer) | 6 |
import empty.env (no KEY=VALUE line) |
VALIDATION_ERROR (No KEY=VALUE lines) | 6 |
pull --file .env.nope (no such file) |
PROJECT_NOT_FOUND, server text "No DDB secrets for .env.nope" | 5 |
describe NO_SUCH_KEY (no such key) |
warning (description not applied) | 1 |
A missing version and a missing file both come back as PROJECT_NOT_FOUND with exit code 5. What was not found is named in the server text in parentheses.
Next
Part 5 is "Still sending .npmrc over Slack? Push whole files, scan before you commit": dotvault push for files that do not split into keys and values, and dotvault scan for catching secrets before they are committed, with screens.
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.








![dotvault import in the terminal (recomposed): cat .env.import shows a comment line, REDIS_URL and FEATURE_FLAGS; dotvault import .env.import prints "imported 2 keys into .env [demo-app/development]"; dotvault versions DATABASE_URL prints "1 versions" and a single v1 line.](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmgt5gfngrkp1dygn1tha.png)
![dotvault set overwriting a key (recomposed): dotvault set DATABASE_URL=…/app_staging prints "set DATABASE_URL (v2) in .env [demo-app/development]"; dotvault pull writes 5 keys, 1 file synced; grep DATABASE_URL .env shows the app_staging value; dotvault versions DATABASE_URL prints "2 versions" with v2 and v1 lines, each with a timestamp, a writer ID and a sha.](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh0skrmd3tg8rkx9o0a99.png)
![dotvault rollback --to 1 in the terminal (recomposed): "rolled back DATABASE_URL to v1 (now v3) in .env [demo-app/development]"; dotvault versions DATABASE_URL prints "3 versions" with v3, v2 and v1, where v3 and v1 share the same sha; dotvault pull writes 5 keys; grep DATABASE_URL .env shows the app_dev value.](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyz2e01pvof3cx63zofqw.png)

![--file changing which file a command looks at (recomposed): dotvault versions NEXT_PUBLIC_APP_URL prints "[demo-app/development/.env] — 0 versions"; with --file .env.local it prints "[demo-app/development/.env.local] — 1 versions" and a v1 line; dotvault set --file .env.local NEXT_PUBLIC_APP_URL=http://localhost:3001 prints (v2); dotvault rollback NEXT_PUBLIC_APP_URL --file .env.local --to 1 prints "to v1 (now v3)".](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftnagxaf12c00fi353in1.png)


![dotvault describe and dotvault delete in the terminal (recomposed): describe DATABASE_URL "RDS writer (app_dev). Rotated quarterly." prints "described DATABASE_URL in .env [demo-app/development]"; describe NO_SUCH_KEY "x" prints a yellow "warning: NO_SUCH_KEY has no current value … — description not applied" and echo $? prints 1; set FOO=1 BAR=2 prints two (v1) lines; delete FOO BAR prints "deleted 2 keys from .env [demo-app/development]"; versions FOO prints "1 versions" and a v1 line.](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4b1960k4njbhnz86epgf.png)

Top comments (0)