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 API tokens. Provider-owned state, such as upstream credentials, workflow state, and agent sessions/turns, is provisioned by those providers instead of by gestaltd core.

MCP OAuth client registrations (oauth_registrations) are stored separately in a raw SQL table managed outside the IndexedDB provider.

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

Ownership summary

OwnerPersistent state
gestaltd coreusers, api_tokens
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
MCP OAuth supportoauth_registrations raw SQL table

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.

external_credentials

Owned by the configured external credentials provider, not gestaltd core. The default provider stores upstream credentials (OAuth tokens, API keys, manual credentials) for each subject’s connected integrations.

FieldTypeNotes
idstring, PKUUID.
subject_idstringCanonical credential owner such as user:<id>, service_account:<id>, or a runtime-supplied system:<id> subject.
integrationstringProvider name from config (e.g., slack).
connectionstringNamed connection within the provider (e.g., default, mcp).
instancestringUser-chosen or discovery-assigned name for multi-account providers.
access_token_encryptedstringEncrypted. AES-256-GCM.
refresh_token_encryptedstringEncrypted. AES-256-GCM. Empty (still encrypted) if no refresh token.
scopesstringPlaintext. Space-separated OAuth scopes.
expires_attimeNull if the provider doesn’t report expiry.
last_refreshed_attime
refresh_error_countintConsecutive failed refreshes. Reset to 0 on success.
metadata_jsonstringPlaintext. Connection metadata from post-connect discovery.
created_attime
updated_attime

Unique index on (subject_id, integration, connection, instance). Reconnecting overwrites the existing record via Put.

api_tokens

Gestalt API tokens (gst_api_*). The plaintext is never stored, only a one-way hash.

FieldTypeNotes
idstring, PKUUID.
owner_kindstringToken owner kind (user or subject).
owner_idstringOwner ID within owner_kind; subject-owned tokens use a canonical non-system subject ID.
credential_subject_idstringSubject whose upstream credentials the token may use.
namestringDisplay name (e.g., automation, cli-token).
hashed_tokenstring, uniqueSHA-256 hash. Plaintext returned once at creation.
scopesstringPlaintext. Space-separated provider names.
permissions_jsonstringJSON-encoded provider operation permissions and provider-scoped action permissions such as provider_dev.attach.
expires_attimeNull for non-expiring tokens.
created_attime
updated_attime

oauth_registrations

Caches dynamic OAuth client registrations for mcp_oauth connections, so Gestalt doesn’t re-register on every request.

FieldTypeNotes
idstring, PK
auth_server_urlstringUpstream authorization server.
redirect_uristringCallback URI.
client_idstringDynamically registered client ID.
client_secret_encryptedstringEncrypted. AES-256-GCM.
expires_attime
authorization_endpointstringDiscovered URL.
token_endpointstringDiscovered URL.
scopes_supportedstring
discovered_attime
created_attime
updated_attime

Unique index on (auth_server_url, redirect_uri).

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 secretsoauth_registrations.client_secret_encryptedAES-256-GCM
API tokensapi_tokens.hashed_tokenOne-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 four dimensions: subject_id, integration, connection, and instance.

The integration is the provider name from your config. The connection is the named connection within that provider (most have just default). The instance distinguishes multiple accounts within the same connection, which matters 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 a plugins key in config is a breaking change. Stored credentials reference the old name.

Sessions

Platform sessions (OIDC, local, and custom authentication 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.