Secrets
Store API keys and configuration as environment variables your site reads at runtime.
On this page
A secret is one named value your site reads from its environment, such as a payment provider key, a mail service token or a feature flag. You store it in the Secrets tab and read it in code as process.env.KEY_NAME.
Secrets exist so credentials never live in your source files. A key pasted into a component ends up in the GitHub repository behind your project. A key stored here is encrypted at rest and handed to the machine that runs your site instead.
Open the Secrets tab
Secrets lives behind More in the editor tab strip. Click More, then Secrets in the list on the left. Its address is /dashboard/projects/<your-project>/secrets, so the link is shareable and survives a reload.
Secrets is the first entry in that list, so the first time you ever open More you land on it. After that More reopens whichever of the ten tabs you used last. Tabs and visibility covers the rest of the strip.
Before you add anything the panel reads No secrets with Add one, or drop a .env file.
Add a secret
Click Add secret in the top right of the panel. It opens a dialog with the same fields described below, and stays open after each save so you can add several secrets in one sitting instead of reopening it each time.
Type the name
The KEY field takes the variable name. Use letters, digits and underscores, starting with a letter or an underscore, the way environment variables are normally written: STRIPE_SECRET_KEY, RESEND_API_KEY, NEXT_PUBLIC_SITE_URL. Anything else in the name is replaced with underscores when it saves.
You can also paste a whole KEY=value line into this field. The name lands in KEY and the value lands in value. Pasting several lines adds a row for each one, see Paste several at once below.
Type the value
The value field takes the value itself. A value is stored up to 10,000 characters and a name up to 255.
Choose Secret or Public
The lock icon next to the value opens a two entry menu: Secret and Public. Secret is the default.
Add it
Press Add secret, or press Enter in either field. You get a Secret saved toast, the dialog stays open and both fields clear so you can add the next one, and the list scrolls to the new row and highlights it briefly.
The Add secret button inside the dialog stays disabled while the name is empty, while the name duplicates one you already have, and while the name is one the platform reserves.
- A duplicate name outlines the field in red and reads
A secret with this key already exists. - A reserved name reads
This key is reserved by the system and cannot be used.
Secret or Public
The choice controls how the value is stored, not who can see it in a browser.
| Choice | Icon | How it is stored |
|---|---|---|
| Secret | Lock | Encrypted at rest with AES-256-GCM, decrypted only when you reveal it or when it is handed to your site |
| Public | Globe | Stored as plain text |
Both kinds are injected into your site's environment the same way and both are readable from your server code as process.env. Neither is automatically sent to the browser. Whether a value reaches the browser is decided by the name: a Next.js project exposes a variable to client code only when its name starts with NEXT_PUBLIC_.
Never name a real credential with the NEXT_PUBLIC_ prefix. That prefix is what puts the value into the JavaScript bundle your visitors download, whichever storage option you picked.
Paste several at once
Open Add secret, then copy the contents of a .env file and paste them into the KEY field. The first line fills the KEY and value fields, and every further line becomes its own row underneath, each with a name, a value and an x that removes it. The rows are editable before anything is saved. Pasting into the value field does the same when the clipboard holds more than one line. A single line pasted there is treated as an ordinary value, since a value can legitimately contain an equals sign.
The small plus button at the end of the KEY/value row adds one empty row by hand, for anyone who would rather type several secrets than paste them. Press it as many times as you need; each press adds one more row.
Under the rows the button relabels itself Add 3 secrets and saves them all in one go, and Clear discards the extra rows and keeps whatever is in the first one. Enter saves the same set. If the first row's name is empty, only the rows below it are saved.
Every row is checked before the button enables:
- An empty name reads
A key is required. - A name that another row already uses reads
This key appears more than once below. - A duplicate of an existing secret and a reserved name read the same as they do for a single one.
The Secret or Public choice on the first row applies to every row saved with it. Pasting the same name twice keeps the later value, and pasting a name the first row already holds updates that row's value instead of adding a duplicate.
Text that is not a set of KEY=value lines is pasted as typed, so a name copied on its own still lands in KEY unchanged.
What happens when you save
Saving one secret triggers three things.
Your preview updates straight away. The whole set of variables is pushed to the machine running your preview, its environment is replaced with the new set, and the dev server restarts if anything actually changed. The API Logs stream records a line like Runtime environment synced (12 active). A variable you deleted is removed from the environment in the same push.
Your published site does not change. The live machines were given their environment when you last published, so they keep serving the old value. Publish again for a new or changed secret to take effect. The values are read once per publish and go to both the build and the running server, so a value read at build time is baked into the output of that build.
Everyone else in the project sees it. The list refreshes for every collaborator who has the tab open.
Because values are read at build time as well as runtime, a tracking id or a public URL only starts working on the next publish. Publish a site covers that step.
Read a value back
Values are not write-only. Every row shows eight dots until you click the eye icon on it, which fetches the value and shows it.
For a Public row the value is already in the list, so the eye reveals it with no round trip. For a Secret row the eye decrypts it on the server first, and the icon spins while it does. Clicking the dots themselves opens the value for editing, which reveals it too.
Each row also has two copy icons, one on the name and one on the value. Copying a hidden value reveals it first.
Every accepted member of the workspace can open this tab and reveal every value in the project. Workspace access is implicit in every role and there is no separate secrets permission to withhold, so if someone should not see your production keys, they should not be a member of the workspace that owns the project. See Members and roles.
Edit and rename
Both fields on a row edit in place. Click the name or the value, change it, then press Enter or click away to save. Escape cancels and puts the old text back.
Editing a value keeps the row's existing Secret or Public choice. There is no way to flip an existing row between the two, so to change that, delete the row and add it again.
Renaming an encrypted secret reveals its value first, so the value carries over to the new name. If the reveal is refused or cancelled, the rename is abandoned and the key is left as it was.
Delete
The trash icon on a row deletes it. You are asked to confirm with Delete this secret? and the dialog warns that this permanently deletes the secret and removes it from your site, and that it cannot be undone.
Deleting removes the variable from the preview environment on the next sync. Your published site keeps it until you publish again, so a site that depends on the value keeps working live and breaks on the next build.
To delete several at once, tick the checkbox on each row. A three dots menu appears in the list header with Copy secrets, Download secrets and Delete secrets, each acting on the ticked rows. The checkbox in the header selects and clears every row, managed ones included.
Ticking a managed row into the selection does not stop Delete secrets: it deletes the rest and skips the managed ones, and the confirmation names how many are skipped before you commit. If every ticked row is managed, Delete secrets is disabled with a tooltip explaining managed secrets cannot be deleted, so there is nothing to confirm.
Copy and export
The three dots menu next to Add secret, in the top right of the panel, holds two export entries:
- Copy secrets puts every row on your clipboard as
KEY=valuelines, and you get a Copied toast. - Download secrets saves the same lines as a file named
secrets-2026-08-25.env, dated the day you export.
With rows ticked, the same two entries in the list header menu act on the selection instead of everything. Either action reveals any hidden values it needs first.
Managed keys are left out of both.
An exported .env file is plain text with every one of your production credentials in it. Do not put it in the project folder, do not attach it to chat, and delete it when you are done with it.
Import a .env file
Drag a .env file anywhere onto the panel. An overlay reads Drop to import secrets and the file is parsed and saved on drop. The same import runs from Import secrets in the three dots menu next to Add secret, which opens a file picker instead. A line under the list reminds you of both: Drop a .env file here, or import one from the dots menu, to add several at once.
The parser reads KEY=value lines, ignores blank lines and # comments, accepts a leading export , and strips one layer of matching single or double quotes from the value. It stops at 200 entries.
- A file over 1 MB is refused with
Choose a .env file under 1 MB! - A file with a line that has no
=, or a name that is not a valid variable name, imports nothing and readsNo secrets found,Expected KEY=value lines, one per secret! - Reserved names in the file are skipped, and a toast names how many were skipped. A file made only of reserved names reads
Every key in that import is platform-managed, so nothing could be added!
A file import decides Secret or Public by name on its own: keys starting with NEXT_PUBLIC_ are stored as plain text, everything else is encrypted. Existing keys are overwritten with the imported value. Rows pasted into the form are different on both counts: they carry the Secret or Public choice you picked, and a name that already exists is refused rather than overwritten.
Reserved names
Some names belong to the platform and are refused.
DATABASE_URL
DATABASE_TOKEN
CDN_URL
CDN_PRIVATE_KEY
GITHUB_TOKEN
GITHUB_REPO_URL
WS_TOKEN
PROJECT_ID
CLOUDFLARE_CDN_DOMAIN
PREVIEW_BYPASS_TOKEN
MODULIFY_APP_URL
MODULIFY_API_URLEvery name beginning with __MODULIFY_ is reserved as well. Names beginning with ANTHROPIC_ or CLAUDE_CODE_ are also reserved: those save without an error but are stripped before the set is handed to your site, so a value stored under one never reaches your code.
Managed secrets
Some reserved names appear in your list anyway, with a Managed badge. Hovering it reads "This secret is managed by Modulify and cannot be edited or removed."
DATABASE_URL is the one you will meet first. It appears when your project gets a database and holds the connection string, joined by DATABASE_TOKEN on a site on the newer database. CDN_URL and CDN_PRIVATE_KEY appear for file storage the same way. You can reveal, copy and tick the checkbox on a managed row, but you cannot rename it, change it or delete it, and neither can the AI. It lives and dies with the thing that created it.
Its checkbox works the same as any other row's, including select all in the header, so a managed row can be part of a bulk copy or export. Deleting is the one bulk action that refuses it: see Delete below.
Managed is about ownership, not secrecy. The eye icon works on these rows too, so the database credentials and the private storage key read out in full for any member of the workspace, exactly like a key you added yourself. Site databases covers what is inside each of the database values.
Managed rows sort to the bottom of the list. Public rows sort first, then encrypted ones, alphabetically within each band. The list header counts them, reading 12 secrets, or 3 selected once you tick something.
Find a secret
Once a project holds 8 or more secrets, a search box and a sort dropdown appear above the list. Type part of a name in the search box to filter the rows down to matches. The search only matches the key name, not the value. Clear it with the x inside the box, or delete what you typed, to bring back every row.
A search with no matches reads No matches with Try a different keyword. instead of an empty list.
The sort dropdown next to it holds three choices: Name, Newest first and Oldest first. Name is the default: public rows first, then encrypted ones, alphabetically within each band, exactly as the list is ordered below the threshold. Newest first and Oldest first order by when a secret was added instead, mixing public and encrypted rows together. Whichever sort you pick, a managed secret still sorts to the bottom of the list.
What the AI can do
The AI working in chat can list your key names, create keys, set values and delete keys. It cannot read an existing value back: no chat tool reveals one, so a value you saved is a name to it and nothing more.
That shapes how it works. Ask for a Stripe checkout and it creates STRIPE_SECRET_KEY for you with the literal placeholder value REPLACE_ME, writes the code against process.env.STRIPE_SECRET_KEY, and tells you to open the Secrets tab and replace the placeholder. It will not ask you to paste the real key into chat, and it will not create a .env file in your project. Writing prompts covers that convention.
The same actions are available over MCP as list_secrets, set_secret, set_secrets for a whole .env at once, and delete_secret. Reading a value back needs get_secret or get_all_secrets, which sit behind the separate credentials:reveal scope because they hand the client the real value in plain text.
So an AI can read a value, in exactly one place: a connected client, on a token you gave that scope to yourself. It is unticked when the token form opens, and leaving it unticked means the tools are never even registered for that client. get_all_secrets leaves the managed rows out, the way Copy secrets does, so DATABASE_URL and the storage keys come back only from get_secret, one at a time. See MCP tools and Tokens and scopes.
Secrets and the CMS are different things
Both hold data your site reads, and it is worth being clear about which one a value belongs in.
| Secrets | The CMS | |
|---|---|---|
| Holds | One value per name, for the whole site | Rows in tables, as many as you like |
| For | Credentials, endpoints, configuration | Content: posts, products, team members |
| Reached from code as | process.env.KEY_NAME |
A database query |
| Changes take effect | In the preview at once, live on the next publish | Live at once |
| Who edits it | Workspace members, in this tab | Workspace members, in the CMS tab |
The rule of thumb: if the site would break without it and you would not show it to a visitor, it is a secret. If it is something the site displays and you expect to add more of, it belongs in the CMS.
Limits
A project holds up to 200 secrets. At the cap the Add secret button itself is disabled, with a tooltip that reads Limit reached (200). Contact support for more. An import that would take you past the cap is refused before anything is written.
Names are capped at 255 characters and values at 10,000.
Secrets when a project is copied
Cloning or duplicating a project copies your key names across but not your values, unless the owner turned on Include environment variables in the Share panel. Without it every copied key arrives holding the literal text VALUE_HERE for the new owner to fill in. Managed and reserved keys are never copied. Sharing and cloning covers those toggles.
FAQ
Do I need to publish for a secret to take effect?
For your live site, yes. The preview picks up a change within seconds, but the published machines were handed their environment at publish time and keep the old value until you publish again.
Can I see a value after saving it?
Yes. Click the eye icon on the row. Encrypted values are decrypted on the server for that request.
Can the AI read my API keys?
Not the one in chat. It lists the names, creates keys and deletes them, and no chat tool returns a stored value. A connected AI client can, through get_secret or get_all_secrets, but only on a token you gave the credentials:reveal scope to, which is unticked by default.
Why is DATABASE_URL locked?
It is created and owned by your site database, so its value has to match the database that actually exists. Delete the database and it goes with it. Locked stops it being edited or deleted, not read: the eye icon reveals it like any other row.
Should I put a .env file in my project?
No. A .env in the project directory is committed to the GitHub repository behind your site, which leaks every value in it. That is the reason this tab exists.
Next
- Site databases explains the managed
DATABASE_URLsecret. - Scheduled jobs can point a request header at a key from this tab.
- Custom code and trackers is the common case for a
NEXT_PUBLIC_value. - Publish a site is what carries a changed secret to your live site.