Skip to main content

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

{
  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 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:
{
  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

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

bindingId
string
required
Unique binding identifier (e.g., bind_<uuid>). Used as a stable handle for UI operations.
credentialId
string
required
Connection identifier (e.g., conn_<uuid>). Multiple bindings can share the same credential ID for multi-scope storage.
sourceKey
string
required
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..." })
additionalHeaders
Array<{ name: string, value: string }>
Optional additional HTTP headers to include with tool calls (e.g., custom auth headers, request metadata).Source: packages/database/src/database/validators.ts
boundAuthFingerprint
string
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

Additional Headers

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

  1. Navigate to workspace settings → Credentials
  2. Select tool source from dropdown
  3. Choose scope (account/workspace/organization)
  4. Select provider (if multiple available)
  5. Enter secret values:
    • local-convex: Enter JSON object or key-value pairs
    • workos-vault: Secret is stored via WorkOS OAuth flow
  6. Optionally add custom headers
  7. Save credential

Editing a Credential

  1. Find credential in list
  2. Click edit
  3. Update secret values (existing values are not displayed for security)
  4. Update additional headers if needed
  5. 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

Additional Headers

  • 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

Build docs developers (and LLMs) love