Overview
Credentials provide secure storage for API keys, tokens, and other secrets used by tool sources. Executor supports two credential providers:
- Local Convex: Store credentials in Convex database (self-hosted)
- WorkOS Vault: Store credentials in WorkOS encrypted vault (cloud)
Credential Providers
Local Convex
WorkOS Vault
{
provider: "local-convex",
secretJson: {
token: "ghp_abc123..."
}
}
Credentials are stored directly in the Convex database as JSON objects. Suitable for self-hosted deployments.Security: Secrets are stored in plaintext in Convex. Use WorkOS Vault for production deployments.Source: packages/core/src/credential-providers.ts:97-101{
provider: "workos-vault",
secretJson: {
objectId: "secret_01HXYZ...",
apiKey: "sk_workos_..."
}
}
Credentials are stored in WorkOS Vault with encryption at rest. The credential record contains only a reference to the vault object.Security: Secrets are encrypted by WorkOS and retrieved on-demand during tool invocation.Source: packages/core/src/credential-providers.ts:103-125
Provider Selection
The credential provider is determined by environment configuration:
// From credentials.ts:26-42
function managedCredentialMode(): "cloud" | "self-hosted" {
const explicit = process.env.EXECUTOR_DEPLOYMENT_MODE;
if (explicit === "cloud" || explicit === "production") {
return "cloud";
}
if (explicit === "self-hosted") {
return "self-hosted";
}
if (process.env.EXECUTOR_ENFORCE_MANAGED_CREDENTIALS === "true") {
return "cloud";
}
return "self-hosted";
}
// In cloud mode, only workos-vault is allowed
if (shouldEnforceManagedCredentials() && provider !== "workos-vault") {
throw new Error(
"Managed credential storage is required in cloud deployments."
);
}
Cloud deployments (e.g., executor.com) enforce WorkOS Vault for security. Self-hosted deployments can use local-convex storage.
Credential Scopes
Credentials support three scope levels:
Account Scope
Workspace Scope
Organization Scope
{
scopeType: "account",
accountId: "account_abc123",
organizationId: "org_xyz",
workspaceId: undefined,
sourceKey: "source:src_github"
}
Personal credentials, only accessible by the specified account. Used for user-specific API keys.Use case: Personal GitHub tokens, user-specific OAuth tokens{
scopeType: "workspace",
accountId: undefined,
organizationId: "org_xyz",
workspaceId: "workspace_abc",
sourceKey: "source:src_github"
}
Workspace-wide credentials, accessible by all tasks in the workspace.Use case: Staging API keys, workspace-specific service accounts{
scopeType: "organization",
accountId: undefined,
organizationId: "org_xyz",
workspaceId: undefined,
sourceKey: "source:src_github"
}
Organization-wide credentials, shared across all workspaces.Use case: Production API keys, shared service accounts
Scope Resolution
When resolving credentials for a tool call, Executor searches in order of specificity:
// From credentials.ts:466-489
if (args.scopeType === "account") {
// 1. Try account-scoped credential
const accountDoc = await tryAccount();
if (accountDoc) return mapCredential(accountDoc);
// 2. Fall back to workspace-scoped
const workspaceDoc = await tryWorkspace();
if (workspaceDoc) return mapCredential(workspaceDoc);
// 3. Fall back to organization-scoped
const organizationDoc = await tryOrganization();
if (organizationDoc) return mapCredential(organizationDoc);
return null;
}
Resolution order: Account → Workspace → Organization
This allows personal credentials to override workspace defaults, and workspace credentials to override organization defaults.
Credential Schema
// From schema.ts:355-380
sourceCredentials: defineTable({
bindingId: v.string(), // domain ID: bind_<uuid>
credentialId: v.string(), // domain ID: conn_<uuid>
scopeType: credentialScopeTypeValidator, // "account" | "workspace" | "organization"
accountId: v.optional(v.id("accounts")),
organizationId: v.id("organizations"),
workspaceId: v.optional(v.id("workspaces")),
sourceKey: v.string(), // References tool source: "source:src_<uuid>"
provider: credentialProviderValidator, // "local-convex" | "workos-vault"
secretJson: jsonObjectValidator, // Provider-specific secret storage
additionalHeaders: v.optional(credentialAdditionalHeadersValidator),
boundAuthFingerprint: v.optional(v.string()), // Linked to tool source auth
createdAt: v.number(),
updatedAt: v.number(),
})
Credential Properties
Unique binding identifier (e.g., bind_<uuid>). Used as a stable handle for UI operations.
Connection identifier (e.g., conn_<uuid>). Multiple bindings can share the same credential ID for multi-scope storage.
References a tool source using the pattern source:<sourceId>. Links the credential to a specific tool source.
provider
'local-convex' | 'workos-vault'
required
Storage provider for the credential secret.
secretJson
Record<string, unknown>
required
Provider-specific secret storage:
- local-convex: Actual secret values (e.g.,
{ token: "abc123" })
- workos-vault: Vault reference (e.g.,
{ objectId: "secret_01..." })
Optional additional HTTP headers to include with tool calls (e.g., custom auth headers, request metadata).Source: packages/database/src/database/validators.ts
Fingerprint of the tool source’s auth configuration. Used to detect when credential needs re-configuration.Source: packages/database/src/database/readers.ts:computeBoundAuthFingerprint
Creating Credentials
// From credentials.ts:74-304
export const upsertCredential = internalMutation({
args: {
id: v.optional(v.string()), // Reuse existing ID or generate new
workspaceId: vv.id("workspaces"),
scopeType: v.optional(credentialScopeTypeValidator),
sourceKey: v.string(), // "source:src_<uuid>"
accountId: v.optional(vv.id("accounts")),
provider: v.optional(credentialProviderValidator),
secretJson: jsonObjectValidator,
additionalHeaders: v.optional(credentialAdditionalHeadersValidator),
},
handler: async (ctx, args) => {
const scopeType = args.scopeType ?? "workspace";
// Validate scope fields match scope type
assertCredentialScopeFields({
scopeType,
workspaceId: scopedWorkspaceId,
accountId: scopedAccountId,
});
// For account scope, verify account is org member
if (scopeType === "account" && scopedAccountId) {
const membership = await ctx.db
.query("organizationMembers")
.withIndex("by_org_account", (q) =>
q.eq("organizationId", organizationId)
.eq("accountId", scopedAccountId)
)
.unique();
if (!membership || membership.status !== "active") {
throw new Error("accountId must be an active member");
}
}
// Enforce provider based on deployment mode
const provider = args.provider ?? "local-convex";
if (shouldEnforceManagedCredentials() && provider !== "workos-vault") {
throw new Error(
"Managed credential storage required in cloud deployments."
);
}
// Compute bound auth fingerprint
const boundAuthFingerprint = await computeBoundAuthFingerprint(
ctx,
args.workspaceId,
args.sourceKey,
);
// Insert or update credential
const connectionId = requestedId || existing?.credentialId ||
`conn_${crypto.randomUUID()}`;
// Trigger tool registry rebuild
await safeRunAfter(ctx.scheduler, 0,
internal.executorNode.rebuildToolInventoryInternal,
{ workspaceId: args.workspaceId, ...accountContext }
);
},
});
In account-scoped credentials, the account must be an active member of the organization. This prevents credential access after membership is revoked.
Credential Resolution
Credentials are resolved during tool invocation:
// From credentials.ts:381-490
export const resolveCredential = internalQuery({
args: {
workspaceId: vv.id("workspaces"),
sourceKey: v.string(),
scopeType: credentialScopeTypeValidator,
accountId: v.optional(vv.id("accounts")),
},
handler: async (ctx, args) => {
// Verify tool source exists and is accessible
const sourceId = sourceIdFromSourceKey(args.sourceKey);
if (sourceId) {
const source = await ctx.db
.query("toolSources")
.withIndex("by_source_id", (q) => q.eq("sourceId", sourceId))
.unique();
// Check source belongs to organization
if (source && source.organizationId !== organizationId) {
return null;
}
// Check workspace scope for workspace-scoped sources
if (source && source.scopeType === "workspace" &&
source.workspaceId !== args.workspaceId) {
return null;
}
}
// Resolve credential with fallback chain
// Account → Workspace → Organization
},
});
Secret Decryption
Retrieving the actual secret value:
// From credential-providers.ts:132-155
export async function resolveCredentialPayload(
record: Pick<CredentialRecord, "provider" | "secretJson">,
options?: { readVaultObject?: VaultObjectReader },
): Promise<CredentialPayload | null> {
const provider = record.provider;
if (provider === "local-convex") {
// Secrets stored directly
return coerceRecord(record.secretJson);
}
if (provider === "workos-vault") {
// Fetch from WorkOS Vault
const reference = parseWorkosVaultReference(record.secretJson);
const objectId = reference.objectId;
const rawValue = await readVaultObject({
objectId,
apiKey: reference.apiKey
});
// Parse secret value (JSON, env-style, or raw token)
return parseSecretValue(rawValue);
}
}
Secret Format Detection: The vault reader automatically detects secret format:
// From credential-providers.ts:34-56
function parseSecretValue(raw: string): CredentialPayload {
const value = raw.trim();
// Try JSON
try {
const parsed = JSON.parse(value);
if (typeof parsed === "object") {
return parsed;
}
} catch {}
// Try env-style (KEY=value)
const envStyle = parseEnvStyleSecret(value);
if (envStyle) {
return envStyle;
}
// Fall back to raw token
return { token: value };
}
Supported formats:
- JSON:
{ "token": "abc123", "key": "xyz" }
- Env-style:
TOKEN=abc123\nKEY=xyz
- Raw token:
abc123
Credentials can include additional HTTP headers:
// Example: Custom headers for API calls
additionalHeaders: [
{ name: "X-Request-ID", value: "req-123" },
{ name: "X-Custom-Auth", value: "Bearer xyz" }
]
These headers are merged with the tool source’s auth headers during invocation.
Source: packages/core/src/tool/source-auth.ts:normalizeCredentialAdditionalHeaders
Listing Credentials
// From credentials.ts:307-346
export const listCredentials = internalQuery({
args: {
workspaceId: vv.id("workspaces"),
accountId: v.optional(vv.id("accounts")),
},
handler: async (ctx, args) => {
const workspace = await ctx.db.get(args.workspaceId);
const organizationId = workspace.organizationId;
// Fetch all scope levels
const [workspaceDocs, organizationDocs, accountDocs] = await Promise.all([
ctx.db.query("sourceCredentials")
.withIndex("by_workspace_created", (q) =>
q.eq("workspaceId", args.workspaceId)
)
.collect(),
ctx.db.query("sourceCredentials")
.withIndex("by_organization_created", (q) =>
q.eq("organizationId", organizationId)
)
.collect(),
args.accountId
? ctx.db.query("sourceCredentials")
.withIndex("by_org_account_created", (q) =>
q.eq("organizationId", organizationId)
.eq("accountId", args.accountId)
)
.collect()
: [],
]);
// Merge, deduplicate by bindingId, sort by creation time
const docs = [...workspaceDocs, ...organizationDocs, ...accountDocs]
.filter((doc, index, entries) =>
entries.findIndex((candidate) =>
candidate.bindingId === doc.bindingId
) === index
)
.sort((a, b) => b.createdAt - a.createdAt);
return docs.map(mapCredential);
},
});
Listing credentials includes all accessible scopes (account, workspace, org) and deduplicates by binding ID.
Available Providers
Query available credential providers for the deployment:
// From credentials.ts:349-378
export const listCredentialProviders = internalQuery({
args: {},
handler: async () => {
const workosEnabled = Boolean(process.env.WORKOS_API_KEY?.trim());
const enforceManaged = shouldEnforceManagedCredentials();
if (enforceManaged && !workosEnabled) {
return []; // No providers available
}
if (enforceManaged) {
return [{
id: "workos-vault",
label: "Encrypted",
description: "Secrets are stored in WorkOS Vault.",
}];
}
return [{
id: workosEnabled ? "workos-vault" : "local-convex",
label: workosEnabled ? "Encrypted" : "Local",
description: workosEnabled
? "Secrets are stored in WorkOS Vault."
: "Secrets are stored locally in Convex on this machine.",
}];
},
});
Dashboard Workflows
Adding a Credential
- Navigate to workspace settings → Credentials
- Select tool source from dropdown
- Choose scope (account/workspace/organization)
- Select provider (if multiple available)
- Enter secret values:
- local-convex: Enter JSON object or key-value pairs
- workos-vault: Secret is stored via WorkOS OAuth flow
- Optionally add custom headers
- Save credential
Editing a Credential
- Find credential in list
- Click edit
- Update secret values (existing values are not displayed for security)
- Update additional headers if needed
- Save changes
Note: Editing a credential updates all bindings sharing the same credentialId.
Deleting a Credential
Deleting a credential removes access to the tool source. Tasks will fail if they attempt to use tools requiring this credential.
Best Practices
Scope Selection
- Use account scope for personal tokens (GitHub PATs, user OAuth tokens)
- Use workspace scope for environment-specific keys (staging APIs)
- Use organization scope for production shared keys
Secret Management
- Rotate credentials regularly
- Use WorkOS Vault for production deployments
- Never commit credentials to source control
- Monitor credential usage via task logs
Provider Selection
- Use workos-vault for cloud deployments and production
- Use local-convex only for self-hosted development
- Configure
EXECUTOR_DEPLOYMENT_MODE to enforce provider selection
- Use for custom auth schemes not covered by standard auth types
- Include request metadata headers (e.g.,
X-Request-ID)
- Be cautious with headers that may conflict with tool source defaults
- Tools - Configure tool sources that use credentials
- Workspaces - Understand workspace and organization scoping
- Policies - Control access to credentialed tools