Skip to Content
ProvidersExternal Credentials

External Credentials

The external credentials provider is the Gestalt provider responsible for connection credentials. When a user completes OAuth, enters a manual API key, or a credential is provisioned for a service account, Gestalt uses the configured external credentials provider to validate, exchange, store, and resolve the runtime credential for that connection.

External credentials are separate from Gestalt authentication, which identifies the caller, and authorization, which decides what the caller may access. An external credential is the credential Gestalt uses to call an upstream API for a configured connection.

Core concepts

Subject scoping

External credentials are scoped to a canonical subject. Connections resolve credentials for the effective credential subject of the current request.

Integration and connection

A credential belongs to a specific configured connection:

  • Provider is the configured provider name, such as github or slack
  • Connection is the named connection within that provider, such as default or enterprise
  • Instance is an optional sub-identifier for multi-account integrations

This lets the same subject connect more than one account for the same provider when the provider supports instances.

Connection modes

Connection mode controls credential ownership:

ModeCredential ownership
noneNo upstream credential is required.
subjectCredentials are owned by the effective subject of the request.

Use subject for SaaS APIs that should act as the caller. For shared service accounts, service-to-service credentials, and model/API credentials, provision the credential for a service account subject and execute with runAs.

Credential lifecycle

External credentials move through three phases:

  1. Connect time. Gestalt validates the connection auth config, completes OAuth or manual auth, optionally exchanges submitted credentials, and stores the resulting credential.
  2. Runtime. When an app, workflow, or agent-bound connection needs to call an upstream service, Gestalt asks the external credentials provider to resolve a usable runtime credential for the selected connection. Resolution may refresh an expiring access token on demand when the connection auth supports it.
  3. Maintenance. Connections with credentialRefresh opt into provider-owned background maintenance. Gestalt passes the resolved connection metadata to the external credentials provider; a supporting provider can refresh stored credentials before first use.

Credential material can include access tokens, refresh tokens, API keys, and provider-specific token responses. The exact shape depends on the connection’s auth block and the configured external credentials provider.

How external credentials work

External credentials are driven by connection config. An app or agent provider declares a connection, the deployer binds or overrides that connection in server config, and callers select a connection and instance when needed.

connections: github-user: mode: subject auth: type: oauth2 authorizationUrl: https://github.com/login/oauth/authorize tokenUrl: https://github.com/login/oauth/access_token scopes: - repo apps: github: source: ./apps/github/manifest.yaml connections: default: ref: github-user

For automation-owned credentials, define a normal subject-scoped connection and provision the credential for the service account subject that will run it:

connections: model-api: mode: subject auth: type: bearer params: baseUrl: https://models.example.com/v1 providers: agent: default: source: ./providers/agent/example/manifest.yaml connections: model: ref: model-api

The credential material is stored in the external credentials provider under the service account subject, such as service_account:model-runner. Workflows and agents select that subject with runAs.

At runtime, providers that support connection bindings receive resolved runtime connection material from Gestalt. They should treat that material as the connection contract and avoid depending on how the credential was obtained.

First-party external credentials providers

First-party external credentials providers live under valon-technologies/gestalt-providers/externalcredentials.

ProviderUse case
github.com/valon-technologies/gestalt-providers/externalcredentials/defaultStores credentials in the host IndexedDB provider

Configuring providers.externalCredentials

server: providers: indexeddb: main externalCredentials: default providers: indexeddb: main: source: package: github.com/valon-technologies/gestalt-providers/indexeddb/relationaldb version: 0.0.1-alpha.1 config: dsn: ${DATABASE_URL} externalCredentials: default: source: package: github.com/valon-technologies/gestalt-providers/externalcredentials/default version: 0.0.1-alpha.1 config: indexeddb: main

The first-party externalcredentials/default provider stores credentials in the selected host IndexedDB provider.

If you omit providers.externalCredentials, Gestalt uses the first-party externalcredentials/default provider with the selected host IndexedDB provider.

server.providers.externalCredentials selects the host external credentials provider. If more than one entry exists under providers.externalCredentials, set server.providers.externalCredentials explicitly or mark one entry default: true.

providers.externalCredentials is a deployment-wide provider selection. Plugin, workflow, and agent providers do not select their own external credentials provider; they declare or bind connections and let Gestalt resolve those connections through the configured external credentials provider.

Building your own external credentials provider

Manifest

An external credentials provider manifest declares kind: externalcredentials:

kind: externalcredentials source: github.com/your-org/external-credentials/vault version: 0.0.1 displayName: Vault External Credentials description: Stores Gestalt connection credentials in Vault. spec: configSchemaPath: ./externalcredentials_config.json

Provider contract

The Go SDK external credential provider contract has three responsibilities:

AreaMethods
Stored credentialsUpsertCredential, GetCredential, ListCredentials, DeleteCredential
Connection configValidateCredentialConfig
Runtime materialResolveCredential, ExchangeCredential

