Arc Forge Labs Docs v2026.10.25

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:read may 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's org_uid and 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:

    1. Press Generate key. The browser makes 32 random bytes with crypto.getRandomValues and shows them in base64 (44 characters).
    2. 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 variable FIELD_SECRETS_KEY wherever 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.
    3. 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.
    4. 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_KEY is missing, or its value cannot be used (too short), and then the new key replaces that value. When the key works but FIELD_SECRETS_KEY_PREVIOUS is 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 same fs1 format as server/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.

  • 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_PREVIOUS separates 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 in FIELD_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 )" or the wizard; everyone else sees only "Encryption key: set" or "not set". Every wizard step writes an audit event without a value (setup_step, secrets_key_generated, pending_credentials_stored, _promoted, _discarded). API and MCP tokens get a 403 on every wizard route.

Rotating the key

  1. Make a new key (above), for example a new 40-character password from the generator.
  2. Set FIELD_SECRETS_KEY_PREVIOUS to the current key, and FIELD_SECRETS_KEY to the new one. Several previous keys can be listed, separated by commas.
  3. 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.
  4. Check Settings, Integrations: each integration is still set up and connected.
  5. Remove FIELD_SECRETS_KEY_PREVIOUS and 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_KEY and the old XERO_CLIENT_SECRET set, 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.