Dashboard

Variables in the dashboard

Add, import, edit, reveal, search, export and delete variables from the environment page, and what each of those actions needs from the workspace key.

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

ViewPathWhat it is
Table/environments/<slug>One row per variable, with per row reveal, inline edit, selection and a row menu.
Raw/environments/<slug>/rawThe 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:

FieldOptionsDefault
NameFree textEmpty
ValueMasked input, with reveal and copy buttons. A boolean row swaps it for a true/false select, a number row switches the keyboard to numericEmpty
TypeString, Number, BooleanString
AccessServer, ClientServer
VisibilitySecret, PublicSecret

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, trimmedInferred type
true or falseBoolean
Anything non empty that Number() turns into a finite numberNumber
Everything elseString

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:

InputResult
Blank line, or a line whose first non space character is #Skipped
A leading export Stripped before parsing
NAME=valueSplit 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 # noteCut 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 caseCollapsed 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:

StatusMeaningAction choicesDefault
NewNo variable with that name in this environmentAdd, SkipAdd
Conflicts existingThe name is already here, compared without regard to caseReplace, SkipReplace

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.

CodeWhen it firesMessage
missing_equalsThe line has no = after a leading export is strippedNo "=" on this line. Write NAME=value, or start the line with # to make it a comment.
empty_keyThere is nothing before the =Missing a name before the "=".
invalid_keyThe 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_quoteThe value opens " or ' and never closes itThis value opens a quote that is never closed.
trailing_charactersThere is text after the closing quote that is not a # commentUnexpected text after the closing quote.
duplicate_keyThe 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 showsMeaning
••••••••••••Masked, not yet revealed
emptyDecrypted successfully, and the stored value is an empty string
Cannot decrypt with current keyThe 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.

KeyEffect
EnterSave
EscapeCancel

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

ItemWhat it does
EditOpens the full variable modal, the only place to set Required and a default value
View historyOpens the change history drawer for that variable
Copy variableCopies one NAME=value line with the decrypted value, so it needs the key. No quoting is applied, unlike the .env export
DeleteType to confirm deletion, see below

A member who can read but not update or delete gets a single history button instead of the menu.

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:

NoticeWhen
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.

ControlEffect
<n> selected chipDismiss it to clear the selection
<n> revealed chipDismiss it to hide every revealed value at once
Select all on pageSelects every row currently listed. Since the table is not paginated, that is every variable matching the current search
Header checkboxSame thing, and unchecks all when everything visible is already selected

The kebab button on the bar holds the bulk actions:

ActionShown whenNeeds the key
Reveal selectedNot every selected row is already revealedYes
Hide selectedAt least one selected row is revealedNo
Copy selectedAlwaysYes
Copy selected exampleAlwaysNo
Download selectedAlwaysYes
Delete selectedThe member holds variables:deleteNo

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.

bash
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:

TriggerNotes
Any whitespaceSpaces, 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:

PathDialogType to confirm
Row menu, DeleteDelete "<NAME>"? / Removes this variable. This cannot be undone.The variable name
Selection menu, Delete selectedDelete <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

LimitValueWhere it bites
Variables per bulk request200Enforced 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 environment1,000 fair-use cap, the same on every planVariables 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 name1 to 100 characters, [A-Za-z_][A-Za-z0-9_]*, unique in the environment without regard to caseStored 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 valueAt most 20,000 characters of ciphertextServer side, measured after encryption, so the plaintext ceiling is lower
Locked projectNo writes, no exportsA 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 keyNo plaintextReveal, value search, value inline edit, .env copy and .env download all stop. Names, .env.example and history keep working

Permissions

ActionPermission
See the table and the Raw viewvariables:read
Add variables, import a file, drag and dropvariables:create
Inline edit, edit modal, apply raw changesvariables:update
Row delete, bulk delete, and the selection checkboxesvariables:delete

Every one of them is additionally gated on the project being unlocked. Roles are managed under Role and Access.