ResolveCredential is on the hot path for app and agent-provider calls. It receives the configured connection, credential subject, and instance, then returns the token or connection parameters that Gestalt should pass to the target provider.

package vaultcredentials import ( "context" "fmt" "sync" "time" gestalt "github.com/valon-technologies/gestalt/sdk/go" ) type Provider struct { mu sync.Mutex credentials map[string]*gestalt.ExternalCredential lookupByID map[string]string } func New() *Provider { return &Provider{ credentials: map[string]*gestalt.ExternalCredential{}, lookupByID: map[string]string{}, } } func (p *Provider) Configure(_ context.Context, _ string, _ map[string]any) error { return nil } func (p *Provider) UpsertCredential( _ context.Context, req *gestalt.UpsertExternalCredentialRequest, ) (*gestalt.ExternalCredential, error) { credential := req.GetCredential() if credential == nil { return nil, fmt.Errorf("credential is required") } value := cloneCredential(credential) if value.GetId() == "" { value.ID = value.GetSubjectId() + ":" + value.GetConnectionId() + ":" + value.GetInstance() } p.mu.Lock() defer p.mu.Unlock() key := lookupKey(value.GetSubjectId(), value.GetConnectionId(), value.GetInstance()) p.credentials[key] = value p.lookupByID[value.GetId()] = key return cloneCredential(value), nil } func (p *Provider) GetCredential( _ context.Context, req *gestalt.GetExternalCredentialRequest, ) (*gestalt.ExternalCredential, error) { lookup := req.GetLookup() if lookup == nil { return nil, fmt.Errorf("lookup is required") } p.mu.Lock() defer p.mu.Unlock() credential, ok := p.credentials[lookupKey( lookup.GetSubjectId(), lookup.GetConnectionId(), lookup.GetInstance(), )] if !ok { return nil, gestalt.ErrExternalCredentialNotFound } return cloneCredential(credential), nil } func (p *Provider) ListCredentials( _ context.Context, req *gestalt.ListExternalCredentialsRequest, ) (*gestalt.ListExternalCredentialsResponse, error) { p.mu.Lock() defer p.mu.Unlock() response := &gestalt.ListExternalCredentialsResponse{} for _, credential := range p.credentials { if req.GetSubjectId() != "" && credential.GetSubjectId() != req.GetSubjectId() { continue } if req.GetConnectionId() != "" && credential.GetConnectionId() != req.GetConnectionId() { continue } if req.GetInstance() != "" && credential.GetInstance() != req.GetInstance() { continue } response.Credentials = append( response.Credentials, cloneCredential(credential), ) } return response, nil } func (p *Provider) DeleteCredential( _ context.Context, req *gestalt.DeleteExternalCredentialRequest, ) error { p.mu.Lock() defer p.mu.Unlock() key, ok := p.lookupByID[req.GetId()] if !ok { return gestalt.ErrExternalCredentialNotFound } delete(p.lookupByID, req.GetId()) delete(p.credentials, key) return nil } func (p *Provider) ValidateCredentialConfig( context.Context, *gestalt.ValidateExternalCredentialConfigRequest, ) error { return nil } func (p *Provider) ResolveCredential( ctx context.Context, req *gestalt.ResolveExternalCredentialRequest, ) (*gestalt.ResolveExternalCredentialResponse, error) { credential, err := p.GetCredential(ctx, &gestalt.GetExternalCredentialRequest{ Lookup: &gestalt.ExternalCredentialLookup{ SubjectID: req.GetCredentialSubjectId(), ConnectionID: req.GetConnectionId(), Instance: req.GetInstance(), }, }) if err != nil { return nil, err } return &gestalt.ResolveExternalCredentialResponse{ Token: credential.GetAccessToken(), ExpiresAt: credential.GetExpiresAt(), Credential: credential, }, nil } func (p *Provider) ExchangeCredential( context.Context, *gestalt.ExchangeExternalCredentialRequest, ) (*gestalt.ExchangeExternalCredentialResponse, error) { return &gestalt.ExchangeExternalCredentialResponse{}, nil } func lookupKey(subjectID, connectionID, instance string) string { return subjectID + "\x00" + connectionID + "\x00" + instance } func cloneCredential(in *gestalt.ExternalCredential) *gestalt.ExternalCredential { if in == nil { return nil } out := *in out.ExpiresAt = cloneTime(in.ExpiresAt) out.LastRefreshedAt = cloneTime(in.LastRefreshedAt) out.CreatedAt = cloneTime(in.CreatedAt) out.UpdatedAt = cloneTime(in.UpdatedAt) return &out } func cloneTime(in *time.Time) *time.Time { if in == nil { return nil } out := *in return &out }

This example keeps credentials in memory to show the SDK shape. Production providers should persist credentials durably, encrypt at rest, preserve created and updated timestamps, and implement refresh or exchange behavior that matches the supported connection auth modes.

For the full server and provider config structure, read Configuration. For the exact providers.externalCredentials reference, continue to Config File. To understand app connection declarations, read Apps and Provider Manifests. To understand agent connection bindings, read Agent.