- MCP Server ToolsUpdated
- Managed MCP with chat()Updated
- Manual MCPUpdated
- MCP Type GenerationUpdated
- MCP Client InputUpdated
- MCP Server Tools: AdvancedNew
- MCP EventsNew
- MCP AppsUpdated
- Serve Tools over HTTPUpdated
- Serve Resources and PromptsUpdated
- Ask for InputUpdated
- Draft Text from a ToolUpdated
- MCP Server TasksUpdated
- MCP Server AuthUpdated
- MCP Server SessionsUpdated
- MCP Server on stdioUpdated
Persistence (Advanced)
- GenerationsUpdated
- Realtime Voice ChatUpdated
- Text-to-SpeechUpdated
- TranscriptionUpdated
- Transcription: AdvancedNew
- Voice CreationUpdated
- Audio RecordingUpdated
- Audio GenerationUpdated
- Image GenerationUpdated
- Image Generation: AdvancedNew
- Video GenerationUpdated
- Video Generation: AdvancedNew
- Live GenerationUpdated
- World GenerationUpdated
- Generation HooksUpdated
- Build your first harnessUpdated
- Host optionsNew
- Send and show mediaUpdated
- Connect clientsUpdated
- Write a client in any languageNew
- Access and tenantsNew
- Run in the terminalUpdated
- Deploy a harnessUpdated
- Send inputs safelyUpdated
- Run a tool without a model
- Durable sessionsUpdated
- Run background work on workersNew
- List, rename, and fork sessionsUpdated
- Lifecycle and retentionNew
- Run side effects onceUpdated
- Wait for a personNew
- Build on the session logUpdated
- Control how a turn endsUpdated
- Store settings per threadUpdated
- Compact a long sessionUpdated
- Fork and reset a threadUpdated
- One log for a session treeUpdated
- Write a pluginUpdated
- Track usage and costUpdated
- Ask before risky tool callsUpdated
- Give your agent coding toolsUpdated
- Sandbox and extend the coding toolsUpdated
- Run the coding tools for many threadsNew
- Undo a turnUpdated
- Build a coding agentUpdated
- Delegate to coding agentsUpdated
- Switch between agent profilesUpdated
- Auth and connectorsUpdated
- Share a thread between peopleUpdated
- Connect model providersUpdated
- Build your own UIUpdated
- Code mode in a harnessUpdated
- Work until a goal is metUpdated
- Run agents from a harnessUpdated
- Hand off a conversationNew
- Add skillsUpdated
- Use MCP serversUpdated
- Give your agent a computerNew
- Use from any MCP clientUpdated
- Use a harness from an editor (ACP)Updated
- OverviewUpdated
- Quick StartUpdated
- ProvidersUpdated
- HarnessesUpdated
- Harness AuthUpdated
- WorkspaceUpdated
- ToolsUpdated
- PolicyUpdated
- Lifecycle & SnapshotsUpdated
- Portable SnapshotsUpdated
- Keep Files After ReloadUpdated
- Save a Named VersionUpdated
- Branch From a VersionUpdated
- Send a Frozen FileUpdated
- Let the Agent Save and ForkUpdated
- Pick Which Files to KeepUpdated
- What a Snapshot StoresUpdated
- Instance DurabilityUpdated
- Durable RunsUpdated
- EventsUpdated
Sandboxes (Advanced)
- Runtime ContextUpdated
- Bring Your Own KeyUpdated
- Keep Keys on the ServerNew
- Debug LoggingUpdated
- Multimodal ContentUpdated
- Files APIUpdated
- Per-Model Type SafetyUpdated
- Runtime Adapter SwitchingUpdated
- Tree-ShakingUpdated
- Extend AdapterUpdated
- Extend Adapter: AdvancedNew
- Typed Pre-Configured OptionsUpdated
- Test with a Fake ModelUpdated
- Prompt CachingUpdated
- Mid-Conversation ChangesUpdated
- Approval Flow ProcessingUpdated
Keep Keys on the Server
A key in the browser is gone when the user opens your app on a new device. A background job has no browser to send it. For these cases, save the key on your server, one key for each user and provider.
Do these four steps:
- Make a credential store.
- Add a route that saves the key.
- Send the key from your form.
- Read the key in the chat route.
1. Make a credential store#
Start with the memory store:
import { memoryCredentialStore } from "@tanstack/ai/byok/server";
export const credentials = memoryCredentialStore();The memory store loses the keys when the process stops. For production, write a store for your database or your secrets manager.
2. Add a route that saves the key#
Get the user from your auth, not from the request body:
import { credentials } from "./credentials";
import { getUser } from "./auth";
export async function POST(request: Request) {
const user = await getUser(request);
if (!user) return new Response("Unauthorized", { status: 401 });
const body: unknown = await request.json();
if (
typeof body !== "object" ||
body === null ||
!("key" in body) ||
typeof body.key !== "string"
) {
return new Response("Send { key }", { status: 400 });
}
await credentials.set({
scope: { userId: user.id },
id: "openai",
credential: { type: "api_key", value: body.key },
});
return new Response(null, { status: 204 });
}3. Send the key from your form#
import { useState } from "react";
export function ServerKeyForm() {
const [value, setValue] = useState("");
return (
<form
onSubmit={(event) => {
event.preventDefault();
void fetch("/api/keys", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ key: value.trim() }),
}).then(() => setValue(""));
}}
>
<input
type="password"
autoComplete="off"
value={value}
onChange={(event) => setValue(event.target.value)}
/>
<button type="submit">Save</button>
</form>
);
}4. Read the key in the chat route#
storedProviderKeys reads the key that the user saved:
import {
chat,
chatParamsFromRequest,
keyedAdapter,
toServerSentEventsResponse,
} from "@tanstack/ai";
import { byokMissing, storedProviderKeys } from "@tanstack/ai/byok/server";
import { createOpenaiChat } from "@tanstack/ai-openai";
import { openaiByok } from "@tanstack/ai-openai/byok";
import { credentials } from "./credentials";
import { getUser } from "./auth";
const gpt = keyedAdapter(openaiByok, (key) =>
createOpenaiChat("gpt-6.1-sol", key),
);
export async function POST(request: Request) {
const user = await getUser(request);
if (!user) return new Response("Unauthorized", { status: 401 });
const keys = storedProviderKeys(credentials, { userId: user.id });
const apiKey = await keys.get(openaiByok);
if (!apiKey) return byokMissing(openaiByok);
const params = await chatParamsFromRequest(request);
const stream = chat({
adapter: gpt.create(apiKey),
messages: params.messages,
threadId: params.threadId,
runId: params.runId,
});
return toServerSentEventsResponse(stream);
}How storedProviderKeys finds a key:
- It reads the api_key credential that the user saved under the provider id.
- If you add tenantId to the scope, a user with no key gets the key of the tenant. A tenant key is saved with tenantId and no userId.
- If there is no saved key, it reads the env var of the provider, here OPENAI_API_KEY.
- To give the same keys to your agents, pass keys.get to subagents.keys.
Your users now save a key once, and every device and job uses it.
Write a store for production#
A CredentialStore has four methods, and two optional ones for compare-and-set. You decide where the secrets live:
- A credentials table in the database that you already use, next to your chat tables.
- A secrets manager, for example AWS Secrets Manager or Vault.
import type { CredentialScope } from "@tanstack/ai/byok";
type Credential =
| { type: "api_key"; value: string }
| { type: "secret_answer"; value: string; expiresAt: number } // epoch ms
| {
type: "oauth";
accessToken: string;
refreshToken?: string;
expiresAt?: number; // epoch ms
scopes?: Array<string>;
client?: {
clientId: string;
clientSecret?: string;
redirectUri?: string;
issuer?: string;
};
};
interface CredentialStore {
get(key: { scope: CredentialScope; id: string }): Promise<Credential | null>;
set(entry: {
scope: CredentialScope;
id: string;
credential: Credential;
}): Promise<void>;
delete(key: { scope: CredentialScope; id: string }): Promise<void>;
list(
scope: CredentialScope,
): Promise<Array<{ id: string; type: Credential["type"]; expiresAt?: number }>>;
// Optional: compare-and-set.
getVersioned?(key: {
scope: CredentialScope;
id: string;
}): Promise<{ credential: Credential; revision: string } | null>;
setIf?(entry: {
scope: CredentialScope;
id: string;
credential: Credential;
expectedRevision: string | null;
}): Promise<{ ok: true; revision: string } | { ok: false; reason: "conflict" }>;
}Type your store with defineCredentialStore from @tanstack/ai/byok. CredentialScope has tenantId and userId. It can also have a threadId, which the store ignores.
Rules for the keys:
- Key each credential by scope.tenantId, scope.userId, and id. Do not use scope.threadId.
- A user credential is not visible at the tenant scope. A tenant credential is not visible at the user scope.
- A scope with no tenant and no user is one bucket that every caller shares. Get the scope from your server-side auth, never from the request body.
- client is the OAuth client that the tokens belong to. A token refresh needs it.
- A secret_answer row is not a user credential. The harness keeps a secret answer there for a short time. Leave this type out of a list of connected accounts. You can delete a row after its expiresAt.
Rules for the secrets (value, accessToken, refreshToken, client.clientSecret):
- Encrypt them at rest. Keep the encryption key in a KMS or a server-only env var, not in the same database.
- list returns ids and types only. It never returns a secret, not even masked.
- delete removes the row. Do not keep a soft-deleted copy of the secret.
- Do not write a secret to a log, an error message, or a trace. To remove known secrets from a text, use scrubSecrets(text, secrets) from @tanstack/ai/byok.
The harness uses the same store: pass it to createHarnessHost({ credentials }).
Add compare-and-set#
Two servers can refresh the same OAuth token at the same time. If the service rotates refresh tokens, the second save then keeps a token that no longer works. Add getVersioned and setIf, so the second save does not remove the first:
- getVersioned returns the credential and its revision, or null when the key is absent.
- setIf writes only when the stored revision equals expectedRevision. Pass null to write only when the key is absent. Success returns { ok: true, revision }. A different revision returns { ok: false, reason: "conflict" } and does not write.
- Every write gives the key a new revision. A plain set does this too. After delete, the deleted revision is never used again.
- Add both methods or neither. memoryCredentialStore() has both.
To refresh a token in your own route, read it with readCredentialEntry. It finds the credential like readCredential (the user first, then the tenant), and also returns the scope where it found it and its revision:
import { readCredentialEntry } from "@tanstack/ai/byok/server";
import type { Credential, CredentialStore } from "@tanstack/ai/byok";
export async function refreshGithub(
store: CredentialStore,
scope: { tenantId: string; userId: string },
refresh: (current: Credential) => Promise<Credential>,
) {
const entry = await readCredentialEntry(store, scope, "github");
if (!entry) return null;
const next = await refresh(entry.credential);
// Save to the scope where the credential was, also when it is the tenant's.
const key = { scope: entry.scope, id: "github", credential: next };
if (!store.setIf || entry.revision === undefined) {
await store.set(key);
return next;
}
const saved = await store.setIf({ ...key, expectedRevision: entry.revision });
if (saved.ok) return next;
// Another server saved first. Use its token, and do not refresh again.
return (await readCredentialEntry(store, scope, "github"))?.credential ?? null;
}In a harness, run.credentials.refresh does this for you. See Refresh a token.