Every environment can be opened as a single .env file instead of a table. The editor decrypts the environment in your browser, hands you the whole document in a textarea, re-parses it on every keystroke, and turns the difference between what you typed and what is live into one atomic change set. It is the dashboard twin of envless push: the same plan, the same drift check, the same server-side apply.
No value leaves your browser in plaintext. Values are decrypted locally to build the file and re-encrypted locally before the apply request is sent, so the API only ever handles ciphertext. Variable names are not encrypted anywhere in Envless, so the plan and the apply request carry them as written.
Where it lives
The editor is the Raw tab on the variables page, next to Table:
https://envless.cloud/dashboard/products/<product>/projects/<project>/environments/<environment>/rawSwitching tabs navigates between that path and the table path and carries the current query string over, so filters and open drawers survive the switch. Moving from Table to Raw clears any selected rows. The older, product-less path /projects/<project>/environments/<environment>/raw still resolves, and redirects here when that project slug belongs to exactly one product. When two products hold a project with the same slug, it asks which one you meant instead of guessing.
The tabs only render for a member with the variables:read permission. Without it the /raw URL does not open the editor.
Building the file
Opening the tab builds the document from the environment's current variables:
- Deleted variables are left out, and the rest are sorted by name.
- Each value is decrypted locally with the workspace key, in parallel, before a single line is written.
- Lines are
NAME=value, one variable per line, and the document ends with a newline. - A value is written bare when it can be. A value that is empty or contains whitespace,
#,",',=, a backtick,$or a backslash is double quoted, and inside those quotes a backslash, a", a newline, a carriage return and a tab are escaped.
Four states replace the editor rather than showing you half a file:
| State | What you get |
|---|---|
| The workspace key is not unlocked in this browser | "Values are encrypted", with an Unlock button to Encryption settings |
| At least one value will not decrypt with your key | "Some values could not be decrypted". Fix those variables in the table first, since a file missing a value cannot round trip |
A variable name cannot be written in .env syntax | "This environment cannot be edited as a file", listing every offending name. Names must match ^[A-Za-z_][A-Za-z0-9_]*$ to be representable |
| The document could not be built for any other reason | "This environment could not be read as a file", with Try again |
An environment with no variables opens as an empty document with a placeholder line, so you can type a fresh file into it.
The live parser
The document is parsed on every change, from scratch, over the whole text. The rules are the ordinary .env ones:
- Blank lines and lines whose first non-space character is
#are ignored. - A leading
exportis stripped, so a sourceable file parses cleanly. - Everything before the first
=is the name, trimmed. Everything after it is the value, trimmed. - A value may be bare, single quoted or double quoted. In a bare value, a
#starts a trailing comment and is cut off, soCOLOR=#fffkeeps its#butPORT=3000 # devstores3000. - After a closing quote, only whitespace or a
#comment is allowed. - Inside double quotes,
\n,\r,\t,\"and\\are decoded. Single quoted values are taken literally. - Names are matched against
^[A-Za-z_][A-Za-z0-9_]*$. Lowercase is legal and is kept as written, but two names that differ only in case count as the same name.
Close to the file envless push reads, but not the same parser. The editor refuses a duplicate name where the CLI silently keeps the last one, refuses a quoted value that runs past the end of its line where the CLI joins the following lines into it, and decodes \t inside double quotes where the CLI leaves those two characters as written.
A line that fails any of those rules produces a diagnostic, is skipped, and blocks the apply:
| Code | Message |
|---|---|
missing_equals | No "=" on this line. Write NAME=value, or start the line with # to make it a comment. |
empty_key | Missing a name before the "=". |
invalid_key | Names can only use letters, numbers and underscores, and cannot start with a number. |
unterminated_quote | This value opens a quote that is never closed. |
trailing_characters | Unexpected text after the closing quote. |
duplicate_key | <NAME> is already defined on line <N>. Two spellings that differ only in case are the same name here |
Diagnostics are listed under the editor with their line numbers, under a heading that counts them: "3 lines need fixing before you can apply".
The change plan
Every parse is followed by a diff against the environment as it was loaded, matched by name and without regard to case, so API_KEY in the document lines up with a live api_key:
| Bucket | Meaning |
|---|---|
| Created | A name in the document that no live variable has |
| Updated | A name that exists live, where the parsed value differs from the decrypted one |
| Deleted | A live variable whose name is not in the document at all |
| Unchanged | A name that exists live with exactly the same value. Not sent, only counted |
The toolbar above the editor shows the live variable count, then +created, ~updated and -deleted. The counts update as you type, before you open anything.
The diff is by name, so the editor cannot rename anything in place. Changing OLD_NAME to NEW_NAME in the file is one delete plus one create, which means the variable's history starts over. Rename in the table when you want the history to follow the variable. Retyping a name in a different casing is not a rename either: the name matches the live variable, so the line is an update at most and the stored casing does not move.
Two more consequences of the file being the whole truth. Comments and blank lines are not stored anywhere, so they are gone the next time the document is built. And a variable created here takes the defaults, type string, visibility server, required false, because a .env line carries no metadata. An update only rewrites the value, so an existing variable keeps the type, visibility and required flag it already had.
The apply gate
Review changes opens a modal that lists the plan by name, deletions first, then additions, then updates, under a summary line of the form "2 added, 5 updated, 1 removed. 12 left alone." Values are never shown in it.
The Apply button in that modal is disabled in three cases:
- The parser reported at least one diagnostic. One bad line anywhere blocks the entire apply, not just that line.
- The plan is empty. Editing whitespace, reordering lines or adding comments changes the document without changing any value, and the editor says so: "Your edits do not change any values, so there is nothing to apply."
- An apply is already in flight. The modal also refuses to close while the request is running.
Discard on the toolbar throws your edits away and restores the last built document. It is disabled while the document is untouched.
What applying does
Applying encrypts each created and updated value in the browser, then sends one request containing the baseline, the creates (name plus ciphertext), the updates (unique ID plus ciphertext) and the deletes (unique ID only). The server runs the whole change set inside a single database transaction, so a rejected apply changes nothing at all and there is no partial write.
On success you get a toast counting what landed, the editor's baseline is replaced by what you just applied, and:
- Every created, updated and deleted variable gets an entry in its own history, recorded with
raw_editoras the source. - Deletes are soft, so the variable's history survives the removal.
- The environment's
updatedAtmoves. - One
variable.appliedwebhook event fires for the whole apply. The individual creates, updates and deletes do not emit their own events.
Applying writes the environment's live values. It does not publish a version. Publish separately when you want a snapshot to restore or expose later, see envless version.
The same service backs the public apply endpoint and envless push, so a change made from any of the three behaves identically.
The baseline, and the 409
The editor is optimistic rather than locked. Nothing is reserved while you type, and the reconciliation happens at apply time.
Every apply carries a baseline: the unique ID, the name and the updatedAt timestamp of every live variable in the environment, as the editor saw them when it built the document. That is what makes the write safe.
The server reads the environment and its revision
It reads the live variables and the environment's revision number together, in one round trip to the database.
It recomputes drift
It compares the live variables against your baseline in both directions: a live variable missing from your baseline is drift (someone added it), a baseline entry missing from the live set is drift (someone deleted it), and a matching ID whose updatedAt differs is drift (someone changed it).
Any drift refuses the whole change set
The response is 409 and it names the variables, sorted: DATABASE_URL, STRIPE_KEY changed while you were editing. Reload to pick up the latest values, then apply again. Nothing is written, not even the parts that did not conflict.
The write lands only on the revision it read
Every change is written in one atomic batch that first checks the environment is still at the revision read in the first step, and then bumps that revision. If another write landed in between, the batch is rolled back and the apply starts again from the first step, so two applies against the same environment never interleave: the later one sees the earlier one's changes as drift and is refused.
Three further conflicts are refused with a 409 too. The first two are checked against the rows read in the first step. The last one needs nothing from the database, so it is checked before anything is read:
| Situation | Message |
|---|---|
| A create targets a name that is live, compared without regard to case, and is not being deleted in the same change | <NAME> already exists in this environment. |
| An update or delete targets a unique ID that is no longer live | This change refers to variables that are no longer in this environment. Reload and try again. |
| The same name appears twice among the creates, ignoring case | <NAME> appears more than once in this change. |
A refused apply surfaces as a "Changes not applied" toast carrying the server's message, and the document stays exactly as you typed it, so nothing you wrote is lost while you reconcile.
Drift you can see before you apply
The editor rebuilds the file whenever the variable list it reads comes back changed. If that happens while you have unapplied edits, it does not overwrite your buffer. It shows an amber banner instead:
Someone else changed this environment. Applying now will be rejected.The banner offers Reload and discard my edits, which rebuilds the file from the current server state and refreshes the baseline the plan is computed against. Your typed document is kept in the editor rather than replaced by the newly built one, so after reloading, re-read the plan counts before applying: they are now measured against the newer values.
If you have no unapplied edits, the document is rebuilt silently and no banner appears.
The banner is not a live watch. The dashboard suspends its background refresh for as long as the document is dirty, precisely so your buffer is never rebuilt underneath you, so the banner only appears when the variable list is refetched for some other reason, such as a reconnect or a change you make from this page. Treat it as a courtesy rather than a guarantee. Drift you never saw is still caught at apply time by the 409.
Leaving with unapplied changes
Navigating to another page in the dashboard while the document is dirty and the plan is not empty raises a browser confirm: "You have unapplied changes. Leave the editor and discard them?". Confirming discards the edits and continues; cancelling keeps you in the editor. Switching to the Table tab is a navigation, so it is guarded too.
The guard is limited to in-app navigation. Reloading the page, closing the tab or following a link out of the dashboard is not intercepted, and unapplied edits are gone without a prompt. Nothing is stored server side until you apply.
Read only mode
The editor renders read only, with the textarea locked and the toolbar reading "Read only. You cannot apply changes here.", when either of these is true:
- You do not hold the
variables:updatepermission. - The project is locked because it exceeds your plan's project limit. The apply endpoint refuses those writes too, with
401andThis project exceeds your plan limit. Upgrade to make changes.
Read only still gives you the whole decrypted file to read, search and copy, since building it only requires variables:read and the workspace key.
The permission that unlocks the editor is variables:update, but the server checks each bucket separately: an apply needs variables:read, plus variables:create if it creates, variables:update if it updates and variables:delete if it deletes. A member who can update but not delete can therefore compose a plan that removes lines, and have the whole apply refused with 401 and The user is not authorized to apply this change to variables in this workspace. Delete those variables from the table, or ask for the permission.
Limits
| Limit | Value |
|---|---|
| Items per apply request | 1000, counting creates, updates and deletes together. Over that, 400 and At most 1000 variables can be applied per request. |
| Baseline size per apply request | 1000. An environment holding more than 1000 live variables cannot be applied from the editor at all, since the baseline covers every one of them |
| Variables per environment | 1000, the fair-use cap that is the same on every plan, checked after the change set is projected. Over it, 400 and This change would leave <count> variables in this environment, and your plan allows 1000. No upgrade moves it; raising it is a support request |
| Variable name | 1 to 100 characters, matching ^[A-Za-z_][A-Za-z0-9_]*$, stored with the casing you type and unique in the environment without regard to case |
| Encrypted value | 1 to 20,000 characters. That ceiling applies to the ciphertext, which is longer than your plaintext, and the API rejects any value that is not an Envless ciphertext envelope |
| Request body | 1 MB. A larger payload is answered with 413 and Request body is too large. before any of the above is evaluated |
Those numbers are per request, not per session, and the editor does not split a change set across requests. A document whose plan exceeds 1000 changes has to be applied in stages, or through several smaller edits.
The bulk endpoints behind the table view are capped lower, at 200 variables per request, for both creating and deleting. The table's import tab sends all of its new variables in one such request, so a .env with more than 200 new names is refused there while the raw editor would take it, up to 1000 changes.
Related
envless push
The same change plan and the same drift check, driven from a local file.
Apply endpoint
The transactional apply as a public API call, with the payload shape.
variable.applied
One event per apply, with the added, updated and removed counts.
Passphrase
The workspace key the editor needs before it can build the file.