Integrations
Field connects to other systems a business already uses. Xero is the first
(docs/XERO.md). This page is the part every integration shares: where its
credentials are entered, how Field stores them, who may change them, and how
to rotate the key that protects them.
This guide uses https://<your-field-host> for your installation's address.
Settings, Integrations shows your real values.
Who sees what
- Everyone who can open Settings sees Settings, Integrations: a
catalogue of cards (logo, name, one line), with a search box and
Connected and Browse tabs. A connected card shows a check; owners
see + on the others. The catalogue never shows a credential, a key or
a warning. A card opens the integration: for an owner who has not
connected it, its Connect wizard; once connected, "Connected to ..." with
Configure, and Advanced inside it for credentials, the key's id
and Disconnect. Who changed it, when, and the recent changes are shown to
owners only. The list is
client/src/features/settings/integrations-list.ts: a later integration is one more entry. - Only the people allowed to manage the organisation's integrations (owners today) can enter credentials, connect, replace or disconnect. The server enforces this on every route that changes anything. Everyone else sees the same screen read-only.
- API and MCP tokens (service accounts, and personal tokens with Clerk
sign-in) get no integration settings at all, and can never change them. A
token whose permissions include
integrations:readmay read the status, as a non-owner sees it.
Credentials are entered in Settings
An integration's own credentials, for example a Xero app's client id and client secret, are entered on its screen in Settings, Integrations.
- They are write-only. Field never shows them again. The screen shows only
"Set · last changed by
on ", with Replace and Remove. - They never reach the browser, a log, Sentry or the MCP server.
- Every change writes an audit event with who and when, never the value: credentials entered, replaced or removed, connected, disconnected. The screen lists the recent ones under "Recent changes".
- They are stored per organisation (
integration_credentials, with the organisation'sorg_uidand row-level security). Another installation, or another organisation in one installation, stores its own.
A self-hoster can still put an integration's credentials in the deployment
environment, for Xero XERO_CLIENT_ID and XERO_CLIENT_SECRET. When both are
set they override the ones entered in Settings, and the screen says so. The
Settings screen is the default path.
The key: FIELD_SECRETS_KEY
Field encrypts every stored credential and OAuth token with one key, the
deployment secret FIELD_SECRETS_KEY.
-
It is its own secret: never
SESSION_SECRET, and never derived from an integration's credentials. Rotating a Xero app's client secret therefore loses nothing. -
In a hosted Field, the hosting provider sets this key automatically at provisioning.
-
Make it in Field (the default). Settings, Integrations, the Xero card (or its +), which opens the Connect wizard. When the key is not set, step 3 of the wizard, "Your encryption key", makes it in your browser and shows it once, with Copy:
- Press Generate key. The browser makes 32 random bytes with
crypto.getRandomValuesand shows them in base64 (44 characters). - Under "Where does this go?", choose your host. On Replit: Publishing,
Adjust settings, Production app secrets, Add deployment secret, name
FIELD_SECRETS_KEY, paste the key, Republish. On any other host: save it as the environment variableFIELD_SECRETS_KEYwherever your Field server keeps its secrets, then restart Field. Copy setup prompt copies the same instructions for an assistant; it names the secret and never holds the key. - Save the key in your password manager too, tick the box and press Done. The key leaves the screen, and Field overwrites the key's bytes. A browser cannot erase text from its own memory on request, so the key's text can stay in this tab's memory until you close the page: close it when you are done.
- Copy leaves the key on the clipboard. After you paste it, clear the clipboard, or copy something else. Done also tries to empty the clipboard, but the browser may not allow it.
The wizard makes a key only while
FIELD_SECRETS_KEYis missing, or its value cannot be used (too short), and then the new key replaces that value. When the key works butFIELD_SECRETS_KEY_PREVIOUSis wrong, the wizard says "Your previous key setting is wrong: fix FIELD_SECRETS_KEY_PREVIOUS" and offers no new key, so a working key is never replaced. Credentials entered or removed later (Replace, Remove) forget any credentials still waiting for a key, and waiting credentials older than the stored ones are never used.The key never reaches Field's server: the server cannot store, log or send to Sentry what it never receives. Generate key records only an audit event (
POST /api/integrations/secrets-key/generated, no body: who and when). The browser also encrypts the Xero app's client id and client secret with the new key, in the samefs1format asserver/secrets/box.js(shared/secrets-web.js), and sends only those encrypted values. Field keeps them as pending credentials (integration_setup) until it is republished with the key; then it stores them as the credentials, with an audit event, and the wizard goes on at step 4. If the deployed key is another one, the wizard says so and offers to start again.The key's element is inside Sentry's replay block (
sentry-block), and the "Report a problem" button is hidden while the key shows, so neither a replay nor a feedback screenshot holds it. - Press Generate key. The browser makes 32 random bytes with
-
Fallbacks for self-hosters. In Bitwarden, open the password generator and make a password of 40 characters or more, with upper case, lower case and numbers. Any random string of at least 40 characters works; Field turns it into its 32-byte key with HKDF-SHA256. Use a generated string, never a phrase you made up: the key is only as strong as the string. Leave commas out, because
FIELD_SECRETS_KEY_PREVIOUSseparates keys with commas. A key of 32 random bytes in base64, or 64 hex characters, also works, and is used as it is. To make one on a computer with Node.js:node -e "console.log(require('crypto').randomBytes(32).toString('base64'))" -
Anything shorter than 40 characters that is not such a key is refused (
SECRETS_KEY_INVALID), and the screen says so.Put the key in the deployment's secrets (on Replit, Publishing, Adjust settings, Production app secrets), then restart or republish Field. Never paste it into chat, an issue, a pull request or a file in the repository. Keep a copy in your password manager: without it, the stored credentials and tokens cannot be read.
-
Field uses AES-256-GCM with a fresh nonce for every value, and binds each value to its organisation, integration and field. Each value records the id of the key that wrote it (a hash of the key, never the key).
server/secrets/is the module every integration uses. -
It fails closed. If
FIELD_SECRETS_KEY(or a value inFIELD_SECRETS_KEY_PREVIOUS) is missing, is too short, or cannot read a stored value, nothing is stored or sent. The screen says which, and a connected integration shows "Connect again". Field does not store that state: once the key is fixed, the integration is connected again by itself.
Who sees it: owners see "Encryption key: set (id setup_step,
secrets_key_generated, pending_credentials_stored, _promoted,
_discarded). API and MCP tokens get a 403 on every wizard route.
Rotating the key
- Make a new key (above), for example a new 40-character password from the generator.
- Set
FIELD_SECRETS_KEY_PREVIOUSto the current key, andFIELD_SECRETS_KEYto the new one. Several previous keys can be listed, separated by commas. - Restart or republish Field. At boot it reads every stored value with the key that wrote it and writes it again with the new key. Xero tokens are re-encrypted the same way, under a lock, so a refresh on another server is never lost. A credential an owner replaces during this is kept.
- Check Settings, Integrations: each integration is still set up and connected.
- Remove
FIELD_SECRETS_KEY_PREVIOUSand restart again.
If you set the new key without keeping the old one in
FIELD_SECRETS_KEY_PREVIOUS, nothing is lost yet: integrations show
"Connect again" until you add the old key, then recover. If the old key is
lost for good, the stored values cannot be read: enter the credentials again
and connect again.
Upgrading from the first Xero release
The first Xero release (#94) read the Xero app's credentials only from
XERO_CLIENT_ID and XERO_CLIENT_SECRET, and encrypted its tokens with a key
derived from XERO_CLIENT_SECRET. At boot, Field moves those tokens under
FIELD_SECRETS_KEY:
- with both
FIELD_SECRETS_KEYand the oldXERO_CLIENT_SECRETset, it re-encrypts them, and Xero stays connected; - otherwise it forgets them, and the connection shows "Connect again"
(
LEGACY_TOKENS_UNREADABLE).
Either way, an audit event records it. After the upgrade, the owner can
enter the credentials in Settings and remove XERO_CLIENT_ID and
XERO_CLIENT_SECRET from the deployment, or keep them as the override.
Adding an integration
Use server/secrets/credentials.js for its credentials (status, write-only
set, remove, audit) and server/secrets/box.js for any tokens, with the
integration's name as the binding. Add an entry to
client/src/features/settings/integrations-list.ts (id, name, one line,
logo, where its status comes from, and the view its card opens). Give it a
screen that follows the Xero one: a wizard to connect, then Configure, with
write-only fields under Advanced, owner-only changes, read-only for everyone
else.