Skip to Content
ArchitectureData Model

Data Model

Gestalt stores most persistent state through configured providers. Gestaltd core provisions only the object stores it directly owns through the configured IndexedDB provider: users and managed subject metadata. Authentication grants (including user-facing API tokens) and upstream credentials live in provider-owned IndexedDB stores.

For encryption mechanics, see Security Internals. For choosing a provider, see Configuration.

Ownership summary

OwnerPersistent state
gestaltd coreusers, managed_subjects
Identity providerauthentication_grants, authentication_token_hashes
External credentials providerexternal_credentials
Agent providerProvider-defined agent session, turn, interaction, event, and idempotency state
Workflow providerProvider-defined schedule, event trigger, run, execution ref, and idempotency state

Object stores

users

Created automatically on first login.

FieldTypeNotes
idstring, PKUUID.
emailstring, uniquePlaintext. From the identity provider.
display_namestringPlaintext.
created_attime
updated_attime

Emails and display names are plaintext. If you need encrypted PII at rest, handle it at the storage layer.

managed_subjects

Metadata for service account identities. The canonical subject ID is always service_account:<id>. Authorization relationships for who can manage the subject live in the configured authorization provider as managed_subject resource relationships; runtime grants live as provider-backed app relationships.

FieldTypeNotes
idstring, PKSame value as subject_id; retained for IndexedDB primary-key compatibility.
subject_idstring, uniqueCanonical service-account subject ID such as service_account:release-bot.
kindstringCurrently service_account.
display_namestringPlaintext.
descriptionstringPlaintext.
created_by_subject_idstringCanonical user subject that created the managed subject.
deletedboolDeleted subjects are hidden and their IDs are not reused.
created_attime
updated_attime
deleted_attimeNull until deleted.

external_credentials

Owned by the configured external credentials provider, not gestaltd core. Every record holds a credential of one of three kinds under the unique key (subject, audience, qualifier):

KindHoldsaudiencequalifier
grantTokens minted for a subject’s authorization (OAuth tokens, pasted bearer secrets)Connection ID (e.g. gmail:default)Instance
client_infoThe client identity issued to gestaltd by an authorization server at RFC 7591 dynamic registrationAuthorization server URLRedirect URI
opaqueExternally supplied named fields the store does not interpret (e.g. Datadog api_key + app_key)Connection IDInstance
FieldTypeNotes
idstring, PKUUID.
subjectstringCanonical credential owner such as user:<id>, service_account:<id>, or system:gestaltd for client_info rows.
audiencestringWhere the credential is presented.
qualifierstringPer-kind disambiguator.
kindstringgrant, client_info, or opaque.
access_token_encryptedstringEncrypted. AES-256-GCM. grant only.
refresh_token_encryptedstringEncrypted. AES-256-GCM. grant only.
scopestringPlaintext. Space-separated OAuth scopes. grant only.
expires_attimegrant only. Null if the provider doesn’t report expiry.
last_refreshed_attimegrant only.
refresh_error_countintgrant only. Consecutive failed refreshes. Reset to 0 on success.
client_idstringclient_info only.
client_secret_encryptedstringEncrypted. AES-256-GCM. client_info only.
client_secret_expires_attimeclient_info only.
fields_encryptedstringEncrypted. AES-256-GCM JSON object. opaque only.
metadata_jsonstringPlaintext. Connection metadata from post-connect discovery.
created_attime
updated_attime

CreateCredential is insert-only and fails with ALREADY_EXISTS on a (subject, audience, qualifier) conflict, which gives first-writer-wins convergence when multiple server instances race RFC 7591 dynamic client registration. Reconnecting overwrites the existing record via UpsertCredential.

authentication_grants and authentication_token_hashes

Owned by the configured identity provider, not gestaltd core. Grant records hold metadata for each issued bearer grant. Token-hash records map SHA-256 hashes back to grants for introspection; plaintext bearer tokens are never stored.

StoreKey fieldsNotes
authentication_grantsid, subject, scope, client_id, created_at, expires_at, revokedOne row per grant. subject is the canonical owner such as user:<id>.
authentication_token_hashesid (SHA-256 hash), grant_id, subject, scope, client_id, expires_atHash-only lookup table for bearer validation.

User-facing API token management (/api/v1/tokens) lists, inspects, and revokes these provider-owned grants.

Encryption summary

Credentials are encrypted. Everything else is plaintext.

DataWhereHow
Access tokensexternal_credentials.access_token_encryptedAES-256-GCM
Refresh tokensexternal_credentials.refresh_token_encryptedAES-256-GCM
MCP OAuth client secretsexternal_credentials.client_secret_encryptedAES-256-GCM
Manual credential fieldsexternal_credentials.fields_encryptedAES-256-GCM
Authentication grant hashesauthentication_token_hashes.idOne-way SHA-256
Emails, names, metadata, scopes, timestampsVariousPlaintext

All encrypted fields use the same key derived from server.encryptionKey. There is no per-user or per-provider key separation.

Credential storage granularity

External credentials are keyed by subject, audience, and qualifier.

For grant and opaque credentials, the audience is the connection ID from your config and the qualifier is the instance, which distinguishes multiple accounts within the same connection for providers with post-connect discovery (e.g., a user connected to three Slack workspaces).

The _connection and _instance selectors in the HTTP API and CLI map directly to these storage keys.

Connection modes

  • none: No credentials stored. Operations run without upstream authentication.
  • user: Each calling subject connects individually. Tokens are keyed by subject_id.

Renaming an apps key in config is a breaking change. Stored credentials reference the old name.

Sessions

Platform sessions (OIDC, local, and custom identity providers) are not stored in the IndexedDB provider. They are HTTP-only cookies (session_token) with Secure (when HTTPS), SameSite=Lax, and a default 24-hour TTL (configurable via sessionTtl for OIDC). Sessions cannot be revoked server-side; they expire on TTL. Logout clears the client cookie.