Access keys
For a step-by-step walkthrough, see Create and manage access keys.
Access keys authorize inbound API and SDK requests to Cuttlely. Access Keys is one list per workspace for the SDK/Settings key and per-flow API keys; a workspace can have many keys, and many keys can grant the same resource. A principal has a workspace, an owner, permissions, operation scopes, resource grants, caller restrictions and limits. Its credentials are bearer secrets. Rotation creates another credential for the same principal, so policy, counters and audit identity stay together.
Provider credentials remain outbound secrets in Credentials. Public embeds, MCP endpoint tokens, webhook signatures and Dashflows public share links retain their separate transports. Keep privileged access keys on your server, including when using a JavaScript SDK.
Rollout and migration
Section titled “Rollout and migration”Back up the application database and the installation encryption key before upgrading. The encryption key also provides the domain-separated HMAC pepper for new credentials; replacing it invalidates those credentials.
Admin > Server settings > Security & network > Access keys > Access-key enforcement requires the designated instance operator. Legacy installs use an explicit panel claim with host-log proof; only an actual verified host CLI binding supplies automatic default authority. Routine transfer/revoke and host-proven recovery/rebind are in the panel; the CLI is emergency recovery only. Every defined host operator override disables panel lifecycle. Other owners, workspace admins and API keys gain no global authority. Operator onboarding. INBOUND_ACCESS_V2 remains the host override:
| Value | Behavior |
|---|---|
off (default) |
Existing integrations retain their legacy authorization. New principal issuance, policy editing and rotation are unavailable. Revocation remains available. |
shadow |
Serve legacy decisions and compare scoped flow authorization without charging quotas. Aggregate mismatches appear as access-key use events in Admin > Audit log. New principals remain unavailable. |
on |
Enforce operation scopes, resource grants, caller policies and limits. Enable this after checking migration diagnostics and the route matrix below. |
off, shadow and on are the only accepted values. The host environment wins and locks the control, including a host-set off as a lockout escape hatch. An empty host value is treated as unset: the saved admin setting wins, then the default off. Saving takes effect on the next request and persists in the installation’s settings/server.json across restarts; it does not change the host environment. Removing the host override and restarting makes the saved setting effective again. An invalid saved value fails safe to off and is identified in the panel; any invalid nonempty host value retains 503 when rollout is checked. CUTTLELY_IGNORE_ADMIN_SETTINGS=1 ignores the saved mode; an empty ignore flag leaves saved settings enabled. A nonempty host rollout mode still wins over ignore-admin. With the shipped Compose files, set the recovery variable in the host .env (or the file selected with --env-file) and run docker compose up -d --force-recreate with the same file/options used for the install. docker compose restart keeps the old container environment. The panel shows the effective mode and its source (host env, admin setting or default). Every mode change requires a review confirmation, including returning to off. Server settings History records the actor, effective from/to modes and their sources without key material. General Discard, Try it again, settings restore and automatic safe boot preserve the current rollout choice. Shadow compares flow decisions where authorizeFlowAccess is used; it does not simulate quota admission or every management/Harness/Dashflows route. Exercise the route matrix separately and review shadowMismatch, legacyAllowed and scopedAllowed in audit details before opting in.
Build the server, then run node scripts/access-keys-migration-report.cjs against a restored database using the deployment’s normal database environment. It reads only key IDs, workspace IDs, existing bindings and Harness names, and prints aggregate counts. It does not migrate or print secrets. On SQLite and PostgreSQL, startup applies the migration in a single transaction and a failure rolls it back. On MySQL and MariaDB, ALTER TABLE and CREATE TABLE commit implicitly, so a failed migration can leave a partially applied schema; recover by stopping the server and restoring the database and installation encryption key from the backup taken before the upgrade, then fix the cause and restart. MySQL/MariaDB migration paths have not been exercised against a live server in this release’s validation. Startup prints the same aggregate summary. Review orphan bindings, cross-workspace bindings, unresolved Harness names, malformed Harness lists and unsupported bound-flow types before enabling enforcement. A report with requiresReview: true exits with status 2; startup logs the aggregate as requiring review, not a clean summary. Repair these source records on the restored backup and rerun the report before enabling enforcement.
The migration preserves existing bearer values, scrypt verifiers, owners and workspaces. It adds apikeys:grant-all only to the canonical built-in Admin role; custom roles require an explicit grant. It creates exact resource grants from each flow’s existing key binding and resolves the key’s Harness names to stable canvas IDs in that workspace. It never grants all resources. Unresolved names, malformed Harness lists, unsupported flow types, orphan pointers and cross-workspace pointers grant nothing. Malformed lists and unsupported bound types have separate aggregate counters; a bound flow stays private while its records are repaired. Flows with an existing binding become explicitly private; unbound flows become explicitly public. Provider credentials and public share tokens are untouched.
Existing principals have a migration-only compatibility marker and no newly imposed expiry. Their existing management permissions continue to work through the legacy exception; their bound prediction resources and Harness lists remain restricted. Dashflows stays session-only in off and shadow; migrated keys cannot enumerate, publish or refresh dashboards. With enforcement on, keys use only the registered Dashflows read/report/refresh routes. In off and shadow, Harness access uses the original Harness-name allowlist, including an empty list granting none; a migrated grant from a canvas’s key binding does not widen that list. The owner’s current workspace access is checked on every request. Replacing a legacy binding updates the binding and its derived grants in one transaction. A bound flow’s type change moves its derived grant inside the same conditional row transaction; a draft, CAS conflict or grant-write failure leaves both unchanged. Existing flow writes match both canvas ID and authenticated workspace. The former legacy key loses the derived flow grant; independently edited policies and independently named Harness grants remain independent. Ordinary canvas, restore and transport-setting writes preserve concurrent access decisions. An ordinary save holding a stale flow type returns 409 rather than restoring that type with mismatched derived grants. Saving a policy replaces compatibility with the explicit scopes and resource grants shown in the editor. It does not change the existing bearer value. Rotate only when you choose to.
To roll back enforcement, save Off in the panel, or force INBOUND_ACCESS_V2=off on the host and restart. This restores legacy principals’ old authorization decisions and refuses scoped principals. Keep the added tables and columns. A schema downgrade cannot reconstruct modern credentials as legacy secrets. Keep the legacy adapter and name columns for at least two minor versions after measured parity; their removal is a separate change.
Create and manage a principal
Section titled “Create and manage a principal”With enforcement enabled, open Access Keys. Start with Call one chatflow/agentflow or Read-only API and select resources. The call preset includes prediction, required view permissions and referenced Harness access. Raw controls are under Advanced. A server preview checks prerequisites and offers explicit additions before save. Direct API creation without scopes/resources still grants nothing; the beginner form requires a usable selection.
Resources are grouped as Chatflows, Agentflows, Harnesses and Dashflows. Grants use stable IDs. Deleting a Harness and recreating its name does not inherit its grants. Executing a flow that calls a Harness also needs a grant for that Harness and the harness:run scope; the editor shows referenced Harness names. Feedback and file operations on that flow do not require Harness execution authority. A grant to a public flow does not make it private. Set the flow’s API Endpoint access control to Private (selected access keys) when the flow must require a bearer. The dialog waits for a known rollout mode before enabling access changes; a failed lookup cannot fall back to legacy binding writes.
Custom assistants (ASSISTANT) are outside the exact resource-grant types in this release. They are omitted from the resource picker, and a migrated bound assistant increments skippedFlowTypes and requiresReview without receiving a grant. Its retained key works in off/shadow but private prediction returns 401 with enforcement on; management refuses an exact assistant grant with 403. Keep enforcement off or in shadow mode for installations relying on these private integrations. The custom assistant’s API dialog labels this limitation. In off/shadow, it preserves an existing private binding but does not offer adding or replacing one, because those writes are unsupported. Existing bindings can still run; selecting Public removes the binding. Ordinary Chatflow legacy binding controls remain available.
All resources in this workspace includes present and future resources in that workspace only. It requires apikeys:grant-all and a separate confirmation. It replaces only resource selection. Scope, permissions, caller policy, expiry and limits still apply. Owners and built-in Admins hold this permission; other roles must receive it explicitly. Keys cannot manage or issue descendant keys: management writes require an interactive session and current RBAC, including reauthorization of the target policy and each granted resource family before rotation or revocation. Workspace-wide grants require authority for every family the scope can exercise. Management mutations require a nonempty key ID (412 if omitted). Creation rechecks current role and resource authority after acquiring the writer mutex. Lifecycle writers reload the principal and grants and recheck authority under the writer mutex and transaction; a policy change while waiting returns 409 without minting, overwriting or revoking.
The default expiry is 90 days. Presets include 30 days, 90 days, a custom absolute UTC time and explicit no expiry. Environment (live or test) and service name are descriptive metadata, not separate resource environments.
Creation and rotation reveal a secret once. New credentials use ctu_live_ or ctu_test_, a public token ID and 32 random bytes. Only an indexed HMAC digest, display prefix and final four characters are stored. Lists never return the digest, legacy verifier or bearer value.
Revoke a principal to block new requests using any of its credentials. Revoke an individual credential to retire only that value. Rotation defaults to one hour of overlap; choose 0 for immediate retirement or up to 86,400 seconds. Repeated rotations never extend an earlier credential’s cutoff. Revoked credentials do not become valid during grace. Changing principal expiry never revives or extends an already issued credential, including a retained legacy bearer. Policy edits cap modern credential rows and the retained legacy bearer at the earliest old or replacement principal expiry in the same transaction as the principal update. Extending the principal before or after that cutoff cannot extend the issued bearer; rotate to inherit the new expiry. Disabling and re-enabling a principal is reversible, while credential expiry and revocation remain irreversible. Authentication is refreshed immediately before the first charged admission, including when preflight ran before a slow request body. Uncharged scope/resource prerequisites and additional route permissions are rechecked against that refreshed principal before quota is consumed. Downstream RBAC uses the refreshed key permissions. Admitted streams and background runs may finish; new requests see the changed policy without a cross-request cache.
Caller policies and counters
Section titled “Caller policies and counters”With enforcement on, access-key preflight returns a flow’s embed-origin refusal as JSON before body parsing, even with Accept: text/event-stream or a streaming field in a JSON or multipart body. At the later prediction middleware/controller, ordinary requests return JSON and recognized streaming requests return text/plain; charset=utf-8. A multipart streaming field is unavailable before upload parsing, so that field alone cannot select plain text in the middleware. No stream starts on refusal; configured messages are literal text.
Origins are exact normalized HTTP(S) origins, with no paths, wildcards or Referer fallback. Adding an origin allowlist defaults to refusing a missing Origin. You may explicitly allow server callers without an Origin. An empty origin list has no origin restriction. CORS and Origin never replace the bearer.
IP rules accept exact IPv4/IPv6 addresses and CIDRs. An empty list has no IP restriction. Checks use Express req.ip and the deployment’s TRUST_PROXY configuration, never arbitrary forwarded headers. Leave proxy trust off unless the deployment has a known trusted proxy boundary.
RPM is principal-wide, with operation buckets, and is shared by rotating credentials. Its default is 60; daily and monthly quotas default to 0 (unlimited). Daily and monthly quotas use UTC calendar windows. A limit of 0 explicitly means unlimited. Count one accepted HTTP operation, including one streaming request, rather than tokens, chunks or nested Harness tasks. Middleware/controller rechecks do not charge again. Authorization, including route RBAC, canvas/reference checks and run or report ownership, completes before quota admission commits. Authorization refusals consume no quota. Application errors after complete authorization remain charged, even when they return 403; admission is not refunded based on response status.
Current UTC day/month totals and per-operation counts are persisted atomically on a dedicated SQLite connection (or the primary transaction on other databases) so an unrelated outer rollback cannot erase an admitted charge. SQLite admission must run before an application transaction opens: an active primary SQLite transaction returns 503 before the sibling can write. The sibling inherits the configured busy timeout and WAL option; WAL enabled during startup persists on the database file. Old windows are removed; no request payload or per-request history is stored. SQL quota failures fail closed. Quota headers describe committed admission only. A full minute-bucket cache refuses new buckets while existing buckets retain their limits and admission. A rejected limit returns 429 and Retry-After. Admitted requests include reset timestamps and, for configured limits, remaining minute/day/month quota headers. Minute admission uses bounded process memory and resets on restart. Configure CUTTLELY_API_KEY_INSTANCES=1; any other value refuses admission. Do not promise globally exact limits across replicas: the shared Redis limiter is a follow-on.
With enforcement on, caller policy and exhausted-budget checks run before JSON parsing for both newly minted and retained bearer values, including adapter X-API-Key headers. Conflicting adapter credential headers are refused before parsing. Preflight does not charge quota. Final admission waits until body-dependent scopes are known. URL-resolved bearer prediction, attachments and vector admission also run before parsers and file writes. Anonymous private prediction is refused before the global parser; attachment/vector cookie-session checks retain their route guard before multipart storage. Multipart prediction requires predict, upload and stream scopes up front so a later multipart field cannot widen access after storage starts. JSON streaming additionally requires stream and is refused before SSE headers. Body-resolved feedback and lead operations are checked after the bounded JSON parser. With enforcement on, lead creation resolves the parent flow and requires flow access; anonymous callers can create leads only for explicitly public flows. A supplied bearer must pass its scope, grant, caller-policy and quota checks even on public flows. off and shadow preserve legacy anonymous lead collection, including private flows. Existing flow/IP prediction-attempt limits also count preflight refusals; they are separate from accepted-operation key quotas. Harness/report caps still apply.
Lifecycle changes and policy/resource diffs appear in Admin > Audit log. Usage/refusal events are normally deduplicated for five minutes per principal, operation and outcome in each process; shadow mismatches additionally distinguish the resource and actual operation. Shadow probes do not emit enforced-refusal events; a failed comparison lookup does not suppress the next audit attempt. Audit persistence remains best effort and never changes the served legacy decision. Bounded dedupe caches evict their oldest entries when full, allowing additional audit events rather than suppressing new events from other principals. They contain IDs and policy data, never bearer values, request IPs or origins, prompts, files or report contents.
With enforcement on, named Harness runs (sync, streaming and async), Harness prediction and Harness cards/tools retain the authorized canvas IDs in their caller context. Each dispatch requires its expected ID, and the queue retains both the expected ID and the identity map for nested calls. A missing expected ID is refused; runtime re-resolution cannot substitute an ungranted same-name canvas. Scoped metadata writes also refuse a concurrent type change before a Harness can be renamed. Dashflows account reports retain the same published-Harness pin through background execution. Refresh authorizes the editable definition’s Harness and binds materialization to that revision and the authorized Harness canvas record; a concurrent dashboard edit or same-name Harness recreation returns 409 before any specialist tool runs. Editing a draft does not change which Harness a published account report requires.
API route matrix
Section titled “API route matrix”Authorization requires an active credential, the resource’s owning workspace, current RBAC, the operation scope and a resource grant. A public flow with a retained legacy binding still requires its key. Invalid, expired or revoked credentials are 401; authentication-store outages are 503; absent scopes/grants or refused caller policy are 403; limits are 429. Workspace and resource refusals use 401, 403 or 404 depending on the route, including concealed missing Harnesses. Existing message/feedback and vector lookup status differences remain: missing IDs can return 404 where an existing forbidden row returns 401 or 403. This change does not eliminate those inherited existence signals. A supplied invalid bearer never falls through to public access when enforcement is on. Orchestrate and A2A preflight refusals use their adapter error envelope (error.message, error.type, error.code), before parsing a body or starting a stream. Signed-in sessions retain their existing role checks. The legacy verify handler retains 412 for a missing bearer and 500 with exception text on store failures outside enforcement preflight; it does not use resource-admission error normalization.
| Route family | Scope and resource |
|---|---|
POST /api/v1/prediction/:id, Orchestrate and A2A adapters |
predict and the exact flow grant; stream for streaming; target Harness canvases and referenced Harnesses also require harness:run and execution RBAC, including current owner permissions |
Attachments, external vector upsert (POST /api/v1/vector/upsert/:id) and authenticated file reads |
upload and the parent flow grant; existing conversation ownership and exact expiring signed-file links still apply |
POST /api/v1/vector/internal-upsert/:id |
Interactive session in the flow’s owning workspace; refused for API keys in every rollout mode |
| Feedback reads/writes | feedback:read / feedback:write and the parent flow grant |
| Chat-message reads/deletes/abort and leads | feedback:read / feedback:write, the parent flow grant and existing route RBAC |
| Flow item/draft/version reads and flow name/category edits (Harness rename stays session-only) | api:read / api:write, the exact resource grant and existing route RBAC |
| Flow streaming/upload capability reads | api:read and the exact flow grant |
| Harness runs, async starts, status and result | harness:run / harness:read, the stable Harness canvas grant and existing run-ownership checks |
| Dashflows published snapshot, refresh status and owned report reads | dashflow:read and the dashboard grant |
| Dashflows refresh and account-report generation | dashflow:report, dashboard grant, referenced Harness grant and harness:run |
| Access-key management | Interactive session, apikeys:view/create/update/delete; additional apikeys:grant-all for workspace-wide grants |
| Unregistered/scoped workspace list APIs, scheduler changes and public-access changes | Refused for new scoped principals; explicit legacy management compatibility remains |
| Public embeds and Dashflows UUID share links | Existing public transport, without a required bearer; existing public abuse/report limits remain |
Harness interpretation diagnostics (GET /api/v1/harnesses/:name/runs/:runId/diagnostics) require a signed-in author. API keys are refused before quota admission. Keyed Dashflows refresh-status reads use view permission; refresh and report generation retain update permission. An account must be in the published snapshot before report admission is charged.
Harness canvas item, draft and version reads use api:read, the exact Harness canvas grant and view RBAC. They retain the granted Harness identity map for downstream canvas checks without requiring harness:run. Draft reads still require grants for Harnesses referenced by the saved draft. Read authority does not permit execution, draft writes or publication.
The registry is closed: adding a handler beneath a route prefix does not automatically expose it to scoped keys. New management operations must be explicitly registered. Management scopes do not imply prediction or Harness execution. A scoped key cannot change execution configuration, publish/restore a canvas, change a schedule, change public/private access or legacy bindings, or invoke draft test/check routes through a management scope. Metadata edits bypass the canvas-save path so an existing schedule cannot be re-enabled as a side effect.
Management requests
Section titled “Management requests”The existing /api/v1/apikey path remains stable. GET /options lists the operations, resource choices and privileged-grant availability for the signed-in actor. POST /validate checks a proposed policy and returns plain-language missing prerequisites, canFix and a suggested policy/resources/permissions body without creating or changing a key. Save rechecks authority and dependencies; preview is not authorization to issue a secret. POST / creates a principal; PUT /:id replaces its policy and grants. The body includes keyName, permissions, accessPolicy, resources and expiresAt. Use explicit null for no expiry; omit it on creation for the 90-day default. Each resource contains resourceType and resourceId. accessPolicy.allResources: true also requires top-level confirmAllResources: true. Updates replace the policy and grants; send the complete desired body, including expiry. An omitted expiry uses the 90-day default on update too; it does not preserve the previous expiry.
POST /:id/rotate accepts graceSeconds. POST /:id/credentials/:credentialId/revoke revokes a credential (legacy identifies the compatibility credential). DELETE /:id soft-revokes the principal and preserves audit history.
Deleting a flow retires its exact flow grants in the same transaction, scoped to principals in its workspace. Recreating the resource with the same or a new ID requires an explicit new grant. Existing policies can still contain grants for unavailable resources, including flows deleted before this cleanup existed. The policy editor shows their type and ID with a Remove grant action. Remove them explicitly to save the remaining policy; this action cannot select a replacement or grant access. Other grants remain selected, and the server still rejects unavailable or unauthorized grants.
Dashflows reports
Section titled “Dashflows reports”Dashflows remains Experimental and off by default. With it enabled, use the dashboard ID with a server-side bearer:
GET /api/v1/dashflows/:id/publishedreads the published snapshot.POST /api/v1/dashflows/:id/accounts/:accountId/reportstarts a report only for an account in that published snapshot and returns202withrunId. The authorized Harness canvas ID and workspace accompany runtime and queue execution; a same-name replacement is refused before tools or model calls. A report already admitted still counts once if execution later refuses the replacement.GET /api/v1/dashflows/:id/reports/:runIdreads that principal’s report for that dashboard. Ownership receipts expire after seven days; another principal or dashboard cannot borrow the run ID. Receipts currently retain the run ID without its original Harness: republishing onto a different Harness can make older reports unavailable. If receipt persistence fails after starting a report, the admitted run can finish without a readable receipt.
Public UUID share URLs retain their existing report-ownership and abuse controls. They do not require both a share token and a bearer.
Nested same-server calls
Section titled “Nested same-server calls”Execute Flow, Chatflow Tool, Agent as Tool, and Harness tools on the same server invoke the target in process as the parent run’s principal. Session and owner callers may reach a Private target in the same workspace. An access-key parent must hold the target’s exact resource grant and required scopes (predict for every target; Harness targets also need harness:run plus view, with referenced Harnesses also granted). A key granted only on the parent cannot reach another Private flow or Harness through that parent. Cross-workspace targets are refused. Anonymous external /prediction on Private remains 401; Public unbound flows stay anonymously callable. A remote Base URL on the card keeps the HTTP path and uses only that card’s credential; the parent bearer is not forwarded. Nested access-key checks reuse the HTTP legacy replacement-binding rule: a retained migration grant does not override a flow bound to a different key. Nested admission does not consume a second HTTP quota charge. Harness gateway caller context is bound to the exact Harness and chat ID. Concurrent chats retain their own principals, nesting depths and execution callbacks, including signed-in Try it alongside access-key runs. The runtime supplies scope on every tool bridge and overwrites model-supplied scope. Missing, unknown or invalid scope fails closed without a card-credential fallback. Reusing a chat ID that is still running refuses only the new run. Other chat IDs are unaffected. Beginner wording: Calls between flows and Harnesses.
When CUTTLELY_HARNESS_QUEUE=external, same-server nested authorization runs in the separate Harness worker using its application database connection. Key lookup, current grants, referenced Harness resolution and owner permissions all use that connection. The worker rechecks that the principal is active and that the workspace, target grants, scopes and legacy replacement binding permit the call, including the owner’s current permissions for migrated legacy keys. An authorized same-server Harness canvas target then runs in process on the worker’s own runtime, without the web process. Pinned tools in that target keep the worker’s database connection, the same caller and the incremented nesting depth for later nested calls. Model-chosen tool calls in a Harness that is itself running as a nested call on the external worker cannot call another same-server flow and are refused with caller_required; use inline queue mode when a Harness called from another Harness must call further private flows. Missing or revoked keys remain authentication refusals, and missing grants remain 403.
Verification and follow-ons
Section titled “Verification and follow-ons”node --test scripts/access-keys.test.cjs scripts/access-keys-production.test.cjs scripts/access-rollout-admin.test.cjs scripts/breakglass-operator.test.cjs exercises the real SQLite migration, lifecycle, concurrent SQL quotas and management API. It also checks portable up/down fixture plans for SQLite, Postgres, MySQL and MariaDB. The production-entry fixtures use the real account-report guard, background service, harness runtime and serialized worker retrieval, and the real conditional updateChatflow writer, including grant-failure rollback. They also check authorization refusals without quota consumption, charged application errors after authorization, off/shadow assistant binding controls, and modern credential expiry non-revival through management and authentication with a fixture clock. Server unit/HTTP tests cover the authorization matrix, caller policy, upload ordering and transport error statuses. The full repository gate is pnpm verify on Node 24.
The rollout-admin production fixture uses actual App.config(), real interactive session cookies, SQL role checks, disposable SQLite and persisted settings. Fixture wiring substitutes the DataSource and running-app accessors, settings dependencies and platform identity; prediction buildChatflow is replaced after admission. Database connection routes receive synthetic PostgreSQL driver responses, so these checks do not verify live PostgreSQL connectivity, a running database switch or a process restart. It covers default-off activation, rejection of workspace-admin and bearer-only writes, reload persistence, host locking, invalid-value behavior, compatibility, quota counts and secret-free settings history. Real UI component tests cover each mode transition and locked-source display.
Shared Redis admission for multiple API replicas, browser delegation, exports/alerts, safe-boundary termination of already-admitted runs and removal of the legacy adapter are follow-ons. They are not implemented by this change. The single-instance implementation writes accepted quota counters synchronously for exact admission; asynchronous telemetry batching is a performance follow-on, not a relaxation of quota enforcement.