Keys that never live in your code.

A key pasted into a source file is one careless share away from being public. Hatchable removes that step. Your app declares which keys it needs, you paste each value once into a form the platform owns, and the value is fetched from encrypted storage at request time.

— what your AI actually does

Your agent names the key. You supply the value.

The declaration is the part that is safe to write into a file, so that is the only part your agent writes. The value goes somewhere your agent cannot reach and your project files never mention.

you, to your AIPost to our Slack channel whenever someone books a call
hatchable.toml
[[secret]]
key         = "SLACK_WEBHOOK_URL"
description = "Incoming webhook for #bookings."

# scope defaults to this one project
# required defaults to TRUE, so the app
# asks for this before it will run
api/bookings.js
import { config, db } from 'hatchable';

export const access = 'public';
export const methods = ['POST'];

export default async function (req, res) {
  const { name, slot } = req.body;
  await db.query(
    `INSERT INTO bookings (name, slot)
     VALUES ($1, $2)`,
    [name, slot]
  );

  // resolved server side, at request time
  const hook = await config.get('SLACK_WEBHOOK_URL');
  await fetch(hook, {
    method: 'POST',
    body: JSON.stringify({ text: `Booked: ${name}` }),
  });

  res.json({ ok: true });
}
search the whole project for that key and you find its name. never its value.
— what you get

Everything a key needs to stay a secret.

One store covers all of it: the provider keys behind every AI call, the signing secret that proves an inbound webhook is genuine, and the tokens behind an account you connected once.

Declared

Named in the manifest, pasted in a form

A [[secret]] block names the key and says what it is for. Nothing else. Required is the default, so a key your app genuinely needs cannot be quietly forgotten and discovered later by a customer.

The file knows there is a webhook. It does not know the webhook.

Project scope

Scoped to one app

The default. The value belongs to this project and no other, and it is read back only by this project's code, only while a request is running. Right for anything specific to one app: a webhook URL, a signing secret, an internal token.

The Slack hook in your booking app stays in your booking app.

Account scope

Set once, found by everything you build

Add a key at the account level and every project on your account can use it without you pasting it again. These never enter app code at all: the platform attaches them to the outbound call itself, which is what makes them safe to share across projects you forked from someone else.

One AI key behind your reading log, your expense tracker, and your Friday digest.

Per-user scope

Everyone brings their own

In an app with sign-in, each person can store their own key against their own account. Your code asks for the value on behalf of whoever is making the request, and the platform hands over that person's. Their usage lands on their bill, not yours.

Every teacher who copies the worksheet generator plugs in their own key.

Storage

Encrypted, and never handed back

Values are encrypted at rest in all three stores. No endpoint returns one. The console tells you whether a key is set, when it changed, and its last four characters, which is enough to check you pasted the right one and not enough to be worth stealing.

A page you can screen-share without thinking about it.

Setup

The app asks for what it is missing

When a required value has not been set, the read raises a setup error instead of a blank 500. In the browser, the platform catches it, shows a paste box for that one key, saves it, and retries the request that failed. Nobody has to go hunting for a settings page.

The missing key becomes a question, at the moment it matters.

Settings

The knobs that are not secrets

Headlines, colours, a list of links, an image, a switch. Declare them with [[config]] and the console renders a form for them, validated against what you declared. Saves take effect on the next request, with no redeploy and no rebuild.

Change the tagline on the menu page from your phone.

Connections

Sign in once, not for every call

For services with a proper login, declare an [[api]] block and the platform runs the connect flow for you: the redirect, the approval, the token exchange, the refresh before expiry. Your code makes an ordinary call and never holds the token.

Connect the Notion workspace once. Every page read after that just works.

— the details

The spec sheet, in plain terms.

Declared inOne [[secret]] block per key in hatchable.toml, carrying the key name plus an optional description, scope, default and allowed-values list.
requiredDefaults to true. Declaring a key is taken as saying the app needs it. Genuinely optional integrations write required = false on purpose.
ScopesProject (the default), account, or per-user. Per-user needs sign-in switched on, because there has to be somebody to scope the value to.
Stored asEncrypted at rest, in a separate store per scope. No API returns a stored value to a client, and secret values are never rendered into a page.
Read in codeawait config.get('KEY'). Resolved on the server in one order: the current user, then the project, then the account, then the declared default. First hit wins.
Shared scopesAccount and per-user values are attached to the outbound call by the platform and are never readable from app code. That is what makes a key you set once safe to leave in place across forked projects.
Missing and requiredThe read raises a typed setup error naming the key. The browser turns it into a paste box and retries; a background job records it in the logs with a link to set the value.
Raw accessSome packages insist on reading the environment directly. An expose = true flag allows it for project-scope values only. On account or per-user scope the deploy is rejected with an explanation.
Settings, not secrets[[config]] declares owner-editable fields: text, number, boolean, email, url, colour, image, a choice from a list, or a repeatable list of rows. The console renders and validates the form.
Webhooks inInbound signatures verify against the exact bytes received, compared in constant time, with a replay window. The signing secret is a declared secret like any other, and the endpoint can live on a domain you own.

Behavior current as of August 2026 and enforced by the deploy validator, not by convention. All three scopes are included on every plan, including free.

— things people ask for

Say it like this.

You do not need to know which block is which. Say what you want to be true and your agent picks the right one.

a new key"Add Stripe to this, and keep the key out of the code"
who pays"Let each person use their own key so I am not paying for everyone"
setup"Ask me for the token the first time the app needs it"
settings"Let me edit the headline and the colours without a redeploy"
webhooks"Check the signature on the GitHub webhook before you trust it"
connect"Connect this to my Notion and pull the pages in"
— common questions

Asked and answered.

Where does the key actually live?

In encrypted platform storage, in one of three stores depending on the scope you chose: the project, your account, or an individual signed-in user of your app. Nothing writes it into your project files, and no endpoint a browser can reach will hand it back. The console shows you whether a key is set and its last four characters, never the key itself.

Does my AI assistant ever see the key?

No. Your agent writes the declaration, which is the name of the key and a note about what it is for. You paste the value yourself, into a form the platform owns. That split is the whole design: the part that gets written into files is the part that is safe to write into files.

What happens if a required key has not been set yet?

The read fails with a typed setup error rather than a mystery crash. A small script the platform injects into every project catches it in the browser, shows a paste box for that one key, saves it, and retries the request that failed. In a background job with nobody watching, the same error surfaces in your logs with the key name and a link to set it.

Can each person who uses my app bring their own key?

Yes. Declare the value at user scope and every signed-in person stores their own. Your handler asks for it on behalf of a specific user and the platform looks up that person's value. Their usage lands on their own bill, which is what makes a shared tool viable when you do not want to pay for everyone.

What is the difference between a secret and a setting?

A secret is something that would be dangerous to show anyone: an API key, a signing secret, a token. A setting is something you want the owner of the app to change: a headline, a colour, a list of links. They use different blocks and different storage. Settings render as a form in the console and save without a redeploy. Secrets never render at all.

Is any of this a paid feature?

No. All three scopes work on every plan, including free, and there is no per-key charge or add-on to switch on. It is part of what a project is.

The full field reference lives in the secrets documentation, with the settings side in the config reference.

— free to start, no card

Stop pasting keys into files.

Connect the AI you already use, describe the app you want, and the keys it needs are asked for properly instead of typed into a source file.