An environment page is where most of the day to day work happens. It lists every variable in one environment, and every value on it is ciphertext until your browser decrypts it locally. Names are stored with the casing you give them and are unique inside an environment without regard to case, so api_key collides with an existing API_KEY instead of sitting beside it. The page lives at https://envless.cloud/dashboard/products/<product-slug>/projects/<project-slug>/environments/<environment-slug>, and the same URL with /raw on the end opens the whole environment as one editable .env document.
Everything on this page is client side encryption. Values are encrypted in your browser before they leave, and decrypted in your browser when you read them, so anything that touches a plaintext value (revealing, searching values, editing a value, exporting a real .env) needs the workspace key unlocked on this device. Unlock it at Settings, Encryption. See Choosing a passphrase.
Two views of the same environment
| View | Path | What it is |
|---|---|---|
| Table | /environments/<slug> | One row per variable, with per row reveal, inline edit, selection and a row menu. |
| Raw | /environments/<slug>/raw | The environment rendered as a .env document you edit as text, then apply as one batch. |
The tabs sit above the search box and carry the current query string across, so a filtered table keeps its filter when you switch back.
Add variables
Creating variables and importing a .env are one modal with two tabs, not two separate flows. Open it with the Add variables button in the page header, or by pressing c anywhere on the page. The shortcut is ignored while focus is in a text field, while a dropdown or dialog is already open, and while Ctrl, Cmd or Alt is held, so it never fights with copy. It is only armed on the table view, for members who hold variables:create on an unlocked project.
The modal's own subtitle states the rule the whole page rests on: values are encrypted in your browser before they leave.
The Editor tab
One row per variable. Each row carries:
| Field | Options | Default |
|---|---|---|
| Name | Free text | Empty |
| Value | Masked input, with reveal and copy buttons. A boolean row swaps it for a true/false select, a number row switches the keyboard to numeric | Empty |
| Type | String, Number, Boolean | String |
| Access | Server, Client | Server |
| Visibility | Secret, Public | Secret |
Add another appends a row, and the trash button removes one. Removing the only row clears it rather than leaving you with none. The submit button counts what it will do, so it reads Create for one row and Create 4 variables for four, and Enter submits it (only on this tab, the Raw tab has no Enter shortcut).
Two duplicate checks run as you type. A name that already exists in this environment is flagged with A variable with this name already exists in this environment., and a name used twice in the batch is flagged on the second row with This name is used by another row above. Both compare without regard to case, so typing api_key where API_KEY already lives is flagged as a duplicate rather than accepted as a second variable. Either one disables the submit button, as does any row with an empty name. An empty value is allowed here and stores an empty value, which the table later renders as empty.
The Editor tab does not check the name against the allowed character set. The server does, and it skips any item whose name is not [A-Za-z_][A-Za-z0-9_]* or is longer than 100 characters. Those skips are not surfaced: the success toast reports only how many were created, so typing MY-VAR into one row of four leaves you with three variables and a toast that says three were created successfully. Check the table after a create if a name was unusual. Casing is never the reason: lowercase names are legal and are stored exactly as you type them.
Paste to expand
Pasting a block of KEY=value lines into a name or value field on the Editor tab expands it into rows instead of dropping the text into one field. The paste is only intercepted when it looks unambiguously like an env block: at least 2 non blank, non comment lines, and every such line matching ^(export\s+)?[A-Za-z_][A-Za-z0-9_]*\s*=. One stray prose line and the paste is treated as ordinary text.
This is the one path that infers a type from the value:
| Value, trimmed | Inferred type |
|---|---|
true or false | Boolean |
Anything non empty that Number() turns into a finite number | Number |
| Everything else | String |
At most 200 rows are kept. If the paste held more, the extras are dropped with Only the first 200 were added. Use the Raw .env tab for larger files.
The Raw .env tab
Three ways to get a file in front of the importer, all landing on the same tab:
Paste
Paste the contents straight into the textarea. Typing in it clears any attached file name, because the text is now yours rather than the file's.
Choose file
The Choose file button in the modal toolbar opens a file picker, reads the file as text and switches to the Raw tab with the file name shown above the textarea.
Drag and drop
Drop a file anywhere on the environment page and the modal opens on the Raw tab with the contents loaded. You can also drop onto the modal while it is already open. The overlay reads Drop your .env file.
Drag and drop accepts one file per drop. A file is taken when its name matches \.env(\..+)?$ case insensitively, so .env, .env.production and .env.local are all fine, or when its MIME type starts with text/, which quietly lets a plain .txt through as well. Anything else is refused with an Unsupported file toast naming what was skipped. Dropping is only enabled for members who can create variables on an unlocked project.
What the importer accepts
The importer parses the text on every keystroke and rebuilds the preview. Its rules are deliberately forgiving:
| Input | Result |
|---|---|
Blank line, or a line whose first non space character is # | Skipped |
A leading export | Stripped before parsing |
NAME=value | Split on the first =, so the value may contain more of them |
NAME="value" or NAME='value' | Quotes removed. In a double quoted value, \n, \r, \t, \" and \\ are decoded |
NAME=value # note | Cut at the first #, so the trailing comment is dropped. There is no such trimming inside quotes |
| The same name twice, including two spellings that differ only in case | Collapsed to one row holding the last occurrence's value, with no warning |
A line the importer cannot read is skipped in silence. A line with no =, an empty name, or a name outside [A-Za-z_][A-Za-z0-9_]* simply never appears in the preview, and nothing tells you it was there. The importer does not report line numbers. If you need a parser that names the broken line, edit the environment in the Raw view instead, which refuses to apply while any line is malformed. Either way, the preview is the source of truth: if a variable you expected is missing from it, the line was dropped.
The per row preview
Every parsed entry becomes a preview row showing Name, Value, Status and an Action select, checked against the variables already in this environment:
| Status | Meaning | Action choices | Default |
|---|---|---|---|
| New | No variable with that name in this environment | Add, Skip | Add |
| Conflicts existing | The name is already here, compared without regard to case | Replace, Skip | Replace |
Set any row to Skip and it is left alone entirely. The header counts the three buckets (3 add, 2 replace, 1 skip) and the submit button spells out the batch: Add 3, Replace 2, or Add 3, replace 2. With everything skipped the count is zero and the button is disabled.
Adds go out as one bulk create. Replaces go out as one update per row, in parallel, and each one is independent: if some fail you get a Variables not replaced toast reading Failed to replace 2 variables., while the ones that succeeded stay saved.
Every variable the Raw import touches is written as type string and secret. A new one lands on Server access, and a replaced one keeps whatever access it already had, because the import never sends that field. Type inference belongs to the Editor tab's paste path only, so importing PORT=3000 from a file gives you a string, not a number. Set the type afterwards from the row menu's Edit, which is also the only place Required and a stored default value can be set. They are not on the create form at all.
Editing the whole environment as text
The Raw view builds one .env document out of the environment, decrypting every value to do it, so it needs the workspace key. You edit the text, and Envless diffs your document against the current variables to produce a plan of creates, updates and deletes, which a confirmation modal shows before anything is written. A name in the document that is not in the environment is a create, a name whose value differs from the decrypted one is an update, and a variable whose name is no longer anywhere in the document is a delete. Removing a line therefore removes the variable. The diff matches names without regard to case, so API_KEY and api_key are the same variable to it.
The six parser diagnostics
This view uses a strict parser, and every problem it finds is reported as Line <n> plus a message, under a heading that counts them (3 lines need fixing before you can apply, or 1 line needs fixing before you can apply). While any diagnostic is present the apply is blocked: the editor says so under the text area, and the confirmation modal keeps its apply button disabled.
| Code | When it fires | Message |
|---|---|---|
missing_equals | The line has no = after a leading export is stripped | No "=" on this line. Write NAME=value, or start the line with # to make it a comment. |
empty_key | There is nothing before the = | Missing a name before the "=". |
invalid_key | The name is not [A-Za-z_][A-Za-z0-9_]* | Names can only use letters, numbers and underscores, and cannot start with a number. |
unterminated_quote | The value opens " or ' and never closes it | This value opens a quote that is never closed. |
trailing_characters | There is text after the closing quote that is not a # comment | Unexpected text after the closing quote. |
duplicate_key | The name was already defined earlier in the document, ignoring case | <NAME> is already defined on line <n>., naming the earlier line |
A line that produces a diagnostic is not guessed at, it is left out of the parse entirely. Blank lines and # comment lines are ignored and never produce one. If your edits parse cleanly but change nothing, you get Your edits do not change any values, so there is nothing to apply. instead.
Reading values in the table
Values are masked as •••••••••••• until you ask for them. The eye button on a row decrypts that one value in your browser and caches the plaintext against the ciphertext it came from, so re-revealing is free until the value changes. Three things a cell can say instead of a value:
| Cell shows | Meaning |
|---|---|
•••••••••••• | Masked, not yet revealed |
empty | Decrypted successfully, and the stored value is an empty string |
Cannot decrypt with current key | The key on this device does not open this ciphertext |
Hovering a row exposes a copy button next to the name (copies the name) and next to the value (decrypts first, then copies the plaintext). Neither the API nor the server is involved in that decryption.
Inline edit
Double click a name or a value cell to edit it in place. It is available to members with variables:update on an unlocked project, and the cell advertises itself with a double-click to edit tooltip. The input selects its contents on open.
| Key | Effect |
|---|---|
Enter | Save |
Escape | Cancel |
An empty name is refused, and so is an empty value: the input turns red and nothing is sent. (Note the asymmetry with the create form, which does accept an empty value.) Saving a value that matches the current plaintext, or a name that matches the current name, closes the editor without a request. Everything else encrypts the new value in the browser and sends a single update, then patches the row in place rather than refetching the list.
The row menu
| Item | What it does |
|---|---|
| Edit | Opens the full variable modal, the only place to set Required and a default value |
| View history | Opens the change history drawer for that variable |
| Copy variable | Copies one NAME=value line with the decrypted value, so it needs the key. No quoting is applied, unlike the .env export |
| Delete | Type to confirm deletion, see below |
A member who can read but not update or delete gets a single history button instead of the menu.
Search
The search box is labelled Search names and values... and it means both. Names are matched client side, case insensitively, on a substring. Values are matched the same way, but only after they have been decrypted, which is the part worth understanding.
When a search is active, Envless decrypts every non deleted variable in the environment in your browser, 8 at a time, and caches each plaintext against its ciphertext so typing another character does not redo the work. Two notices sit above the table when that scan finishes with something to report:
| Notice | When |
|---|---|
Values are locked, so only names are being searched. | The workspace key is not unlocked on this device. Names still match, values are simply not in the index |
<n> values could not be decrypted and were skipped. | The key is unlocked but some values did not open under it |
The input is debounced by 250 ms into the store the URL mirrors, then debounced again by the same amount before it filters the table and starts the value scan, so a search takes about half a second to land. The spinner in the search icon means the input has not settled yet, or the list is revalidating in the background.
Search and sort are mirrored into the URL as q and sort, so a filtered view is a link you can share or reload. Sort accepts name (the default, ascending), updated (newest first, falling back to the created time) and created (newest first). The table is not paginated: every matching variable is rendered.
Bulk actions
The checkbox column appears for members with variables:delete on an unlocked project. Selecting anything opens the selection bar above the table, which also appears on its own when values are revealed, so you always have one click to put them back.
| Control | Effect |
|---|---|
<n> selected chip | Dismiss it to clear the selection |
<n> revealed chip | Dismiss it to hide every revealed value at once |
| Select all on page | Selects every row currently listed. Since the table is not paginated, that is every variable matching the current search |
| Header checkbox | Same thing, and unchecks all when everything visible is already selected |
The kebab button on the bar holds the bulk actions:
| Action | Shown when | Needs the key |
|---|---|---|
| Reveal selected | Not every selected row is already revealed | Yes |
| Hide selected | At least one selected row is revealed | No |
| Copy selected | Always | Yes |
| Copy selected example | Always | No |
| Download selected | Always | Yes |
| Delete selected | The member holds variables:delete | No |
Reveal decrypts the selection as one batch, 8 values at a time, and re-reads the current state before writing the results back, so revealing one group while another is still decrypting does not clobber it. Any value that fails raises The value could not be decrypted. Is this the right workspace key? and the rest still reveal.
Export
The same three export actions appear in two places. In the page header they sit behind the kebab attached to the Add variables button (or behind a plain Actions button when you cannot add variables), and there they cover the whole environment. In the selection bar's kebab they cover only what you selected, and the labels gain a "selected". They are hidden on a locked project and when the environment has no variables.
.env.example, no key required
Copy example writes one bare NAME= line per variable, with a trailing newline. No value is read, nothing is decrypted, and the workspace key is never touched, so this works on a locked workspace and is the safe thing to paste into a repo or a ticket.
DATABASE_URL=
STRIPE_KEY=
PORT=The real .env, key required
Copy and Download produce the actual file, so both decrypt every variable first and both refuse without an unlocked key, prompting Unlock your workspace key first to continue. with an Unlock action on the toast.
The export is all or nothing. If a single value fails to decrypt, no file and no clipboard write is produced at all, and you get the decryption error rather than a partial .env. That is deliberate: a partial env file that looks complete is worse than none.
Downloads are named .env for a whole environment with no name available, and .env.<environment> otherwise, where the environment name is lower cased, anything outside a-z0-9._- becomes -, and leading and trailing dashes are trimmed. A name that sanitizes to nothing falls back to .env.export.
Quoting
A value is written bare unless it needs quoting. It is wrapped in double quotes when it is empty, or when it contains any of:
| Trigger | Notes |
|---|---|
| Any whitespace | Spaces, tabs, newlines and carriage returns included |
# | Otherwise the rest of the line would read as a comment |
" or ' | Either quote character |
= | |
` | Backtick |
$ | Otherwise a shell would try to expand it |
\ | Backslash |
Inside the quotes, \ becomes \\, " becomes \", a newline becomes \n, a carriage return becomes \r and a tab becomes \t. A single quote is left as it is, since it is harmless inside double quotes. Those are exactly the escapes the importer decodes, so a multi line value survives an export and re-import intact.
The export carries names and values and nothing else. Type, access, secret, required and any stored default value are not in the file, and re-importing an exported .env writes every variable back as a secret string. Treat the export as a way to run the app, not as a backup of the environment. For a snapshot you can restore, publish a version instead, see Version.
Deleting
Both delete paths are type to confirm, and they ask for different things:
| Path | Dialog | Type to confirm |
|---|---|---|
| Row menu, Delete | Delete "<NAME>"? / Removes this variable. This cannot be undone. | The variable name |
| Selection menu, Delete selected | Delete <n> variables? / Removes the selected variables. This cannot be undone. | The environment slug |
A selection of more than 200 is refused before the dialog opens, with At most 200 variables can be deleted at once. Clear some of the selection and try again. Clear part of the selection and run it twice.
Limits and things that will stop you
| Limit | Value | Where it bites |
|---|---|---|
| Variables per bulk request | 200 | Enforced in the browser on paste and on bulk delete, and again by the API, which answers 400 with At most 200 variables can be created per request. |
| Variables per environment | 1,000 fair-use cap, the same on every plan | Variables are unlimited on every plan, but the API stops creates at 1,000 in one environment. Past it creates are skipped and you get Some variables were not saved. Contact support to raise the limit. The ones that fit are still created, and no upgrade moves the cap: raising it is a support request. See Plans and billing |
| Variable name | 1 to 100 characters, [A-Za-z_][A-Za-z0-9_]*, unique in the environment without regard to case | Stored with exactly the casing you type, never upper-cased. Server side: on a create a bad name is skipped without a word, on a rename it comes back as an error |
| Stored value | At most 20,000 characters of ciphertext | Server side, measured after encryption, so the plaintext ceiling is lower |
| Locked project | No writes, no exports | A project over your plan limit shows This project is locked and drops the checkbox column, the Add variables button and the export actions |
| Locked workspace key | No plaintext | Reveal, value search, value inline edit, .env copy and .env download all stop. Names, .env.example and history keep working |
Permissions
| Action | Permission |
|---|---|
| See the table and the Raw view | variables:read |
| Add variables, import a file, drag and drop | variables:create |
| Inline edit, edit modal, apply raw changes | variables:update |
| Row delete, bulk delete, and the selection checkboxes | variables:delete |
Every one of them is additionally gated on the project being unlocked. Roles are managed under Role and Access.