Guides
Provision Secrets for Backend Services
Store API keys and service tokens in Alter instead of environment variables, scoped to principals.
By the end of this guide, a backend service calls a third-party API using an operator-provisioned credential — a Datadog API key, a Stripe secret key, an AWS access key — without that credential ever appearing in the application’s environment, config files, or logs.
The flow:
- The operator stores the credential once in the developer portal.
- The operator issues a grant binding the credential to a principal (system, user, group, or agent).
- The backend calls
app.request()with the resultinggrant_id. Alter injects the credential into the outgoing call.
Prerequisites
Section titled “Prerequisites”- An app with a runtime API key (
alter_rk_…, or legacyalter_key_…). - A managed-secret provider configured — this guide uses Datadog as the example.
- The credential itself, generated at the provider (Datadog → API Keys → New Key).
Walkthrough
Section titled “Walkthrough”1. Store the credential
Section titled “1. Store the credential”In the developer portal:
- Open the app and go to Managed Secrets.
- Pick Datadog from the catalog (or Custom for anything not listed).
- Paste the API key into the Credential field.
- Click Store.
The plaintext is encrypted and sent to the vault. The portal will not display it again.
2. Issue a grant
Section titled “2. Issue a grant”A stored credential is not usable until a grant binds it to a principal. From Managed Secrets → Datadog → Grants → New Grant:
- System — the credential is callable by anyone holding the app key, with no user identity required. Use this for backend services and cron jobs.
- User — bind to one named user (resolved from their JWT). Useful when each user gets a per-user Datadog key.
- Group — bind to an IDP group. All current and future members of the group can use it. Only available when the identity provider supports group grants (Clerk, or Okta with stable-ID group keying — see the support table).
- Agent — bind to a managed agent. Only that agent can reach the credential.
For a system-bound grant, the portal returns a grant_id. Copy it into application config.
Equivalent SDK call (operator path):
import asyncio, osfrom alter_sdk import Appfrom alter_sdk.models import SystemPrincipal
async def main(): async with App(api_key=os.environ["ALTER_API_KEY"]) as app: result = await app.create_managed_secret_grant( managed_secret_id="ms_datadog_abc", principal=SystemPrincipal(label="prod-monitoring"), ) print(result.grant_id)
asyncio.run(main())import { App } from "@alter-ai/alter-sdk";
const app = new App({ apiKey: process.env.ALTER_API_KEY! });
try { const result = await app.createManagedSecretGrant("ms_datadog_abc", { principal: { type: "system", label: "prod-monitoring" }, }); console.log(result.grantId);} finally { await app.close();}3. Call the provider
Section titled “3. Call the provider”The application calls request() against the provider, passing the grant_id:
import asyncio, osfrom alter_sdk import App, HttpMethod
async def main(): async with App(api_key=os.environ["ALTER_API_KEY"]) as app: response = await app.request( HttpMethod.POST, "https://api.datadoghq.com/api/v1/events", grant_id=os.environ["DATADOG_GRANT_ID"], json={"title": "Deployment", "text": "Service deployed to prod"}, ) print(response.status_code, response.json())
asyncio.run(main())import { App, HttpMethod } from "@alter-ai/alter-sdk";
const app = new App({ apiKey: process.env.ALTER_API_KEY! });
try { const response = await app.request( HttpMethod.POST, "https://api.datadoghq.com/api/v1/events", { grantId: process.env.DATADOG_GRANT_ID!, json: { title: "Deployment", text: "Service deployed to prod" }, }, ); console.log(response.status, await response.json());} finally { await app.close();}The Datadog API key is injected as DD-API-KEY: … (per the Datadog template). Application code never holds it.
Patterns
Section titled “Patterns”Per-user managed secrets
Section titled “Per-user managed secrets”For a product where each user holds their own API key at a provider, bind a managed secret per user. The user uploads the credential through a UI flow; the backend stores it bound to the user’s identity. At call time, the SDK resolves the grant from the user’s JWT exactly as it would for an OAuth grant — see Call APIs on behalf of users.
Instead of tracking a grant_id per user, you can resolve the current user’s secret by its slug — the unique identifier shown on the secret’s page (e.g. stripe-production):
response = await app.request( HttpMethod.POST, "https://api.stripe.com/v1/charges", provider="stripe-production", # the secret's slug, unique per app user_token=current_user_jwt, # the end-user's identity token json={"amount": 1000, "currency": "usd"},)const response = await app.request(HttpMethod.POST, "https://api.stripe.com/v1/charges", { provider: "stripe-production", userToken: currentUserJwt, json: { amount: 1000, currency: "usd" },});The slug resolves to exactly one secret, so this is unambiguous. If a single user holds several secrets, enumerate them with list_grants() (scope to the user with end_user_token=…) — each item carries its managed_secret_slug and grant_id — then call by the specific grant_id. (Filtering list_grants(provider_id="stripe-production") narrows to that one slug, so it doesn’t enumerate the others.)
Rotation
Section titled “Rotation”When the credential at the provider rotates:
- Generate the new credential at the provider.
- Open Managed Secrets → [Provider] → Update credential.
- Paste the new value and save.
Every existing grant_id keeps working — the credential is replaced in place, the grant identity is unchanged. No application restart, no env-var update.
AWS managed secrets
Section titled “AWS managed secrets”AWS managed secrets carry extra structure because Alter computes SigV4 signatures per request. See AWS managed secret reference.
Group-bound managed secrets
Section titled “Group-bound managed secrets”For a “support team” Datadog key, bind the grant to an IDP group:
from alter_sdk.models import GroupPrincipal
await app.create_managed_secret_grant( managed_secret_id="ms_datadog_abc", principal=GroupPrincipal( external_group_id="support", idp_id="11111111-2222-3333-4444-555555555555", # the AppIdentityProvider id (UUID) label="support-team", ),)await app.createManagedSecretGrant("ms_datadog_abc", { principal: { type: "group", externalGroupId: "support", idpId: "11111111-2222-3333-4444-555555555555", // the AppIdentityProvider id (UUID) label: "support-team", },});Any user whose JWT carries the support group claim can now resolve this grant under JWT identity.
Troubleshooting
Section titled “Troubleshooting”| Error | Likely cause | Fix |
|---|---|---|
GrantNotFoundError | grant_id is wrong or the grant was revoked. | Re-issue the grant from the portal. |
ProviderAPIError (401) | The credential at the provider was revoked or expired. | Generate a new credential at the provider, update the stored value. |
ProviderAPIError (403) | The credential lacks permission for the route. | Check the credential’s permission scope at the provider. |
| Application can read the credential | Misconfiguration. | Managed secrets are write-only; if anything in the application has plaintext access, the storage path is wrong. |