Skip to Content
Polyant is open source under AGPL-3.0 — star us on GitHub.
How-toManage Per-Instance Secrets

Manage Secrets

On this page

Polyant stores per-instance secrets (API keys, tokens, bucket names) in the instance_secrets table, encrypted at rest with AES-256-GCM. Values are never returned by the API - only the list of configured keys.

This guide walks through listing, creating, updating, and deleting secrets, plus rotating the master encryption key.

Managed services. With managed services the keys of the AI and speech providers an agent chats, embeds, transcribes and speaks with are not secrets: they are held by named provider connections, bound to the agent’s roles. The secret routes refuse to write them. Everything else on this page - tool keys, LangSmith, object storage, inbound-auth keys - works the same in both builds.

Prerequisites

  • An engine running with ENCRYPTION_KEY set in the environment.
    • The key MUST be 64 hex characters (32 bytes, AES-256). Generate one with openssl rand -hex 32.
    • Changing this value invalidates every existing encrypted row - plan ahead (see Rotate the master key below).
  • An existing instance, identified by its slug.
  • A valid session cookie or Authorization: Bearer <token> header (the admin API is authenticated).

Step 1 - List the configured secret keys

curl -s http://localhost:4000/api/instances/my-assistant/secrets \ -H "Cookie: authjs.session-token=$TOKEN" | jq

Response (well-known keys are always listed; configured: false means the row does not exist yet):

{ "secrets": [ { "key": "openai_api_key", "configured": true }, { "key": "anthropic_api_key", "configured": false }, { "key": "langsmith_api_key", "configured": false } ] }

Keys outside the well-known set (e.g. hubspot_api_key, serpapi_api_key, slack_*) are NOT in the SECRET_KEYS enum, so they appear in this listing only after they have been explicitly written via the PUT below.

With managed services the listing no longer offers the provider keys that connections hold; a copy left in the secrets by the upgrade is still listed, so it can be deleted.

Values are NEVER returned by this endpoint. To verify a value, your only options are the consumers (e.g. send a Playground message and see the LLM respond) or query the DB directly with decrypt() from the engine.

Step 2 - Set one or more secrets

The PUT endpoint accepts a batch payload. Use it to add or update keys in a single call.

curl -s -X PUT http://localhost:4000/api/instances/my-assistant/secrets \ -H "Content-Type: application/json" \ -H "Cookie: authjs.session-token=$TOKEN" \ -d '{ "secrets": [ { "key": "openai_api_key", "value": "sk-..." }, { "key": "langsmith_api_key", "value": "ls_..." } ] }'

Key naming: well-known keys are all lowercase with underscores. The recognised set (declared in secrets.store.ts) is:

openai_api_key, anthropic_api_key, nebius_api_key, bedrock_api_key, aws_provider_access_key_id, aws_provider_secret_access_key, aws_provider_region, langsmith_api_key, auth_api_key, tavily_api_key, github_token, s3_bucket_name, aws_region, http_api_key, deepgram_api_key

With managed services a batch naming a provider key (openai_api_key, anthropic_api_key, nebius_api_key, bedrock_api_key, the three aws_provider_* keys, deepgram_api_key, and the TensorX, Scaleway, Regolo.ai, ElevenLabs and Typesafe keys) is refused whole with 400, and nothing is written: the message points to the connection routes, because a key stored there would be read by nothing.

The bucket’s access keys, aws_access_key_id and aws_secret_access_key, are written the same way. In the panel the bucket, its region and both keys are typed in the agent’s Advanced section, in the card that holds the Store attachments switch.

s3_endpoint is gone. It was handed straight to the S3 client so an agent could write to MinIO or Cloudflare R2 - which made an agent secret into a request-forgery primitive: whoever could write an agent’s secrets could point every upload and download, file contents included, at any host, the deployment’s private network included. The key is ignored and the stored values were deleted on upgrade, rather than left inert to read as configuration still in force. Bringing S3-compatible storage back means a deployment-tier allow-list of endpoints, not this key.

The AWS provider keys carry a _provider_ segment on purpose (open-source build). aws_provider_access_key_id and friends feed the AI provider - Bedrock chat and embeddings, AWS Transcribe. They are deliberately a separate namespace from the generic aws_* keys that reach object storage (aws_access_key_id, aws_secret_access_key, aws_region), so the provider and the agent’s bucket can live in different AWS accounts. A key written as aws_access_key_id is read by storage, never by a model provider.

You can also store custom keys (e.g. for skill-specific credentials) using the same endpoint - they will appear in the listing alongside the well-known ones.

Where each key comes from

Every well-known key is a credential issued by somebody else. This is where you get it, and what it looks like when you have it. With managed services the provider keys in this table go into a provider connection instead, under the connection’s own field names (api_key; for Bedrock api_key or access_key_id and secret_access_key, plus region); where each comes from does not change.

KeyProvider consoleNotes
openai_api_keyplatform.openai.com/api-keys  → Create new secret keysk-…, displayed once. Keys are scoped to a project: a key from the wrong project authenticates and then cannot see your models.
anthropic_api_keyconsole.anthropic.com  → Settings → API keyssk-ant-…, displayed once.
nebius_api_keythe Nebius Token Factory console → API keys (docs.tokenfactory.nebius.com )The provider is OpenAI-compatible; the key is Nebius’s own, not an OpenAI one.
tensorx_api_key, scaleway_api_key, regolo_api_keythe TensorX, Scaleway and Regolo.ai consolesManaged services. Keys for the three EU providers the managed-services build registers, typed into a provider connection. Scaleway’s is an IAM secret key for Generative APIs.
bedrock_api_keyAmazon Bedrock console  → API keysA bearer token, and the simplest Bedrock path - it skips SigV4 signing entirely. AWS issues short-term keys (up to 12 hours, inheriting your IAM permissions) and long-term ones (to an expiry date, creating an IAM user); for a long-lived deployment use a long-term key or the SigV4 credentials below.
aws_provider_access_key_id, aws_provider_secret_access_key, aws_provider_regionIAM console → your user → Security credentials → Access keysThe SigV4 alternative to the key above, used for Bedrock chat, Bedrock embeddings and AWS Transcribe. The IAM principal needs bedrock:InvokeModel (and bedrock:InvokeModelWithResponseStream for streaming) on the models you intend to call, in that region. Model access must also be requested in the Bedrock console per model - a fully correct key returns AccessDenied until it is.
langsmith_api_keysmith.langchain.com  → Settings → API keyslsv2_…. Only enables tracing; nothing depends on it. See Observe with LangSmith.
tavily_api_keyapp.tavily.com  → API keystvly-…. Needed by the web-search tool and by nothing else. Set on the agent only.
github_tokengithub.com/settings/personal-access-tokens  (fine-grained) or Developer settings → Tokens (classic)Read access to the repositories the agent should clone. A fine-grained token must name those repositories explicitly - the default is none. Set on the agent only.
deepgram_api_keyconsole.deepgram.com  → API keysTranscribes inbound voice notes on Telegram and WhatsApp. Without it, audio is ignored rather than failing. With managed services it goes into a Deepgram connection, which also serves the voice channel’s speech recognition and turn detection.
ElevenLabs and Typesafe keyseach provider’s API keys pageManaged services. For the voice channel: ElevenLabs speaks and transcribes calls, Typesafe Jev classifies for the call’s judges. Typed into a provider connection; there is no secret for them.
s3_bucket_name, aws_regionnot credentialsThis agent’s object storage: the bucket stored attachments go to, and its region. The extra plugin’s extra:fileUpload tool reads the same keys. Required as a pair. The region is not resolved from the environment; with managed services both can come from the organization.
aws_access_key_id, aws_secret_access_keyIAM console → your user → Security credentials → Access keysThe credentials for that bucket. The principal needs s3:PutObject and s3:GetObject on arn:aws:s3:::<bucket>/*. Both are required: exactly one is a configuration error, and with neither nothing is stored. The deployment’s own AWS identity is never used in their place. A separate namespace from aws_provider_* on purpose, so storage and the Bedrock provider can live in different accounts.
hubspot_api_keyHubSpot → Settings → Integrations → Private Apps → the app’s Auth tabpat-…. Used by the HubSpot tools, which arrive as a plugin, and unrelated to the HubSpot channel, which keeps its own credentials on the channel. Not in the well-known set: it appears in the listing only once written.
auth_api_key, http_api_keyminted by you, for this agentThese are the only two that come from nowhere else: they authenticate an inbound caller to this agent (the OpenAI-compatible endpoint, and the HTTP channel). Generate a random value - openssl rand -hex 32 - and hand it to the caller. This is also why neither can be shared at organization level.

Two failure shapes worth expecting. A key pasted with surrounding whitespace or a trailing newline is a different string and authenticates as nothing; and most of these are shown once at creation, so the recovery for a lost value is a new key, never a lookup. Nothing in Polyant can read a stored value back.

Step 3 - Delete a secret

curl -s -X DELETE http://localhost:4000/api/instances/my-assistant/secrets/openai_api_key \ -H "Cookie: authjs.session-token=$TOKEN"

The :key path segment is the lowercase secret name. There is no soft-delete: the row is removed immediately and any consumer relying on it (LLM provider, S3 client, etc.) will fail until you set it again.

What is NOT in instance_secrets

The following live in different tables and are managed by separate endpoints - do not try to store them under /secrets:

CredentialWhere it livesEndpoint
Telegram bot tokeninstance_channels (type=telegram)PUT /api/instances/:slug/channels/telegram
Slack bot token (xoxb-) and signing secretinstance_channels (type=slack)PUT /api/instances/:slug/channels/slack
Twilio Account SID / Auth Token / WhatsApp From numberinstance_channels (type=whatsapp)PUT /api/instances/:slug/channels/whatsapp
Skill-specific env varsinstance_skill_envPUT /api/instances/:slug/skills/:slug/env

See Connect a channel for the channel-specific flow.

Organization-level shared secrets

Managed services.

An organization can hold secrets of its own, shared by every agent in it, through GET/PUT/DELETE /api/organizations/:orgSlug/secrets. The panel has no page that writes them; an agent’s settings show a value it inherits this way.

They use the same table shape, the same AES-256-GCM envelope, and the same key vocabulary as an agent’s own secrets, so an org-level key and an agent-level key name the same thing.

Resolution is agent-first. An organization’s secret is the fallback for any agent that has none of its own under that key - so no existing agent’s behaviour changes the day its organization gets one too.

Reading and writing them needs org.secret:read / org.secret:write - Admin and above. Deliberately not the agent-level agent.secret:*, which a Member holds: gating the tenant’s shared credentials on one agent’s rights would open them to every member.

Only a few keys can be shared

The fallback applies through an explicit allowlist: the LangSmith key, and the S3 bucket name with its region. The organization endpoint refuses any other key with 400, naming it, and nothing in the batch is written. The bucket’s access keys (aws_access_key_id, aws_secret_access_key) are not on the list: an organization can hold the bucket and its region for its agents, but each agent sets its own keys.

The credentials of tools belong to the agent that enables them. tavily_api_key and github_token were shareable until 1.3.4-ee: since then an organization cannot hold them and agents no longer inherit them. The upgrade (migration 0175) deleted the values organizations held for these two keys and copied nothing to the agents, so an agent that relied on one needs its own key, typed in the tool’s panel.

Provider keys are not shared this way any more. The organization endpoint refuses them like the agent endpoint does, because an organization shares a provider credential by creating a provider connection and lending it to workspaces. Copies left by the upgrade are read by nothing and can be deleted with DELETE /api/organizations/:orgSlug/secrets/:key.

auth_api_key and http_api_key are deliberately absent too, and refused the same way rather than accepted as a value that would be silently ignored. Those two authenticate an inbound caller to one specific agent: letting them fall back from the organization would let a single org-level key authenticate every agent in it, voiding the per-agent authEnabled model.

An allowlist rather than a denylist, on purpose - with a denylist, a newly added inbound-auth key would ride the fallback by default until someone noticed.

Rotate the master key

The ENCRYPTION_KEY env var is the single point of failure for all encrypted data. Rotating it is destructive - you must re-enter every secret afterwards.

  1. Stop the engine so no readers/writers race the rotation.
  2. Export every instance (GET /api/instances/:slug/export). The bundle does NOT include secret values, only the list of which keys were configured - that is exactly what you need to know what to re-enter.
  3. Generate a new key with openssl rand -hex 32 and update ENCRYPTION_KEY in your environment.
  4. Wipe the old rows: DELETE FROM instance_secrets; (and, if you also rotate channel encryption, DELETE FROM instance_channels;). The old ciphertexts cannot be read with the new key, so leaving them creates noise on every read.
  5. Start the engine and re-enter the secrets via the PUT endpoint (or the admin panel: provider keys in the agent’s Model section, tool keys in the tool’s panel). With managed services provider connections are encrypted with the same key: rotate each connection with its key again (see Provider connections and roles).
  6. Rotate the upstream credentials too: anyone who had the old ENCRYPTION_KEY could have decrypted the old values, so treat them as compromised.

Verification

After running Step 2 you should see:

curl -s http://localhost:4000/api/instances/my-assistant/secrets | jq '.secrets[] | select(.configured)'
  • The openai_api_key and langsmith_api_key entries return configured: true.
  • In the agent’s Model section, the OpenAI key field shows Configured instead of an empty input (open-source build; with managed services the section names the connection bound to each task).
  • A test message via the Playground or POST /v1/chat/completions succeeds without a “missing API key” error.

See also

Last updated on