Generated file
This page is generated from hadron-server/prisma/schema.prisma.
To refresh it, run npm run docs:entities from the root of this repo.
Code paths below are not local. The descriptions come from doc comments
written inside hadron-server, so a path like src/lib/tokenCrypto.ts or
prisma/post-push.sql is relative to the
hadron-server repository —
there is no such file here. They point at the implementation and are
volatile; the durable statement is the description itself.
Hadron Data Model¶
Generated by
prisma-markdown
default¶
erDiagram
"users" {
String id PK
String handle UK
Json policy "nullable"
Int github_id UK "nullable"
String github_username UK "nullable"
String google_id UK "nullable"
String apple_id UK "nullable"
IdentityProvider identity_provider "nullable"
String external_id "nullable"
String external_app_id FK "nullable"
DateTime linked_at "nullable"
String name "nullable"
String email UK "nullable"
String avatar_url "nullable"
Role roles
Int max_referrals "nullable"
DateTime created_at
String created_by "nullable"
DateTime updated_at
String updated_by "nullable"
DateTime deleted_at "nullable"
String deleted_by "nullable"
String principal_id FK,UK "nullable"
}
"dashboard_widgets" {
String id PK
String user_id FK
DashboardWidgetType type
Int span
Int row_span
Json config "nullable"
Int position
DateTime created_at
String created_by "nullable"
DateTime updated_at
String updated_by "nullable"
}
"dashboard_widget_presets" {
String id PK
String user_id FK
DashboardWidgetType type
String name
Json config
DateTime created_at
DateTime updated_at
}
"organizations" {
String id PK
String name
String urn UK
Boolean listed_on_marketplace
Int github_installation_id UK "nullable"
String github_app_id "nullable"
String github_app_private_key_encrypted "nullable"
DateTime created_at
String created_by "nullable"
DateTime updated_at
String updated_by "nullable"
DateTime deleted_at "nullable"
String deleted_by "nullable"
Json policy "nullable"
String principal_id FK,UK "nullable"
}
"principals" {
String id PK
String name UK
String kind
String status
DateTime released_at "nullable"
DateTime created_at
DateTime updated_at
}
"org_members" {
String id PK
String organization_id FK
String user_id FK
Role role
Boolean can_invite
DateTime created_at
String created_by "nullable"
DateTime updated_at "nullable"
String updated_by "nullable"
DateTime deleted_at "nullable"
String deleted_by "nullable"
}
"memories" {
String id PK
String organization_id FK "nullable"
String owner_user_id FK "nullable"
String app_id FK "nullable"
String user_id FK "nullable"
String user_memory_of_agent_id FK "nullable"
String shared_memory_of_agent_id FK "nullable"
String urn UK
String name
String short_description "nullable"
String description "nullable"
Json data "nullable"
Json schema "nullable"
String tags
String license "nullable"
String category_0 "nullable"
String category_1 "nullable"
String category_2 "nullable"
String icon_url "nullable"
String hero_url "nullable"
String home_url "nullable"
String source "nullable"
String source_token_encrypted "nullable"
DateTime source_token_expires_at "nullable"
String read_branch "nullable"
String write_branch "nullable"
MemoryVisibility visibility "nullable"
Boolean listed_on_marketplace
Boolean is_encrypted
MemoryClass class
DateTime anonymous_expires_at "nullable"
Boolean requires_license
DateTime last_synced_at "nullable"
SyncStatus sync_status
String sync_error "nullable"
Int pending_edge_count
Boolean accepts_uploads
Boolean vector_index_enabled
EmbeddingSource embedding_source
Int chunk_tokens "nullable"
Int chunk_overlap "nullable"
Boolean force_fixed_size
Int max_rev_count
String kdf_salt "nullable"
Json kdf_params "nullable"
String key_verifier "nullable"
DateTime vector_index_encrypted_ack_at "nullable"
DateTime created_at
String created_by "nullable"
DateTime updated_at
String updated_by "nullable"
DateTime deleted_at "nullable"
String deleted_by "nullable"
DateTime urn_normalized_at "nullable"
String urn_migration_failed_reason "nullable"
String legacy_urn "nullable"
}
"assets" {
String id PK
String memory_id FK "nullable"
String filename
String mime_type
Int size_bytes
String storage_key UK
AssetScanStatus scan_status
String scan_signature "nullable"
Int scan_attempts
DateTime scanned_at "nullable"
DateTime scan_due_at "nullable"
String description "nullable"
DateTime uploaded_at
String uploaded_by "nullable"
DateTime deleted_at "nullable"
}
"nodes" {
String id PK
String memory_id FK
String node_type
String object_type "nullable"
String name
String alias "nullable"
String loc
Boolean is_link
String description "nullable"
String abstract "nullable"
String content "nullable"
String content_hash "nullable"
String abstract_origin_hash "nullable"
DateTime embedding_pending_at "nullable"
DateTime embedding_failed_at "nullable"
Int embedding_attempts
String embedding_error "nullable"
Int tokens "nullable"
String tags
Json properties "nullable"
Json data "nullable"
Int seq "nullable"
String owner_repo "nullable"
Boolean is_runnable "nullable"
String llm_model "nullable"
String ai_agent "nullable"
Int rev_seq
DateTime created_at
String created_by "nullable"
DateTime updated_at
String updated_by "nullable"
DateTime deleted_at "nullable"
String deleted_by "nullable"
}
"node_embeddings" {
String id PK
String node_id FK
String memory_id FK
EmbeddingKind kind
String provider
String model
Int dim
Int chunk_index "nullable"
Int char_start "nullable"
Int char_end "nullable"
String chunk_text "nullable"
DateTime created_at
}
"edges" {
String id PK
String memory_id FK
String source_id FK
String target_id FK
String loc
String name "nullable"
String description "nullable"
Boolean is_runnable "nullable"
Json condition "nullable"
Int priority
Json data "nullable"
DateTime created_at
String created_by "nullable"
DateTime updated_at "nullable"
String updated_by "nullable"
DateTime deleted_at "nullable"
String deleted_by "nullable"
}
"pending_edges" {
String id PK
String source_id FK
String target_id
String loc "nullable"
String name "nullable"
DateTime created_at
String created_by "nullable"
DateTime updated_at "nullable"
String updated_by "nullable"
DateTime deleted_at "nullable"
String deleted_by "nullable"
}
"sessions" {
String id PK
SessionType type
String app_id FK "nullable"
String user_id FK "nullable"
String agent_id FK "nullable"
String worker_id FK "nullable"
String memory_id FK "nullable"
DateTime expires_at "nullable"
String repo "nullable"
String branch "nullable"
Int pr_number "nullable"
String customer_id "nullable"
String language "nullable"
String plan "nullable"
String llm_model "nullable"
String transcript_path "nullable"
String host "nullable"
String tool "nullable"
Int input_tokens "nullable"
Int output_tokens "nullable"
Int turn_count "nullable"
Int error_count "nullable"
String parent_session_id FK "nullable"
String prev_session_id FK "nullable"
String summary "nullable"
Float outcome "nullable"
String outcome_ref "nullable"
Json outcome_meta "nullable"
DateTime started_at
DateTime ended_at "nullable"
DateTime auto_expired_at "nullable"
DateTime created_at
String created_by "nullable"
DateTime updated_at "nullable"
String updated_by "nullable"
DateTime deleted_at "nullable"
String deleted_by "nullable"
}
"usage_events" {
String id PK
String type
String node_loc "nullable"
String node_id FK "nullable"
String memory_id "nullable"
String session_id FK "nullable"
String app_id FK "nullable"
Json action_args "nullable"
String model "nullable"
Int tokens_in "nullable"
Int tokens_out "nullable"
String user_id FK "nullable"
String organization_id FK "nullable"
String provider "nullable"
String key_owner_type "nullable"
BigInt cost_micro_cents "nullable"
DateTime created_at
String created_by "nullable"
DateTime updated_at "nullable"
String updated_by "nullable"
DateTime deleted_at "nullable"
String deleted_by "nullable"
}
"agents" {
String id PK
String organization_id FK "nullable"
String owner_user_id FK "nullable"
String urn UK
String name
String description "nullable"
String system_prompt "nullable"
String system_memory_id "nullable"
AgentVisibility visibility
Boolean listed_on_marketplace
AgentType type
String surfaces
String published_revision_loc "nullable"
String editor_lock_user_id "nullable"
DateTime editor_lock_expires_at "nullable"
Json properties "nullable"
Json memory_provisioning
Json installation_policy
String persona_role "nullable"
String persona_prompt "nullable"
DateTime created_at
String created_by "nullable"
DateTime updated_at "nullable"
String updated_by "nullable"
DateTime deleted_at "nullable"
String deleted_by "nullable"
DateTime urn_normalized_at "nullable"
String urn_migration_failed_reason "nullable"
String legacy_urn "nullable"
}
"agent_memory_items" {
String id PK
String agent_id FK
String memory_id FK
String role
DateTime created_at
String created_by "nullable"
DateTime updated_at "nullable"
String updated_by "nullable"
DateTime deleted_at "nullable"
String deleted_by "nullable"
}
"memory_licenses" {
String id PK
String memory_id FK
String license_type
Int seats
DateTime valid_from
DateTime valid_until
String license_keys
String terms
Boolean activated
DateTime created_at
String created_by "nullable"
DateTime updated_at "nullable"
String updated_by "nullable"
DateTime deleted_at "nullable"
String deleted_by "nullable"
}
"memory_subscriptions" {
String id PK
String memory_id FK
String organization_id FK
String license_id FK "nullable"
Role role
Boolean activated
DateTime created_at
String created_by "nullable"
DateTime updated_at "nullable"
String updated_by "nullable"
DateTime deleted_at "nullable"
String deleted_by "nullable"
}
"hadron_server" {
String id PK
String organization_id "nullable"
String url
LogLevel log_level
String version "nullable"
DateTime last_heartbeat_at "nullable"
DateTime created_at
String created_by "nullable"
DateTime updated_at "nullable"
String updated_by "nullable"
DateTime deleted_at "nullable"
String deleted_by "nullable"
}
"ai_service_configs" {
String id PK
String name
String hadron_server_id FK "nullable"
String organization_id FK "nullable"
String app_id FK "nullable"
String agent_id FK "nullable"
String provider
String model
String api_key_encrypted "nullable"
String api_key_preview "nullable"
Json params "nullable"
Boolean enabled
DateTime created_at
String created_by "nullable"
DateTime updated_at "nullable"
String updated_by "nullable"
}
"server_log" {
String id PK
String server_id FK
LogLevel level
String memory_id "nullable"
String message
Json detail "nullable"
DateTime created_at
String created_by "nullable"
DateTime updated_at "nullable"
String updated_by "nullable"
}
"memory_log" {
String id PK
String memory_id FK
LogLevel level
MemoryLogEventType event_type
String message
Json detail "nullable"
DateTime created_at
String created_by "nullable"
DateTime updated_at "nullable"
String updated_by "nullable"
}
"org_member_invitations" {
String id PK
String member_user_id FK
String recipient_user_id FK
Role role
DateTime expires_at "nullable"
DateTime accepted_at "nullable"
DateTime created_at
String created_by "nullable"
DateTime updated_at "nullable"
String updated_by "nullable"
DateTime deleted_at "nullable"
String deleted_by "nullable"
}
"user_invitations" {
String id PK
String sender_user_id FK "nullable"
String organization_id FK "nullable"
String slug UK
Role user_role "nullable"
Role member_role
String new_user_id "nullable"
String name "nullable"
String email "nullable"
String github_username "nullable"
String phone_number "nullable"
Int max_activations "nullable"
DateTime expires_at "nullable"
DateTime accepted_at "nullable"
DateTime created_at
String created_by "nullable"
DateTime updated_at "nullable"
String updated_by "nullable"
DateTime deleted_at "nullable"
String deleted_by "nullable"
}
"user_invitation_activations" {
String id PK
String invitation_id FK
String user_id FK
DateTime created_at
String created_by "nullable"
DateTime updated_at "nullable"
String updated_by "nullable"
DateTime deleted_at "nullable"
String deleted_by "nullable"
}
"email_verification_tokens" {
String id PK
String email
String token UK
DateTime expires_at
DateTime used_at "nullable"
DateTime created_at
String client_state_hash "nullable"
}
"node_revisions" {
String id PK
String node_id FK
String memory_id "nullable"
String loc
String name
String alias "nullable"
String description "nullable"
String abstract "nullable"
String abstract_origin_hash "nullable"
String content "nullable"
String tags
String node_type "nullable"
String object_type "nullable"
Boolean is_link "nullable"
Json properties "nullable"
Json data "nullable"
Int seq "nullable"
Boolean is_runnable "nullable"
String llm_model "nullable"
String ai_agent "nullable"
String edited_by "nullable"
String edited_by_info "nullable"
String rev_label "nullable"
String changes
String approved_by FK "nullable"
Int rev_no "nullable"
String created_by "nullable"
DateTime created_at
}
"pending_setups" {
String id PK
String user_id FK,UK
String app_id FK
String raw_key_encrypted
Boolean consumed
String created_by "nullable"
DateTime created_at
}
"exchange_connections" {
String id PK
String organization_id FK
String user_id FK
String mailbox_email
String display_name "nullable"
String provider
String tool_connection_id "nullable"
String refresh_token_encrypted "nullable"
Boolean sync_enabled
SyncStatus sync_status
DateTime last_sync_at "nullable"
String last_error "nullable"
String webhook_subscription_id "nullable"
DateTime webhook_expires_at "nullable"
DateTime created_at
String created_by "nullable"
DateTime updated_at
String updated_by "nullable"
DateTime deleted_at "nullable"
String deleted_by "nullable"
}
"connection_grants" {
String id PK
String connection_id FK
String grantee_app_id FK
String granted_by_user_id FK
String scopes
DateTime expires_at "nullable"
DateTime created_at
String created_by "nullable"
DateTime updated_at
String updated_by "nullable"
DateTime deleted_at "nullable"
String deleted_by "nullable"
}
"principal_grants" {
String id PK
String principal_type
String principal_id FK
String organization_id FK
String actions
DateTime expires_at "nullable"
DateTime created_at
String created_by "nullable"
DateTime updated_at
String updated_by "nullable"
DateTime deleted_at "nullable"
String deleted_by "nullable"
}
"slack_connections" {
String id PK
String organization_id FK
String default_app_id FK
String provider
String tool_connection_id UK
String team_id
String team_name
String bot_user_id
String bot_id
String status
String last_error "nullable"
DateTime created_at
String created_by "nullable"
DateTime updated_at
DateTime deleted_at "nullable"
String deleted_by "nullable"
}
"slack_event_receipts" {
String id PK
String connection_id
String envelope_id
DateTime created_at
}
"webfetch_poll_bindings" {
String job_id PK
String source_run_id FK
String organization_id
String app_id
String agent_id "nullable"
String user_id "nullable"
String entry_node_urn
Json policy "nullable"
String credential_secret_id "nullable"
String url
DateTime cancelled_at "nullable"
DateTime created_at
}
"webfetch_event_receipts" {
String id PK
String job_id
String snapshot_hash
String kind
String routed_run_id FK
DateTime created_at
}
"mcp_servers" {
String id PK
String organization_id FK
String slug
String name
String url
String headers_encrypted "nullable"
String tool_allowlist
Boolean enabled
DateTime created_at
String created_by "nullable"
DateTime updated_at
String updated_by "nullable"
}
"secrets" {
String id PK
String owner_type
String owner_id
String name
String kind
Json metadata
String value_encrypted
String key_id "nullable"
DateTime created_at
String created_by "nullable"
DateTime updated_at
String updated_by "nullable"
}
"home_assistant_instances" {
String id PK
String organization_id FK
String slug
String name
String url
String token_encrypted
String op_allowlist
Boolean enabled
DateTime created_at
String created_by "nullable"
DateTime updated_at
String updated_by "nullable"
}
"resend_senders" {
String id PK
String organization_id FK,UK
String from_address
String api_key_encrypted
Boolean enabled
DateTime created_at
String created_by "nullable"
DateTime updated_at
String updated_by "nullable"
}
"waiting_list" {
String id PK
String email
String requested_features "nullable"
DateTime created_at
}
"apps" {
String id PK
String name
Json policy "nullable"
String urn
String organization_id FK "nullable"
String owner_user_id FK "nullable"
CreateUserPermission create_user_permission
IdentifyUserMethod identify_user_method
Int session_timeout_seconds
Int anonymous_ttl_days
AppType app_type
AppMembershipRole role
String description "nullable"
String system_prompt "nullable"
String agent_tools
DateTime expires_at "nullable"
Boolean training_mode
String surfaces
DateTime created_at
String created_by "nullable"
DateTime updated_at "nullable"
String updated_by "nullable"
DateTime deleted_at "nullable"
String deleted_by "nullable"
DateTime uninstalled_at "nullable"
DateTime urn_normalized_at "nullable"
String urn_migration_failed_reason "nullable"
String legacy_urn "nullable"
}
"app_keys" {
String id PK
String app_id FK
String key_hash UK
String key_preview
String label "nullable"
DateTime created_at
String created_by "nullable"
DateTime last_used_at "nullable"
DateTime revoked_at "nullable"
}
"app_members" {
String app_id FK
String user_id FK
String role
DateTime created_at
String created_by "nullable"
DateTime updated_at "nullable"
String updated_by "nullable"
}
"agent_org_grants" {
String org_id FK
String agent_id FK
DateTime activated_at "nullable"
DateTime expires_at "nullable"
DateTime revoked_at "nullable"
String revoked_by "nullable"
DateTime created_at
String created_by "nullable"
DateTime updated_at "nullable"
String updated_by "nullable"
}
"agent_imports" {
String parent_agent_id FK
String imported_agent_id FK
Int position
Boolean required
String agent_org_grant_org_id FK
String agent_org_grant_agent_id
DateTime created_at
String created_by "nullable"
}
"agent_subscriptions" {
String user_id FK
String agent_id FK
DateTime activated_at "nullable"
DateTime expires_at "nullable"
DateTime revoked_at "nullable"
String revoked_by "nullable"
DateTime created_at
String created_by "nullable"
DateTime updated_at "nullable"
String updated_by "nullable"
}
"app_log" {
String id PK
String app_id FK
LogLevel level
String memory_id "nullable"
String session_id "nullable"
String message
Json detail "nullable"
DateTime created_at
String created_by "nullable"
DateTime updated_at "nullable"
String updated_by "nullable"
}
"app_agents" {
String app_id FK
String agent_id FK
DateTime created_at
String created_by "nullable"
DateTime updated_at "nullable"
String updated_by "nullable"
}
"workers" {
String id PK
String app_id FK
String agent_id FK
String name
String slug
String role "nullable"
String prompt_override "nullable"
String memory_id FK "nullable"
DateTime created_at
String created_by "nullable"
DateTime updated_at "nullable"
String updated_by "nullable"
DateTime retired_at "nullable"
String retired_by "nullable"
}
"memory_shares" {
String memory_id FK
String grantee_id FK
String grantor_id FK
MemoryShareRole role
DateTime created_at
String created_by "nullable"
DateTime updated_at "nullable"
String updated_by "nullable"
}
"memory_members" {
String memory_id FK
String user_id FK
MemoryMemberRole role
DateTime created_at
String created_by "nullable"
DateTime updated_at "nullable"
String updated_by "nullable"
}
"user_api_keys" {
String id PK
String user_id FK
String key_hash UK
String key_preview
String label "nullable"
DateTime created_at
String created_by "nullable"
String issued_via "nullable"
DateTime last_used_at "nullable"
DateTime revoked_at "nullable"
}
"impersonation_sessions" {
String id PK
String admin_user_id FK
String target_user_id FK
String organization_id FK
String reason "nullable"
DateTime created_at
DateTime expires_at
DateTime ended_at "nullable"
String ended_by "nullable"
}
"oauth_clients" {
String client_id PK
String client_name
String redirect_uris
DateTime created_at
String created_by "nullable"
DateTime updated_at
String updated_by "nullable"
DateTime deleted_at "nullable"
String deleted_by "nullable"
}
"auth_codes" {
String id PK
String code_hash UK
String client_id FK
String user_id FK
String redirect_uri
String code_challenge
String resource
DateTime expires_at
DateTime redeemed_at "nullable"
DateTime created_at
String created_by "nullable"
}
"auth_handoff_codes" {
String id PK
String code_hash UK
String kind
String user_id FK
String redirect "nullable"
Boolean is_new
String org_id "nullable"
String client_state_hash "nullable"
DateTime expires_at
DateTime redeemed_at "nullable"
DateTime created_at
}
"pending_login_states" {
String state PK
String provider
String redirect "nullable"
String return_to "nullable"
String client_state "nullable"
DateTime expires_at
DateTime created_at
}
"app_runs" {
String id PK
String organization_id
String app_id
String agent_id "nullable"
String user_id "nullable"
AppRunTriggerKind trigger_kind
String trigger_id "nullable"
String entry_node_urn
Json event_data "nullable"
Json data "nullable"
Json hops "nullable"
Json spawn "nullable"
Json policy "nullable"
Int budget_tokens "nullable"
Int budget_actions "nullable"
Int budget_tokens_initial "nullable"
Int budget_actions_initial "nullable"
Int tokens_spent "nullable"
Int actions_spent "nullable"
AppRunStatus status
String cur_node_urn "nullable"
String parent_run_id "nullable"
Json failure "nullable"
Int timeout_ms "nullable"
Int attempts
DateTime published_at "nullable"
DateTime started_at "nullable"
DateTime finished_at "nullable"
DateTime created_at
String created_by "nullable"
}
"agent_schedules" {
String id PK
String organization_id
String app_id
String agent_id "nullable"
String user_id "nullable"
String name
String cron "nullable"
String timezone
DateTime run_at "nullable"
String entry_node_urn
Json event_data "nullable"
Json policy "nullable"
String ai_config_name "nullable"
Boolean enabled
DateTime last_run_at "nullable"
DateTime next_run_at "nullable"
DateTime created_at
String created_by "nullable"
DateTime updated_at "nullable"
String updated_by "nullable"
DateTime deleted_at "nullable"
String deleted_by "nullable"
}
"quota_counters" {
String id PK
String scope_kind
String scope_id
String action
String window
Int used
Int limit
DateTime reset_at
}
"action_tickets" {
String id PK
String organization_id
String app_id "nullable"
String action
String minted_by
String note "nullable"
String consumed_by_run_id "nullable"
DateTime consumed_at "nullable"
DateTime expires_at "nullable"
DateTime created_at
}
"ai_model_prices" {
String id PK
String provider
String model
Int tier
BigInt input_micro_cents_per_mtok
BigInt output_micro_cents_per_mtok
Int markup_basis_points "nullable"
DateTime effective_at
DateTime created_at
String created_by "nullable"
DateTime updated_at "nullable"
String updated_by "nullable"
DateTime deleted_at "nullable"
String deleted_by "nullable"
}
"credit_ledger_entries" {
String id PK
String organization_id FK
String kind
BigInt amount_micro_cents
DateTime expires_at "nullable"
String source_type "nullable"
String source_id "nullable"
String note "nullable"
DateTime created_at
String created_by "nullable"
DateTime updated_at "nullable"
String updated_by "nullable"
DateTime deleted_at "nullable"
String deleted_by "nullable"
}
"agent_webhooks" {
String id PK
String organization_id
String app_id
String agent_id "nullable"
String user_id "nullable"
String name
String secret UK
Json args_schema "nullable"
String entry_node_urn
Json event_data "nullable"
Json policy "nullable"
String ai_config_name "nullable"
Boolean enabled
DateTime last_called_at "nullable"
DateTime created_at
String created_by "nullable"
DateTime updated_at "nullable"
String updated_by "nullable"
DateTime deleted_at "nullable"
String deleted_by "nullable"
}
"mcp_session_state" {
String id PK
String user_id FK
String client_key
String app_id FK "nullable"
String memory_id FK "nullable"
DateTime created_at
DateTime updated_at "nullable"
}
"_AppToUser" {
String A FK
String B FK
}
"users" }o--o| "apps" : externalApp
"users" |o--o| "principals" : principal
"dashboard_widgets" }o--|| "users" : user
"dashboard_widget_presets" }o--|| "users" : user
"organizations" |o--o| "principals" : principal
"org_members" }o--|| "organizations" : organization
"org_members" }o--|| "users" : user
"memories" }o--o| "users" : ownerUser
"memories" }o--o| "organizations" : organization
"memories" }o--o| "agents" : userMemoryOfAgent
"memories" }o--o| "agents" : sharedMemoryOfAgent
"memories" }o--o| "users" : user
"memories" }o--o| "apps" : app
"assets" }o--o| "memories" : memory
"nodes" }o--|| "memories" : memory
"node_embeddings" }o--|| "nodes" : node
"node_embeddings" }o--|| "memories" : memory
"edges" }o--|| "memories" : memory
"edges" }o--|| "nodes" : source
"edges" }o--|| "nodes" : target
"pending_edges" }o--|| "nodes" : source
"sessions" }o--o| "sessions" : parent
"sessions" }o--o| "sessions" : prev
"sessions" }o--o| "apps" : app
"sessions" }o--o| "users" : user
"sessions" }o--o| "agents" : agent
"sessions" }o--o| "workers" : worker
"sessions" }o--o| "memories" : memory
"usage_events" }o--o| "nodes" : node
"usage_events" }o--o| "sessions" : session
"usage_events" }o--o| "apps" : app
"usage_events" }o--o| "users" : user
"usage_events" }o--o| "organizations" : organization
"agents" }o--o| "users" : ownerUser
"agents" }o--o| "organizations" : organization
"agent_memory_items" }o--|| "agents" : agent
"agent_memory_items" }o--|| "memories" : memory
"memory_licenses" }o--|| "memories" : memory
"memory_subscriptions" }o--|| "memories" : memory
"memory_subscriptions" }o--|| "organizations" : organization
"memory_subscriptions" }o--o| "memory_licenses" : license
"ai_service_configs" }o--o| "hadron_server" : hadronServer
"ai_service_configs" }o--o| "organizations" : organization
"ai_service_configs" }o--o| "apps" : app
"ai_service_configs" }o--o| "agents" : agent
"server_log" }o--|| "hadron_server" : server
"memory_log" }o--|| "memories" : memory
"org_member_invitations" }o--|| "org_members" : sender
"org_member_invitations" }o--|| "users" : recipient
"user_invitations" }o--o| "users" : sender
"user_invitations" }o--o| "organizations" : organization
"user_invitation_activations" }o--|| "user_invitations" : invitation
"user_invitation_activations" }o--|| "users" : user
"node_revisions" }o--|| "nodes" : node
"node_revisions" }o--o| "users" : approvedByUser
"pending_setups" |o--|| "users" : user
"pending_setups" }o--|| "apps" : app
"exchange_connections" }o--|| "organizations" : organization
"exchange_connections" }o--|| "users" : user
"connection_grants" }o--|| "exchange_connections" : connection
"connection_grants" }o--|| "apps" : granteeApp
"connection_grants" }o--|| "users" : grantedBy
"principal_grants" }o--|| "users" : principal
"principal_grants" }o--|| "organizations" : organization
"slack_connections" }o--|| "organizations" : organization
"slack_connections" }o--|| "apps" : defaultApp
"webfetch_poll_bindings" }o--|| "app_runs" : sourceRun
"webfetch_event_receipts" }o--|| "app_runs" : routedRun
"mcp_servers" }o--|| "organizations" : organization
"home_assistant_instances" }o--|| "organizations" : organization
"resend_senders" |o--|| "organizations" : organization
"apps" }o--o| "users" : ownerUser
"apps" }o--o| "organizations" : organization
"app_keys" }o--|| "apps" : app
"app_members" }o--|| "apps" : app
"app_members" }o--|| "users" : user
"agent_org_grants" }o--|| "organizations" : organization
"agent_org_grants" }o--|| "agents" : agent
"agent_imports" }o--|| "agents" : parentAgent
"agent_imports" }o--|| "agents" : importedAgent
"agent_imports" }o--|| "agent_org_grants" : agentOrgGrant
"agent_subscriptions" }o--|| "users" : user
"agent_subscriptions" }o--|| "agents" : agent
"app_log" }o--|| "apps" : app
"app_agents" }o--|| "apps" : app
"app_agents" }o--|| "agents" : agent
"workers" }o--|| "apps" : app
"workers" }o--|| "agents" : agent
"workers" }o--o| "memories" : memory
"memory_shares" }o--|| "memories" : memory
"memory_shares" }o--|| "users" : grantee
"memory_shares" }o--|| "users" : grantor
"memory_members" }o--|| "memories" : memory
"memory_members" }o--|| "users" : user
"user_api_keys" }o--|| "users" : user
"impersonation_sessions" }o--|| "users" : adminUser
"impersonation_sessions" }o--|| "users" : targetUser
"impersonation_sessions" }o--|| "organizations" : organization
"auth_codes" }o--|| "users" : user
"auth_codes" }o--|| "oauth_clients" : client
"auth_handoff_codes" }o--|| "users" : user
"credit_ledger_entries" }o--|| "organizations" : organization
"mcp_session_state" }o--|| "users" : user
"mcp_session_state" }o--o| "apps" : app
"mcp_session_state" }o--o| "memories" : memory
"_AppToUser" }o--|| "apps" : App
"_AppToUser" }o--|| "users" : User
users¶
A person who uses Hadron, authenticated via GitHub OAuth (or email in future). N:N to Organization via OrgMember. (Spec 048: signup no longer auto-creates a personal organization — an individual owns Apps/Agents/Memories directly via spec 047 user-owned tenancy.)
Invitation chain: UserInvitationActivation links each User back to their UserInvitation, whose senderUserId points to the inviter, recursively up to the root (senderUserId IS NULL = system seed from Baragaun).
Referral quota (maxReferrals): the maximum number of new
platform users this user is allowed to onboard. Unaccepted
invitations don't count; the count is on accepted activations only.
Quota is tracked transitively up the chain — if A invites B and B
invites C, both A's and B's quotas decrement when C joins. When
sending an invite, UserInvitation.maxActivations must not exceed
the minimum free quota across the inviter's referral chain. There
is a known race condition: if a referral starts onboarding and the
"create account" request would push the chain over quota, the
account is still created (don't block a real human on a counter).
Properties as follows:
id:handleUnique username, e.g. "holger"; displayed as
@holgerin UI and node frontmatterauthorfield, and the owner namespace of a user-owned App/Agent/Memory URN (spec 047). NOT NULL (all users backfilled 2026-07-14): every real signup path assigns one viagenerateUniqueHandle; the DB-generated random default is a backstop for handle-omitting inserts (test fixtures) so those need no churn. Case-insensitive uniqueness is theusers_handle_lower_uniqfunctional index (post-push.sql); the@uniquehere is the plain case-sensitive one.policyUser-layer action-policy link (cor:acl:040:02) — constrains runs made on-behalf-of this user. Same allow-list shape as Organization.policy; NULL = no restriction. Self-authored (updateMyPolicy).
github_id: GitHub numeric account id.github_username: GitHub login.google_idGoogle account id — the OIDC
subclaim (#581). A string of digits, but stored as text: Google documents it as an opaque string and it can exceed 2^53.apple_idApple account id — the Sign-in-with-Apple
subclaim (#584). An opaque team-scoped string (e.g. "001234.abcd1234ef….0102").identity_provider:external_id: App-scoped external user id. Composite-unique withexternalAppId.external_app_idThe App that minted this externalId. Forms a (App, externalId) idempotency key for chatbot user provisioning.
linked_atWhen this app-scoped external identity was linked to the platform User (i.e. when the chatbot user "graduated" to a real account).
name:email: Used for invite matching and email-based auth.avatar_url:roles: Global platform roles (not org-scoped). Default[READER].max_referralsMaximum number of new platform users this user can onboard. See the model-level doc for the referral-quota semantics.
created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:principal_id:
dashboard_widgets¶
A widget the user has placed on their /app/dashboard grid (#772) —
user-scoped layout configuration, one row per placed widget, ordered by
position. Owner-only: no org, no sharing. The whole set is read via
dashboardWidgets and replaced atomically via setDashboardWidgets.
Properties as follows:
id:user_id:type:span: Columns to span on the 12-col desktop grid (clamped on smaller screens).row_span: Rows to span on the fixed-height row grid (#788; clamped server-side to 1-4).configWidget-instance settings, shape widget-defined (#788) — e.g. a stats widget scoped to one org. Stored + round-tripped only; no server-side behavior until dashboardWidgetData goes per-instance.
position: 0-based sort order among the user's widgets.created_at:created_by:updated_at:updated_by:
dashboard_widget_presets¶
A named, user-scoped widget configuration (#806) — a palette entry the
portal COPIES into a placement's config on apply. Deliberately not a live
link: editing a preset never reaches back into placements already seeded
from it, and deleting one never orphans a placement. Generic over widget
types so the next configurable widget inherits the mechanism.
Owner-only in every direction, like DashboardWidget: no org, no sharing.
Properties as follows:
id:user_id:type:name: Display name, unique per (user, type). Trimmed, 1-60 chars.configThe saved settings, shape widget-defined — same 4KB cap and plain-object guard as a placement's
config, since it becomes one verbatim.created_at:updated_at:
organizations¶
A company, team, or group that owns memories and has members. Must have at least one OrgMember with role OWNER (can have many).
Properties as follows:
id:name:urnThe organization's domain name, e.g. "baragaun.com", "micromentor.org". Serves as the namespace for all resources owned by this org (URN prefix).
listed_on_marketplaceOpt-in marketplace catalogue flag (cor:acl:080:04): whether this org is advertised in the marketplace browse. DISTINCT from access/visibility — it governs advertisement, not who may read the org. Contributes to non-member discoverability (cor:acl:080:02). (Spec 048 dropped the former
isVisiblelisting flag with the auto-personal org — every org now lists.)github_installation_idGitHub App installation id. Used to mint ephemeral tokens (1-hour expiry) for memory git-sync against repos within this installation's scope.
github_app_id:github_app_private_key_encryptedAES-256-GCM-encrypted GitHub App private key. Plaintext never stored or shown after initial entry. Encryption key: HADRON_ENCRYPTION_KEY env var.
created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:policyOrg-layer action-policy link of the conjunctive chain (cor:acl:040:02): allow-list JSON
{ "allow": ["action", "prefix.*", "*"] }. NULL = no restriction (the layer is absent, not deny-all). Snapshotted into AppRun.policy at mint — edits apply to FUTURE runs only.principal_id:
principals¶
692 — the unified per-server PRINCIPAL POOL. One row per root name (a user¶
handle OR an organization name), enforcing CROSS-KIND uniqueness by
constraint so a URN <root> atom resolves to exactly one principal without
an @ sigil or kind marker (GitHub's model). Users and Organizations FK
into this; their own handle / urn columns stay the denormalized read
source-of-truth, kept in sync inside the mint transaction.
A released root name is TOMBSTONED (status = 'released', the owning
user/org detached) — the row is kept, and the name @unique spans ALL
statuses, so a released name is reserved FOREVER (indefinite, no recycling —
the identity guarantee the @-sigil rename-safety is replaced by). Reserved
platform words (hadron, platform, …) are pre-seeded rows with
status = 'reserved'. name is written lowercased everywhere, so the plain
(Prisma-modeled) unique doubles as case-insensitive and backs findUnique.
Properties as follows:
id:nameThe canonical root name, stored lowercase.
@uniqueis a plain (case- sensitive) index, which is sufficient because every write lowercases the name (mintPrincipal + the migration backfill) — so it is effectively case-insensitive AND index-backed for thefindUniqueavailability probe. Spans ALL statuses (no status filter), so areleasedtombstone reserves its name forever (indefinite, no recycling).kind: 'user' | 'org' | 'platform'.status: 'active' | 'released' (tombstone) | 'reserved' (platform word).released_at:created_at:updated_at:
org_members¶
Associates a User with an Organization. To remove a User from an Organization, delete the OrgMember row. Must NOT delete the OrgMember if it is the only one with role OWNER (enforced at the resolver, not the schema).
Properties as follows:
id:organization_id:user_id:role:can_inviteWhether this member can invite other existing platform users to the organization. Inviting a new platform user (UserInvitation) is governed separately by User.maxReferrals.
created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
memories¶
A Hadron knowledge graph, owned by an Organization. The four-way
class field (system / app / knowledge / personal) governs
addressing and access — see the MemoryClass enum doc.
Git-backed memories — the root node.yaml in the repo is the
memory manifest, declaring the memory's urn and marketplace
metadata. On import the server reads this manifest and creates or
updates the Memory row. The org portion of the URN must match an
existing Organization on the server.
Git authentication — the server first tries the owning
Organization's githubInstallationId to mint ephemeral tokens
(1-hour expiry). If the repo is outside the GitHub App's scope
(different GitHub org, personal repo, GitLab, etc.), the server
falls back to sourceTokenEncrypted. The GitHub App's private key
(used to sign JWT requests for installation tokens) lives in the
server's environment/secrets manager — not in the database. For
the SaaS server, Baragaun manages this key.
Token encryption — sourceTokenEncrypted uses AES-256-GCM
with a symmetric key from the server's environment
(HADRON_ENCRYPTION_KEY). Plaintext never stored or shown after
initial entry. To replace an expired/revoked token, the user
pastes a new one (overwrites the old encrypted value). Key
rotation: re-encrypt all stored tokens with the new key in a
single migration.
Properties as follows:
id:organization_id:owner_user_idThe user TENANT owner of a user-owned Memory (rooted on the bare
<handle>— #697 v2, no@sigil). DISTINCT fromuserId(the personal/private strict-owner). The exactly-one-oforganizationId/ownerUserIdXOR is ENFORCED —chk_memory_owner_xorin post-push.sql:(organization_id IS NULL) <> (owner_user_id IS NULL)— so exactly one is non-null (the URN backfill dispatches owner KIND off it; don't add a redundant ambiguity branch).app_idThe App this memory belongs to. Required when
classisapp; optional forknowledge/group/personal/private(set when app-scoped, NULL when free-standing); NULL forsystem. Enforced by chk_memory_class_app_id (#653 allowed knowledge + group to attach to an App).user_idOwning user for personal/private-class memories. Bidirectional invariant (CHECK chk_personal_visibility_user):
class IN ('personal','private')⇔userId IS NOT NULL.user_memory_of_agent_idLegacy 005 Agent-pivot for personal memories. Retained for backward-compat queries (008 R-3); not consulted by post-008 access-control, which uses
appIddirectly. A future cleanup spec may collapse it.shared_memory_of_agent_id723 — the IDENTITY of an App's
sharedapp-class memory: the Agent whose¶shared space this is, under
appId. Set ONLY on that runtime memory (NULL on every other row, including the name-addressablecreateMemoryInAppapp-class memories), so@@unique([appId, sharedMemoryOfAgentId])is the find-or-create key forfindOrCreateAppClassMemory— Postgres treats NULLs as distinct, so unset rows are unconstrained. Before #723 that identity was the URN STRING (<app-urn>:<agent-slug>:shared), which forced the row to keep a compound v1 URN (node URNs inside it could not round-trip throughsplitNodeUrn, so portal links to them 404'd) and silently orphaned the memory when its App was renamed. Enforced by chk_memory_shared_agent_scope (set ⇒class='app'ANDapp_id IS NOT NULL).urnGlobally unique identifier, e.g. "baragaun.com:secureid". Derived from
org.urn + ":" + slug.name: Human-friendly display name, e.g. "SecureID Service".short_description: One-line summary for marketplace listings and search results.description: Full description (markdown); shown when expanding a listing.dataFree-form structured metadata for this memory — a client-defined JSON bag with no server-side semantics. Written via the GraphQL createMemory / updateMemory mutations; surfaced read-only over MCP (hadron_list_memories renders it as a
data:line). The MCP surface has no memory-mutation tool, so writes stay GraphQL-only. NULL when unset.schema725 — opt-in per-memory PROPERTY SCHEMA (structured storage): the¶
collections (object types) this memory declares and each one's typed fields. A dedicated column (NOT nested in
data) so a schema'd memory is visible at a glance. Shape + well-formedness validated by src/lib/memorySchema.ts; authored via updateMemory (GraphQL/portal), read-only over MCP for discovery. NULL ⇒ no schema (unstructured). Nodeproperties/objectTypeare validated against it in the enforcement phase (a later PR) — free-form until then.tags: Classification tags for search and filtering.license: License identifier (e.g. "MIT", "CC-BY-4.0", "proprietary").category_0: Primary category (e.g. "Developer Tools").category_1: Secondary category (e.g. "Backend").category_2: Tertiary category (e.g. "Node.js").icon_url: Square icon/logo URL for listings.hero_url: Banner image URL for the detail page.home_url: External homepage or documentation URL.source: Git clone URL; null = DB-only mode (no git backing).source_token_encryptedAES-256-GCM-encrypted access token for repos not covered by the org's GitHub App installation (e.g. personal repos, GitLab, Bitbucket). Plaintext never stored or displayed after initial entry.
source_token_expires_atExpiry of the stored token; server warns via MemoryLogEntry before expiration.
read_branch: Branch to clone/pull from (default: repo default branch).write_branch: Branch for mutations (default: "hadron-updates").visibility035-visibility-enum-cleanup: nullable, no default. Set only for
knowledge(PUBLIC/ORGANIZATION) andgroup(GROUP); NULL for system/app/personal/private (enforced by chk_memory_visibility_class).listed_on_marketplaceOpt-in marketplace catalogue flag (cor:acl:080:04): whether this memory is advertised in the marketplace browse. DISTINCT from
visibility— PUBLIC governs access (reachable by URN), this governs advertisement. "Unlisted but public" is valid; contributes to the owner org's discoverability (cor:acl:080:02).is_encryptedWhether the memory's contents (including asset bytes) are encrypted at rest using AES-256-GCM under HADRON_ENCRYPTION_KEY.
classPer-class governance — see MemoryClass enum doc for the four classes and their addressing rules.
anonymous_expires_atWhen an anonymous (pre-account-creation) memory expires and is reaped by the janitor. NULL for non-anonymous memories.
requires_license: If true, any subscription must have a valid MemoryLicense.last_synced_at: Last successful git sync.sync_status:sync_error: Error message from the last failed sync.pending_edge_countNumber of unresolved PendingEdge records (denormalized for dashboard display).
accepts_uploadsWhether this memory accepts asset uploads. system-class memories typically opt out.
vector_index_enabledOpt-in: when true, this memory's nodes are vector-indexed. DB default is
false; the class-derived default is applied at createMemory — aknowledge-class, non-encrypted memory is created withtrue, all other classes and any encrypted memory stayfalse(FR-001). Flippable per memory.embedding_sourceWhat the vector index is built from. Memory property, NOT a query parameter (FR-002). Only meaningful when vectorIndexEnabled.
chunk_tokensPer-memory chunking dials (FR-003). NULL ⇒ platform default (512 / 64). Consulted only when embeddingSource includes chunks.
chunk_overlap:force_fixed_size: Force fixed-size chunking, bypassing structure-aware (FR-015).max_rev_count621 — cap on how many NodeRevision rows are kept per node in this¶
memory. On each new revision the oldest overflow is pruned. Must be >= 1 (chk_memory_max_rev_count_positive); default 10.
kdf_saltSpec 041 (D-2026-07-06-B) — passphrase-derivation material for a caller-held-key encrypted memory. Parameters, NOT secrets: salt (base64) + scrypt params. NULL when the memory was encrypted with a raw data key (no passphrase) or is not encrypted.
kdf_params:key_verifierenc:v1 encryption of a fixed check string under the data key — lets every unlock verify the supplied key WITHOUT touching node rows (wrong passphrase = typed WRONG_KEY, never garbage plaintext). The server still never holds the key itself.
vector_index_encrypted_ack_atTimestamp the owner acknowledged the embedding-inversion disclosure for an ENCRYPTED memory (FR-026). NULL ⇒ not acknowledged. Gates encrypted-memory indexing via chk_memory_vector_encrypted_ack.
created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:urn_normalized_atOnline URN-shape migration gate (spec 021 FR-032, repurposed for the #697 v1→v2 emission flip). Null = pre-normalization row; set = row normalized onto the flat v2 form (
<root>:<slug>, single colon). Transient. TODO(spec-021-cleanup): drop once the FR-032 migration completes.urn_migration_failed_reasonSpec 021 sentinel for data-defect rows during the FR-032 migration. TODO(spec-021-cleanup): drop alongside urnNormalizedAt.
legacy_urn697 stored alias map: the pre-v2 (v1) URN this row was migrated FROM.¶
Null until the online migration flips
urnto the flat v2 form; then a legacy input (wikilink, export, external link) resolves to this row by matchinglegacyUrn. Indexed, non-unique — resolution disambiguates a rare collision to the live row, else a typed error. Transient with the migration; TODO(#697-cleanup): drop once every reference is v2-native.
assets¶
A user-uploaded file (document, image) bound to a Memory. Bytes
live in object storage (Cloudflare R2 in v1; MinIO for local dev);
this row carries metadata, the storage pointer, and virus-scan
state. Spec: hadron-concept/spec-kits/specs/004-asset-upload/spec.md (sister repo).
Why a separate table (not a nodeType): normalization (asset
and node schemas diverge), scale (raw-material assets shouldn't
bloat the knowledge-node table), and FK-shape (containment is
foreign-key-shaped, not graph-shaped). See
hadron-concept/design-discussions/hadron-platform/2026-04-28-asset-upload-design/010-summary.md
(sister repo) §"Why this summary supersedes 007-summary.md".
URN form (computed, NOT stored): grammar v2
hrn:asset:<root>:<mem...>:assets:<asset.id>, emitted by emitAssetUrnV2
from the holding memory's stored bare urn. The GraphQL Asset.urn docstring
carries the full contract, including the two degraded shapes a parser has to
tolerate and why <mem...> is not always a single atom.
Cross-references from nodes are reserved for a future spec —
Node.attachedAssetId? (one-asset case) or a node_assets join
(many-to-many). Not in v1.
Properties as follows:
id:memory_idThe Memory this asset belongs to; NULL while the asset is staged (uploaded but not yet attached). Janitor sweeps staged assets past the 24h TTL.
filenameOriginal filename as supplied by the user. Never appears in
storageKey.mime_type: RFC 6838 type/subtype.size_bytes: Verified at complete-upload time againstHeadObject.contentLength.storage_keyObject-store key shape:
{orgUrn}/{memoryUrn-segment}/{assetId}.{ext}. Encrypted at rest when the memory is encrypted; plaintext otherwise.scan_status: Virus scan state. Downloads gated on CLEAN.scan_signatureEngine signature name recorded when the scan verdict was BLOCKED (e.g. "Eicar-Signature"). A BLOCKED row is kept as an audit tombstone; its object bytes are deleted (#896).
scan_attemptsNumber of scan attempts so far (inline at complete-upload plus sweep retries). Drives the retry sweep's exponential backoff.
scanned_atWhen the last scan attempt ran (whether or not it produced a verdict). NULL = never attempted. Informational; the sweep selects on
scan_due_at, not this.scan_due_atWhen the next background scan action is due (the #882 embedding-queue pattern: a future-dated stamp IS the queue). PENDING rows: next scan retry. BLOCKED rows: next purge retry for object bytes that survived a failed delete. NULL = nothing pending (CLEAN, or BLOCKED with bytes confirmed gone). Every sweep touch pushes it forward, so no row can monopolize a batch.
description: Optional; agent- or user-provided.uploaded_at:uploaded_byfk to User.id — who uploaded. NULL when the asset was created server-side by an app run with no on-behalf-of user (hadron_store_file); the run id is recorded on the asset's reference node and in the audit log.
deleted_atSoft-delete marker. Janitor sweeps R2 object + hard-deletes after 24h.
nodes¶
A single node in a Memory. loc is the colon-delimited path
within the memory (e.g. auth:tokens), memory-relative. The full
URN is computed memory.urn + ":" + loc (e.g.
baragaun.com:secureid:auth:tokens) and not stored. id is
optional in YAML; the server assigns it on import if missing.
Node types define the node's role in the memory:
- info (default) — living knowledge: specs, guides, facts.
Shown in tree, normal search priority.
- abstract — condensed knowledge: summaries, TL;DRs,
paper abstracts. Highest search priority.
May be the only representation if the source
is archived.
- reference — external source: papers, URLs, legislation,
books. Carries metadata (DOI, ISBN, URL) in
data.
- record — immutable history: chat messages, session
logs. Hidden in tree by default, low search
priority.
- system — agent configuration: conversation designs,
stages, prompt templates, partials. Hidden
in tree, excluded from search.
(The earlier asset node-type was promoted to its own Asset table
in spec 004-asset-upload.)
Column type is String (not enum) because the running schema
pre-dates the formalization; the intended-values list above is the
contract — tightening to a Prisma enum is reserved for a future
cleanup spec.
Chat-root data.scope (spec 017, 2026-05-12) — chat-root nodes
(loc chats:<id>, depth 2, nodeType record) carry a scope field
in data recording the chat's visibility tier (private | shared).
Set at chat creation and immutable thereafter (FR-002).
Denormalized from Memory.class for chat-list query performance;
MUST stay consistent with the holding memory's class per the
invariant validator in src/api/graphql/schema/chat-scope.ts.
Properties as follows:
idUUIDv7 from
node.yamlwhen supplied by the importer (stable across moves); falls back tocuid()for inserts that omitid.memory_id:node_type: See the node-types list in the model doc. Defaultinfo.object_type725 — collection discriminator (the MongoDB-collection equivalent): which¶
domain object this node is an instance of, e.g. "competitor" / "insight". Orthogonal to
nodeType(the platform-kind axis). NULL for an ordinary node. When set on a schema-governed memory it must name a declared object type (enforced at write time — a later phase); free-form until then.name: Human-friendly display name of this node.alias: Alternative name.loc: Colon-delimited path within the memory (e.g. "auth:tokens").is_link:description:abstractParagraph-length summary of this node. Cap is 2000 characters, enforced at the API boundary (not as a DB CHECK — caps are validation, not invariants). Surfacing rules across MCP tools (find/read/list/validate), the
Source: abstract-fallbackmarker onhadron_get_nodecontent-scope fallback, and the RAG-substrate motivation are documented in spec 031 (FR-005 + edge cases). That is the load-bearing reference; this comment is the cold-read pointer for someone scanning the schema.content: The topic content (topic.md full text).content_hashSHA256(yaml+md)[0:8] for change detection. Set by external producers (YAML frontmatter via importer, Git sync). UNCHANGED by spec 032 — that spec adds
abstractOriginHashbelow as a separate column with its own domain (SHA256(content)[0:8]); the two are never compared against each other.abstract_origin_hashSpec 032 — fingerprint of
contentvalue at the timeabstractwas authored. SHA-256 of plaintext content truncated to 8 hex chars (same width ascontentHash, different domain). At read time, compared againstcomputeContentHash(node.content)(viasrc/lib/contentHash.ts) to detect staleness; when the two values differ ANDabstractOriginHashis non-null, the abstract may not reflect current content. Surfaced byhadron_get_node'sSource: abstract-stalemarker,hadron_validate's[stale-abstract]warning, and the upcoming RAG layer. System-managed: never settable via GraphQLNodeInput.embedding_pending_atSet when this node needs (re-)embedding; cleared on success. The single work signal the embedding worker drains (FR-006/FR-007). Operational state only — intentionally NOT versioned on NodeRevision.
embedding_failed_atSet when an embed attempt failed (record, not a work signal). Transient failures keep pending set for retry; permanent failures clear pending (FR-009).
embedding_attempts: Attempt counter for backoff / give-up. Non-null; reset to 0 on success/revoke.embedding_error: Last embed error (diagnosability; surfaced by hadron_validate).tokens: Number of input tokens.tags: Classification tags.properties: Typed key-value structured data; queryable via the API.dataStructured JSON on the node; used for template-variable resolution, user profiles, extracted facts, reference metadata. Returned by
hadron_get_node; written byhadron_update_node(replaces the bag) orhadron_update_node_data(shallow merge).seqSort order among sibling nodes (lower values come first); used by
hadron_get_next_nodeand portal drag-and-drop reordering.owner_repo: GitHub repo slug (e.g. "micromentor/mmdata").is_runnable: If true, this node can be executed as a task.llm_modelLLM model that created or last modified this node (e.g. "claude-opus-4-6", "gpt-4o").
ai_agentAI agent tool that created or last modified this node (e.g. "claude-code", "cursor", "hadron-portal").
rev_seq696: the per-node revision counter. Atomically incremented (the¶
QuotaCounter pattern) inside the tx that writes each NodeRevision, so the value it yields is that snapshot's monotonic
revNo— race-safe, unlike a read-max-then-+1. Backfilled to the node's current revision count.created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
node_embeddings¶
033-rag-vector-retrieval: a stored embedding vector for a node's abstract
or one of its content chunks. Vectors are computed on plaintext at write
time (FR-027) and stored plaintext (pgvector). The embedding column is
Unsupported("vector(768)") — Prisma has no native vector type, so it is
NEVER read/written through the generated client; all access is raw SQL
($queryRaw/$executeRaw). The HNSW index + abstract partial-unique index
live in the migration + post-push.sql (not expressible in Prisma schema).
Properties as follows:
id:node_id:memory_idDenormalized for fast per-memory similarity filtering (search filters by memory before ORDER BY distance).
kind:provider: Embedding provider — "local" | "openai" | "voyage" … v1 is always "local".modelStored model id (FR-014) — e.g. "nomic-embed-text-v1.5". Forward-compat for a deliberate re-embed when the platform model changes.
dim: Vector dimension (sanity / forward-compat guard).chunk_index:char_start:char_end:chunk_text:created_at:
edges¶
Directed edge between two Nodes. The label describes the
relationship — e.g. "imports", "depends_on", "related-to", "cites",
"abstract-of". An edge whose target is not yet synced lives in
PendingEdge until the target arrives.
A directed Node→Node relation, upgraded to a first-class, addressable
entity (it has its own URN hrn:edge:<root>:<memory>:<loc>). Edges
drive task execution/automation and the chatbot/conversation platform,
not just describe graph relationships.
Properties as follows:
id:memory_idDenormalized from the SOURCE node's memory so
loccan be made unique within a memory (a unique index can't span the source-node table). For cross-memory edges this is the source's memory.source_id:target_id:locColon-delimited path within the memory — the edge's identity and the suffix of its URN (
hrn:edge:<root>:<memory>:<loc>). Required; derived from the endpoints + name when not authored (seesrc/lib/edgeLoc.ts). MirrorsNode.loc.nameFree-form relationship name (e.g. imports, depends_on, abstract-of). Optional since spec-037 —
locis now the identity. (Waslabel.)description: Human-readable description of the edge (parity withNode.description).is_runnableIf true, this edge can be executed/run (task automation, conversation transitions). Mirrors
Node.isRunnable.conditionJSONLogic gating expression evaluated by the shared edge-condition evaluator at edge-resolution time. NULL means the edge always fires. Validated against the v1 operator subset and the five variable scopes (
memory.*,chat.*,agent.*,message.data.*,now()/today()). Spec:hadron-concept/spec-kits/specs/001-edge-condition-spec/spec.md(sister repo). User docs: https://docs.hadronmemory.com/reference/edge-conditions/.priorityResolution-order hook for edges sharing a source node; lower fires first, ties broken by insertion order. Reserved space for the future agent-exceptions feature to slot handler edges at the top.
dataFree-form structured payload on the edge (e.g. transition metadata for conversation-design edges).
created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
pending_edges¶
An edge whose target Node has not been synced yet; stored without a foreign key to the target. When the target Node is synced, the PendingEdge is converted to a regular Edge.
Properties as follows:
id:source_id:target_idIntended target node id — NOT a foreign key; resolved at promotion time.
locEdge loc carried so an authored loc survives until promotion; the promoted Edge derives one when this is null. (Mirrors
Edge.loc.)name: Free-form relationship name (waslabel).created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
sessions¶
Tracks a unit of work by an App: an AI/agent conversation, a chatbot interaction, an automation run. Sessions are the top-level tracking concept; UsageEvent rows are recorded within a session.
Properties as follows:
id:type:app_id:user_idThe User this session is attributed to (typically the end-user in a chatbot/automation session; the developer in a DEVELOPER session).
agent_idThe Agent driving the session (NULL for free-form developer sessions not tied to a specific Agent). When
workerIdis set this records the ROLE-agent behind the casting (cor:agt:020:03).worker_id974 — the named identity doing the work: the Worker (casting) this¶
session is bound to (cor:dmo:050:11, cor:agt:020:03). Binding is gated at start (the worker must belong to the session's App); "taken right now" derives from an ACTIVE session bound to the worker.
memory_idThe active write-memory for this session, when there's an unambiguous one. Resolves the multi-writable-memory ambiguity.
expires_atHard expiry; the session reaper (src/jobs/reapSessions.ts, #930) ends sessions past this point. NULL means no hard expiry — those are governed by the reaper's inactivity rule instead. Set by startAgentSession from the App's sessionTimeoutSeconds; startSession deliberately leaves it null (the client contract carries no TTL).
repo: Git repo slug.branch: Git branch name at session start.pr_number: GitHub PR number.customer_id: Tenant/customer identifier (free-form, supplied by the App).language: Spoken ("en", "es") or programming language.plan: The execution plan, when one was prepared.llm_model: LLM model used (e.g. "claude-opus-4-6").transcript_path928 coding-session provenance: where the driving tool's transcript¶
physically lives on
host(a filesystem path in that tool's store).host: #928: machine identifier the session ran on.tool: #928: the tool driving the session ("claude-code", "codex", ...).input_tokens: Total prompt tokens across the session.output_tokens: Total completion tokens across the session.turn_count: Number of human-AI exchanges.error_count: Failed tool calls / action runs.parent_session_id: Sub-agent vertical relation (this session was spawned by a parent).prev_session_idChain horizontal relation (linked journey — this session continues from a previous one).
summary: AI-composed summary at session end.outcome: 0.0-1.0, reported externally.outcome_ref: GUID or URL of authoritative outcome record.outcome_meta: Domain-specific outcome detail.started_at:ended_at:auto_expired_atSet by the reaper (#930) when the session was auto-expired — past
expiresAt, or no heartbeat inside its type's idle window — rather than explicitly ended. Always written together withendedAtand to the same instant, so "active" stays the single predicateendedAt IS NULLand this column records only HOW it ended.created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
usage_events¶
Record of a single operation within a Session (read, write, action run, edge traversal, session summary).
Column type is String (not enum) — intended values:
read | write | action-run | edge-traversal | session-summary.
Tightening to a Prisma enum is reserved for a future cleanup spec.
Properties as follows:
id:type:node_loc: Stored directly so the event survives node deletion.node_id: Set null on node deletion.memory_idMemory the event is attributed to (#796). Stored DIRECTLY at write time like
nodeLoc— deliberately NO relation, unlike this model's five other FKs. Those are allSetNull, and memory-scoped analytics used to reach the memory by joining throughnode, so deleting a node silently erased the event from its memory's history. ASetNullFK here would recreate that loss one level up (on memory deletion), which is exactly what this column exists to prevent.session_id: Set null on session deletion.app_id: The App that authored this event.action_args: Key-value args whentype = 'action-run'.model: LLM model used for this specific event.tokens_in: Input tokens consumed.tokens_out: Output tokens generated.user_idThe on-behalf-of user (billing attribution). Nullable — pure-App service runs have no user. Distinct from the audit
createdBystring.organization_idThe billable organization. Set on every metered event so ledger attribution never needs a join through App.
providerLLM provider that served the call —
modelalone is ambiguous across providers (e.g. claude via anthropic vs bedrock).key_owner_typeAiConfigOwnerType of the API key that served the call (HADRON_SERVER | ORGANIZATION | APP | AGENT). Only HADRON_SERVER spend is platform-paid and thus debitable; BYOK is observability-only.
cost_micro_centsProvider cost snapshot in micro-cents (1e-8 USD), computed from the AiModelPrice row effective at event time. NULL = no price known (or the event carries no tokens). Never recomputed — prices change (FR-003).
created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
agents¶
The Agent definition — what a Builder creates and what the marketplace sells. One Agent row holds the canonical conversation design (system memory), the default AI config, the declared memory-provisioning policy, and the declared installation policy. Spec: 008-agent-installation (2026-05-02).
An App is the deployment of an Agent in some org. The Agent is what is shared across installations; the App is what authenticates, holds members, and accumulates per-deployment data. (PWA analogy: you build an Agent, and when someone installs it, the running instance is called an App.)
Examples: "Mealplan" (the Agent your family meal planner is built from), "Juno" (the mentee chat agent shared across MicroMentor orgs).
Properties as follows:
id:organization_id:owner_user_idUser owner of a user-owned Agent (rooted on the bare
<handle>— #697 v2, no@sigil). The org/owner XOR is ENFORCED —chk_agent_owner_xorin post-push.sql:(organization_id IS NULL) <> (owner_user_id IS NULL)— so exactly one oforganizationId/ownerUserIdis non-null.urnGlobally unique identifier (e.g. "micromentor.org:juno"); derived from
org.urn + ":" + slug.name: Display name of this agent.description:system_prompt: System prompt sent to the LLM at session start.system_memory_idMemory containing conversation designs (stages, prompts, partials). Universally read-only from any App context post-008 (DEVELOP-from-App is dropped per R-10); edits happen in the Agent ownership surface.
visibilityPUBLIC | ORGANIZATION | PERSONAL (035-visibility-enum-cleanup gave agents their own enum). PERSONAL is the creator-only draft state.
listed_on_marketplaceOpt-in marketplace catalogue flag (cor:acl:080:04): whether this agent is advertised in the marketplace browse. DISTINCT from
visibility— PUBLIC governs access, this governs advertisement. Contributes to the owner org's discoverability (cor:acl:080:02) only when that org is activated.typeCoarse behavior hint — see AgentType enum doc. Largely legacy post-008.
surfacesSurfaces this Agent is designed for (free-form per 008 R-9; v1 known values: 'chat', 'slack', 'mcp', 'web'). Distinct from the per-App
surfaces— this is the design-time declaration.published_revision_locLoc of the currently-published revision of the Agent's system memory. Lets the Builder ship draft revisions without affecting running Apps.
editor_lock_user_idOptimistic editor-lock owner (one Builder at a time can hold the edit lock on the system memory).
editor_lock_expires_at: When the editor lock expires (auto-released on stale).propertiesAgent working state (per-user data like family budget, preferences); automatically available to the LLM as context.
memory_provisioningDeclares how this Agent expects per-App memory to be provisioned. Shape:
{ appMemory: 'shared' | 'user' | 'none' }.personalMemoryprovisioning is implicit (lazy-create on first user-attributed write). 008-agent-installation FR-008.installation_policyDeclares membership constraints for any App that installs this Agent. Shape:
{ maxMembers: number | 'unlimited', memberRoles: string[] }.maxMembersenforced at AppMember invite-acceptance time. 008-agent-installation FR-009.persona_rolePersona role — free string with conventions ("backend-engineer"). A dedicated column so roster/casting queries can filter without parsing the prompt.
persona_promptThe persona identity TEMPLATE ("You are {{name}}, a senior backend engineer ...").
{{name}}is bound at casting time with the Worker's name (cor:agt:020:01); by CONVENTION clients compose the bound result into the head of systemPrompt — the server does no injection.created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:urn_normalized_atOnline URN-shape migration gate (spec 021 FR-032, repurposed for the #697 v1→v2 emission flip). Null = pre-normalization row; set = row normalized onto the flat v2 form (
<root>:<slug>, single colon). Transient. TODO(spec-021-cleanup): drop once the FR-032 migration completes.urn_migration_failed_reasonSpec 021 sentinel for data-defect rows during the FR-032 migration. TODO(spec-021-cleanup): drop alongside urnNormalizedAt.
legacy_urn697 stored alias map: the pre-v2 (v1) URN this row was migrated FROM¶
(see Memory.legacyUrn). Null until the online migration flips
urnto v2.
agent_memory_items¶
Associates a Memory with an Agent, with a per-memory access level.
The Agent controls which memories are read-only vs read-write
per-attached-memory. After 005-agent-subscription,
AgentMemoryItem.role is the sole per-knowledge-memory write cap
(the AppAgent-level cap was first relaxed in 005, then the
AppAgent join itself was dropped in 008-agent-installation).
Properties as follows:
id:agent_id:memory_id:role"read"or"read-write". Default"read". Controls whether the Agent can write to this memory.created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
memory_licenses¶
Commercial usage grant of a Memory; defines capacity and terms. Consumed via MemorySubscription.licenseId.
Properties as follows:
id:memory_id:license_typeColumn type is
String(not enum) — intended values:user | app | organization.seats: Number of consumers allowed to use this license.valid_from:valid_until:license_keys: One or more license keys.terms: Usage terms & conditions.activatedIf true, the memory will be merged into the blended graph for the consumer.
created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
memory_subscriptions¶
An Organization subscribes to another Organization's Memory. Governed by a MemoryLicense when the memory requires one.
Access evaluation order (cross-org chain — extended in
005-agent-subscription with the App↔Agent and User↔Agent gates;
008-agent-installation added AgentOrgGrant; 023-app-shape replaced the
direct App.agentId with the AppAgent join (an App can install N Agents)):
1. App ↔ Agent — the App has the Agent installed (an AppAgent
row links them) AND Agent.visibility allows the App's org.
Cross-org installs (App.organizationId != Agent.organizationId)
additionally require an active AgentOrgGrant(App.organizationId,
agentId). Same-org installs auto-pass.
2. Agent ↔ Memory (knowledge / app / system) — current
implementation (src/mcp/access-control.ts#foldAgentIntoAccessMap):
AgentMemoryItem(agent, memory) exists OR memory is the
Agent's systemMemoryId. When cross-org for knowledge:
MemorySubscription(orgB, memory) AND any required
MemoryLicense valid (not expired, seats available).
Known gap (tracked in
issue #115):
this gate does NOT auto-grant access to app-class memories
where memory.appId = App.id AND memory.class = 'app'.
Today an explicit AgentMemoryItem row is required for
app-class memories to be reachable through the MCP/GraphQL
gates. The spec called for auto-grant; the implementation
currently does not.
3. User ↔ Agent (personal-class memory only) —
AgentSubscription(user, agentId) exists AND active per
the predicate, AND Memory.appId = App.id (the per-App
isolation that User Story 2 hangs on — a session through App
A cannot read the personal memory at (B.id, user.id) for
some other App B of the same Agent).
4. Effective role — most restrictive of
AgentMemoryItem.role and MemorySubscription.role. (The
legacy AppAgent.role is dropped post-008 — system memory is
universally read-only from any App.) personal/private-class
memories enforce strict ownership additionally: even an
ADMIN/OWNER who would otherwise satisfy gate 2 cannot read a
personal/private memory they do not own (FR-016).
Worked example (the four-org Juno scenario, 008 vocabulary):
an App in OrgA deploys Juno (Agent in OrgB with
visibility=ORGANIZATION since Alice is a Micromentor org-member;
or PUBLIC for marketplace shape) which references a knowledge
Memory in OrgC. The cross-org App↔Agent gate is satisfied by an
active AgentOrgGrant(OrgA, juno). End-user Alice (personal org
personal-alice) chats through that App; her records land in a
personal Memory at (App.id, alice.id) (Memory.userId = alice,
Memory.appId = App.id, class = personal, visibility = null).
The cross-org write into Alice's
personal memory is granted by AgentSubscription(alice, juno) —
without it, Juno has zero access. If Alice installs Juno into a
second App (different Org), her records there go to a separate
(B.id, alice.id) row — fully isolated from the OrgA installation.
Each gate above must clear in order; failure denies at the right
layer with a typed error.
Properties as follows:
id:memory_id:organization_id: The subscribing organization (not the owner).license_id:roleColumn type is the full
Roleenum, but the intended grant set isCONTRIBUTOR | READER(reader = read-only, contributor = read-write). Tightening reserved for a future cleanup spec.activatedIf true, the memory will be merged into the blended graph for the subscriber.
created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
hadron_server¶
Represents a running Hadron server instance (SaaS or self-hosted). Each organization connects to one server; a server serves one or many organizations.
Properties as follows:
id:organization_idThe organization that owns/operates this server; NULL for the Baragaun SaaS server.
url: Base URL of this server (e.g. "https://server.hadrongraph.io").log_level: Minimum level for ServerLogEntry records.version: Deployed software version.last_heartbeat_at: Last health-check timestamp.created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
ai_service_configs¶
Named AI service configuration (spec 036). Owned by exactly one of
HadronServer / Organization / App / Agent — enforced by the Postgres
CHECK chk_ai_service_config_owner (see prisma/post-push.sql; Prisma
cannot express it). Resolved by name walking App -> Agent -> Org (of
the App, else of the Agent) -> HadronServer; the first ENABLED config
with the requested name wins. Well-known fallback name: 'default'
(conventional extras: 'fast', 'frontier'). Name is unique per owner
via four partial unique indexes (also post-push.sql). Replaces the
legacy Agent.ai_ / App.ai_ columns, which the 036 migration dropped
without backfill (stored values confirmed disposable 2026-06-11).
Properties as follows:
id:name: Lookup name used by the resolution walk; lower-case canonical.hadron_server_id:organization_id:app_id:agent_id:providerProvider identifier; v1 known: 'anthropic' | 'openai' | 'glm' | 'bedrock'. Validated against the registry in src/lib/aiConfig.ts.
model: Model identifier, passed verbatim to the provider.api_key_encryptedAES-256-GCM-encrypted API key (src/lib/tokenCrypto.ts encryptToken). For 'bedrock' this is a JSON-encoded {accessKeyId, secretAccessKey, region} triple, stored opaque.
api_key_preview: Ellipsis + last 4 chars of the raw key for masked display.paramsProvider-specific knobs (maxTokens, thinking, effort, baseUrl, ...). Validated per provider by zod in src/lib/aiConfig.ts.
enabled: Disabled configs are skipped by resolution (walk continues outward).created_at:created_by:updated_at:updated_by:
server_log¶
Log entry for the Hadron server, surfaced in hadron-portal for troubleshooting. Examples: memory sync failures, GitHub App errors, system-level events.
Properties as follows:
id:server_id:level:memory_id: Set if the log relates to a specific memory.message: Human-readable log message.detail: Structured context (stack trace, request/response, etc.).created_at:created_by:updated_at:updated_by:
memory_log¶
Log entry for a specific memory, surfaced in hadron-portal for troubleshooting. Examples: sync failures, license expirations, pending edge resolution, branch conflicts.
Properties as follows:
id:memory_id:level:event_type: Categorizes the log for filtering.message: Human-readable log message.detail: Structured context (sync diff, expired license details, etc.).created_at:created_by:updated_at:updated_by:
org_member_invitations¶
An invitation sent to an existing platform User to join an
Organization. The role cannot be higher than that of the sender.
(Contrast with UserInvitation, which is for inviting new platform
users.)
Properties as follows:
id:member_user_id: fk to OrgMember.id — the sender of the invitation.recipient_user_id: fk to User.id — the recipient (an existing platform user).roleColumn type is the full
Roleenum, but the intended grant set isADMIN | CONTRIBUTOR | READER(you can't invite someone as OWNER — promotion happens via a separate transfer flow).expires_at: When this invitation expires.accepted_at: When the recipient accepted the invitation.created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
user_invitations¶
Invitation sent out to one or more persons that do not yet have a
user account. If maxActivations = 1 this invitation is
single-use. If email or githubUsername is given, the new user
is validated against this — which essentially locks the identity
of the new person and sets maxActivations to 1.
Quota is tracked on User.maxReferrals, not on the invitation.
When a new user is created from this invitation, their
maxReferrals is set by the onboarding process (e.g. from the
sender's remaining quota or a system default).
Race conditions: if a referral arrives at the landing page
after activations exceed maxActivations, the page shows
"The invitation has expired. Please contact the sender." If
onboarding has started and the create-account request would push
the count above the cap, the account is still created (don't block
a real human on a counter).
Role bounds: userRole cannot be higher than the sender's;
memberRole cannot be higher than the sender's OrgMember role in
the target org.
Properties as follows:
id:sender_user_idfk to User.id — the sender; NULL for system/seed invitations from Baragaun.
organization_id: If the new user should be added to an organization on signup.slug: Random string to get a unique invite URL.user_roleColumn type is the full
Roleenum, but the intended set isADMIN | null(most invites grant no global role).member_roleColumn type is the full
Roleenum, but the intended grant set isADMIN | CONTRIBUTOR | READER.new_user_idfk to User.id — set when the invitation is accepted and a User is created.
name: Recipient's name.email:github_username:phone_number:max_activationsMaximum number of times this invitation can be accepted by a new recipient; current count is determined from UserInvitationActivation rows.
expires_at:accepted_atWhen this invitation was accepted and the new User was created. (For multi-use invites this is set on the first acceptance.)
created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
user_invitation_activations¶
One row per User created in response to a UserInvitation. Role
bounds match UserInvitation's: userRole and memberRole cannot
exceed the sender's at activation time.
Properties as follows:
id:invitation_id: The invitation that was accepted.user_id: The newly created User.created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
email_verification_tokens¶
Temporary token for email verification during signup.
Properties as follows:
id:email:token:expires_at:used_at:created_at:client_state_hash505 session-fixation binding: SHA-256 of the browser nonce the portal set¶
(HttpOnly cookie) when the magic link was REQUESTED. Carried into the login handoff code's clientStateHash at verify time so the browser that redeems must be the one that requested the link. NOT NULL in practice (POST /auth/email requires clientState); nullable only for forward-compat.
node_revisions¶
A versioned snapshot of a Node's content. Written every time a Node is updated, so the full history is recoverable.
Properties as follows:
id:node_id:memory_idMemory this snapshot's content belonged to at capture time. A node can be MOVED across memories (moveNode keeps the stable id), so gating revision history by the node's CURRENT memory alone would leak snapshots captured while the node lived in a memory the caller can't read (#602 / Codex P1). Every snapshot site stamps this; nullable only for legacy rows predating the column (backfilled to the node's current memory — the read gate treats NULL as the current memory).
loc:name:alias:description:abstractParagraph summary at the time of the snapshot. Spec 031. Added in the US1 implementation when the live
Node.abstractcolumn landed — revision history would otherwise silently drop abstract edits.abstract_origin_hashSpec 032 — snapshot of
Node.abstractOriginHashat the time of revision capture. Restored verbatim byrestoreNodeRevision; legacy revisions (pre-spec) have NULL here, which short-circuits the staleness check on the live row after restore (no false positives).content:tags:node_type:object_type: #725 — collection discriminator at capture time (parity with nodeType).is_link:properties:data:seq:is_runnable:llm_model:ai_agent:edited_by620: the User.id of the editor, when a user is known — nothing¶
else goes here anymore. Historically this column was overloaded (
reason ?? clientId ?? userId, so it held edit rationales, OAuth identity strings like "github:<login>", or a User id); the #620 migration moved identity strings toeditedByInfo, rationales torevLabel, and backfilled fromcreatedBywhere that held a User id.edited_by_info620: what we know about the editor when no User.id is available.¶
Identity-string forms in the wild: "github:<login>" / "email:<addr>" / "user:<id>" (AuthContext.clientId) and "app:<App.id>" (App-key principals).
rev_label620: user-settable label for this revision (updateNodeRevision¶
mutation; REV_LABEL_MAX_LEN cap). Also captures the #88
reasonarg at snapshot time — the edit rationale that previously squatted ineditedBy(legacy reasons were migrated here).changes620: which node fields the edit that took this snapshot changed¶
(e.g. ["abstract","tags"]). The snapshot itself is the PRE-edit state, so
changesdescribes the delta between this snapshot and the state that replaced it. Empty for legacy rows (NULL reads as []).approved_by620: reserved — User who approved this revision. No write surface¶
yet; the approve flow lands later.
rev_no696: per-node MONOTONIC revision number (1, 2, 3, …) — the stable atom¶
in the
hrn:noderev:<root>:<mem>:<loc...>:<rev>URN (renderedr<N>). Minted race-safely fromNode.revSeq(atomic increment) inside the same tx that inserts the snapshot. Nullable only for legacy rows predating the column (backfilled per-node by createdAt order in the migration).created_by:created_at:
pending_setups¶
Temporary record holding an encrypted API key for a newly
provisioned workspace, consumed during first-time setup. The raw
key is the hdr_app_<…> value, shown once via /setup/info.
Properties as follows:
id:user_id:app_id: The workspace's App; ON DELETE CASCADE.raw_key_encrypted: AES-256-GCM-encrypted raw API key.consumed:created_by:created_at:
exchange_connections¶
A connected email mailbox (identity record). Provider state — refresh
tokens, Graph subscriptions — lives in the provider capability tool
(hadrontool-ms-exchange, #396); mail operations route through
src/lib/emailClient.ts using toolConnectionId.
Properties as follows:
id:organization_id:user_id:mailbox_emailThe mailbox/account email address; (organizationId, provider, mailboxEmail) is unique — provider-scoped so a calendar connection (provider 'google-cal') for the same Google account COEXISTS with its mail connection instead of overwriting it (PR-638 Codex P1).
display_name:providerEmail provider backing this connection — the emailClient dispatch key (#396: one capability tool per provider; 'ms-exchange' today).
tool_connection_idThe provider tool's connection id. NULL only for legacy rows whose token hasn't been transferred yet (scripts/admin-transfer-exchange-tokens.ts).
refresh_token_encryptedLEGACY (#396): AES-256-GCM-encrypted MS Graph refresh token, kept (NULL for new rows) only as the rollback path until the one-time core→tool transfer is verified; a follow-up migration drops it.
sync_enabledSoft toggle — operator can pause sync without removing the connection.
sync_status:last_sync_at:last_error: Error message from the last failed sync.webhook_subscription_id: MS Graph change-notification subscription id.webhook_expires_atSubscription expiry — MS Graph webhooks are short-lived; the renewer cron job watches this column.
created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
connection_grants¶
User→App scoped delegation on a user-owned connection (spec-042 Track B,
593; supersedes the deferred AgentEmailGrant). Lets a connection's OWNER¶
grant a specific App install scoped access (e.g. mail.read) to their
mailbox/calendar. Enforced in requireEmailConnection (owner OR live grant
covering the operation's scope).
- Grantee is the App INSTALL (
granteeAppId), never the Agent — one Agent in two Apps/orgs is two trust contexts (the Juno fixture). - Owner-only create/revoke, no org-admin bypass. Revoke is a SOFT delete;
liveness lookups use
findFirst+deletedAt: null(neverfindUnique). - Cross-org is intended: connection org and grantee-App org may differ; the grant row is the sole authority.
- Multiple live grants per (connection, App) are valid — union-of-scopes — so there is deliberately no unique constraint on the pair.
Properties as follows:
id:connection_id:grantee_app_id: The App install this grant delegates to (the grantee =ctx.appId).granted_by_user_id: The connection owner who created the grant (owner-only create).scopesOperation scopes covered — subset of {mail.read, mail.send, calendar.freebusy, calendar.read}. See src/lib/emailOperationScopes.ts.
expires_at: Optional expiry; NULL = perpetual until revoked.created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
principal_grants¶
An individual action grant — the ADDITIVE half of the grant model (design:grant-model; evaluated by @hadron-memory/access-control as a PrincipalGrantLike). Hands one principal extra management actions within one organization WITHOUT changing their OrgMember role: the gate's effective allow-set is union(role bundle, live grants). Multiple live grants per (principal, org) are valid = union-of-actions (no unique constraint — the ConnectionGrant precedent). Revocation is soft delete; expiry optional (NULL = perpetual). Liveness (deletedAt + expiresAt) is filtered IN the gate's query, never after it.
Properties as follows:
id:principal_type: 'USER' (v1). Reserved for future app/agent principals.principal_id: The granted user (v1: principalType='USER').organization_id:actionsAllowed actions — the access-control matcher grammar: exact ('memory.clone'), prefix ('memory.'), or ''.
expires_at: Optional expiry; NULL = perpetual until revoked.created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
slack_connections¶
One Slack workspace install of the Hadron Slack app (spec 043).
Org-owned at WORKSPACE granularity (decision 1) — unlike the App-owned
TelegramConnection design and the user-owned ExchangeConnection.
defaultAppId names the Hadron App that serves /hadron commands from
this workspace; per-channel App overrides are a later binding table.
The tokens live ENCRYPTED IN THE TOOL (hadrontool-slack) — core stores
identity only, and no token ever appears in a GraphQL field.
Properties as follows:
id:organization_id:default_app_id: The Hadron App that serves commands from this workspace.provider: The slackClient dispatch key ('slack' today).tool_connection_idThe provider tool's connection id — the ops-call handle and the events-ingress resolution key.
team_id: Slack workspace (team) id, e.g. "T0123456789".team_name:bot_user_id: The bot's Slack USER id ("U…") — the sender on messages it posts.bot_idThe bot's BOT id ("B…") — what bot message EVENTS carry as
bot_id. Distinct from botUserId; the bridge loop filter needs BOTH (spec 043 decision 8, PR #594 review). Don't "simplify" to one column.status'ACTIVE' | 'ERROR' — mirrors the tool's connection-status vocabulary (there is no sync loop here, so SyncStatus does not apply).
last_error: Error message from the last permanent provider failure.created_at:created_by:updated_at:deleted_at:deleted_by:
slack_event_receipts¶
Envelope-dedupe receipts for the at-least-once Slack events ingress
(spec 043 decision 5): the tool's persist-before-ack inbox may deliver
an event more than once (a crash between core's 2xx and the tool's row
delete), so dispatch INSERTs here first and drops duplicates on
conflict. connectionId is the TOOL's connection id — dedupe runs
before resolution, so no FK by design.
Properties as follows:
id:connection_id:envelope_id:created_at:
webfetch_poll_bindings¶
Core-owned routing/audit binding for a sidecar-owned webfetch poll job. Poll state, scheduling, snapshots, and credentials remain in the sidecar; this immutable snapshot answers which already-authorized run principal an at-least-once event may wake.
Properties as follows:
job_id:source_run_id:organization_id:app_id:agent_id:user_id:entry_node_urn:policy:credential_secret_idThe resolved
Secretrow a credentialed poll used (#677 — audit provenance; supersedes the #628 credentials_node_urn node scheme).url: Query- and fragment-free URL for audit/event-origin validation.cancelled_at:created_at:
webfetch_event_receipts¶
Durable completed-dispatch receipt for the at-least-once webfetch event ingress. An absent snapshot hash is represented by the empty string so the compound unique key remains effective under Postgres NULL semantics.
Properties as follows:
id:job_id:snapshot_hash:kind:routed_run_id:created_at:
mcp_servers¶
Registered EXTERNAL MCP server — the org-scoped registry behind the
hadrontool-mcp conduit (headless runs calling third-party MCP tools).
The conduit tool is stateless and Hadron-blind (cor:int:030:01); core
owns identity, governance, and the encrypted static headers. A run
reaches a server's tool via the run-tool name mcp__<slug>__<tool>,
and every call is gated by the policy chain as tool.mcp__<slug>__<tool>
plus the run's action budget — this row grants nothing by itself.
Properties as follows:
id:organization_id:slugRun-tool name segment (
mcp__<slug>__<tool>); lowercase [a-z0-9-], unique per org. Keep it short — provider tool-name caps are ~64 chars.name:urlStreamable-HTTP MCP endpoint URL. The conduit refuses private-network hosts unless explicitly allowed (SSRF guard, dev only).
headers_encryptedAES-256-GCM-encrypted JSON object of static request headers (src/lib/tokenCrypto.ts), e.g. {"Authorization":"Bearer ..."} — v1 auth posture (no OAuth). WRITE-ONLY on the API surface: resolvers expose only a hasHeaders boolean, never the value.
tool_allowlist: Exposed-tool allowlist; empty = every tool the server advertises.enabledDisabled servers fail run-tool resolution loud (unknown tool), same posture as an unregistered slug.
created_at:created_by:updated_at:updated_by:
secrets¶
One named, owner-scoped secret — the general secret store (#677).
Polymorphic owner (user | org | app | memory) — deliberately no
FK: entitlement is checked at resolution time under the caller/run
principal, and a dangling owner simply never resolves. A bare name
resolves via the CSS cascade, most-specific-first: memory → app → user
→ org. kind is extensible (generic opaque; webfetch-auth typed:
metadata { type, urlPrefix }, origin-bound at use).
Properties as follows:
id:owner_type:owner_id:name: Cascade-resolution key; unique per owner; lowercase [a-z0-9-], max 64.kind:metadataThe NON-secret, inspectable half — safe on every read surface. A reader can see THAT a secret exists and (webfetch-auth) WHERE it applies without decrypting.
value_encryptedencryptToken(JSON.stringify(payload)) — AES-256-GCM under the platform key (src/lib/tokenCrypto.ts). NEVER selected into a read surface: redaction by construction, same as the aiConfig / HA / Resend / MCP registries. Rotation is overwrite (updateSecret), no versioning (v1).
key_idKey-rotation lineage headroom; unused (null) while HADRON_ENCRYPTION_KEY is the only key.
created_at:created_by:updated_at:updated_by:
home_assistant_instances¶
One registered Home Assistant instance — the org-scoped registry behind
the hadrontool-home-assistant capability tool (hadron-server#640). Same
posture as McpServer: the row grants nothing by itself (every run-time
call walks the policy chain as tool.ha__<slug>__<op> + action budget),
hard delete by design, slug immutable because flow nodes reference it
inside data.tools names.
Properties as follows:
id:organization_id:slugRun-tool name segment (
ha__<slug>__<op>); lowercase [a-z0-9-], unique per org. Keep it short — provider tool-name caps are ~64 chars.name:urlHome Assistant base URL (e.g. https://ha.example.com or a Nabu Casa remote URL). The tool refuses private-network hosts unless its HA_TOOL_ALLOW_PRIVATE_NETWORKS flag is set (self-hosted posture).
token_encryptedAES-256-GCM-encrypted long-lived access token (src/lib/tokenCrypto.ts). WRITE-ONLY on the API surface: resolvers never return it; update replaces it.
op_allowlistExposed-op allowlist over the closed HA_OPS catalog; empty = every op. Lets an admin register a READ-ONLY instance (no call_service).
enabledDisabled instances fail run-tool resolution loud (unknown tool), same posture as an unregistered slug.
created_at:created_by:updated_at:updated_by:
resend_senders¶
One BYO Resend sender for an organization. Core owns and encrypts the API key; the stateless hadrontool-resend sidecar receives it only for an already-authorized send. One sender per org keeps credential selection deterministic and outside model control.
Properties as follows:
id:organization_id:from_addressFixed sender identity accepted by Resend, e.g.
Hadron Agent <agent@example.com>. Not secret.api_key_encryptedAES-256-GCM-encrypted Resend sending-access API key (src/lib/tokenCrypto.ts). WRITE-ONLY on the API surface.
enabled:created_at:created_by:updated_at:updated_by:
waiting_list¶
Pre-launch waitlist signup capture from the marketing site. Holds an email plus an optional free-form "what features would you most want?" answer. Not connected to User (signups happen before the user has an account).
Properties as follows:
id:email:requested_features:created_at:
apps¶
The runtime deployment of an Agent in some Organization — what someone installs when they pick an Agent off the marketplace (or install one from their own org). Holds credentials, surfaces, optional AI override, members, and per-deployment memory pivots. Per Constitution Technology Invariant #6, this is also the unified caller-identity entity that authenticates against the Hadron platform.
Per (org, Agent) install: typically one App per pair. Multi-install
of the same Agent across orgs is supported and explicitly isolates
app- and personal-class memory at (app_id, …) keys.
Auth credentials live on AppKey rows (one or many per App,
independently revocable). The wire-token prefix hdr_app_ and the
appKey developer-facing identifier are deliberately preserved by
008 — there is no rename.
008-agent-installation drops the AppAgent join: an App now
references its Agent directly via agentId. The legacy
OPERATE/DEVELOP role distinction is removed (system memory is
universally read-only from any App context — DEVELOP-from-App is
gone per R-10).
Cascade behavior: hard App deletion cascades to AppKey,
AppLogEntry, AppMember, and the App's app/personal-class Memory
rows. The orphan-retention behavior in 008 FR-015 (personal memory
survives an AppMember removal) is independent — it's a row delete
in app_members, not in apps.
Properties as follows:
id:name: Display name.policyApp-layer action-policy link (cor:acl:040:02) — same allow-list shape as Organization.policy; NULL = no restriction. Org-admin-authored (updateApp). Snapshotted into AppRun.policy at mint.
urnGlobally unique identifier; derived from
org.urn + ":" + slug.Spec 021 (FR-027) — uniqueness is now PARTIAL. The DB-level constraint is
apps_urn_active_uniq WHERE uninstalled_at IS NULL(declared in the 021 migration). The same URN can legitimately appear on multiple rows: one active (uninstalledAt IS NULL) plus N uninstalled history rows. Prisma's field-level@uniquedeclaration was REMOVED because it would generate a non-partial global unique index that fights with the migration.Callers MUST use
findFirst({ where: { urn, uninstalledAt: null } })for active-row resolution per FR-028 resolution precedence; thefindUniquecallers inresolvers.tswere migrated to this pattern in PR #124.organization_id:owner_user_idUser owner of a user-owned App (rooted on the bare
<handle>— #697 v2, no@sigil). The org/owner XOR is ENFORCED —chk_app_owner_xorin post-push.sql:(organization_id IS NULL) <> (owner_user_id IS NULL)— so exactly one oforganizationId/ownerUserIdis non-null.create_user_permission:identify_user_method:session_timeout_seconds:anonymous_ttl_days:app_typeDeprecated — superseded by
surfaces. Scheduled for removal in a follow-up cleanup spec (per 008 R-4); kept in place to avoid spurious portal churn.roleDefault member role applied when the Agent's
installation_policy.memberRolesdoesn't apply.description:system_promptDeprecated — superseded by
Agent.systemMemoryId+Agent.systemPrompt. Scheduled for removal in a follow-up cleanup spec (per 008 R-4).agent_tools: Tools this App's agent can access; default[].expires_at:training_modeMigrated from the dropped
app_agents.training_mode. Sessions through this App are flagged as training data for the Agent's owner to review. 008-agent-installation FR-022.surfacesFree-form per 008 R-9; v1 known values:
'chat','slack','mcp','web'. Surface enforcement (which features are gated on which surfaces) is handled by the portal/app code; chat-tab gating in hadron-portal#187 readssurfaces.includes('chat'). 008-agent-installation FR-014.created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:uninstalled_atSoft-uninstall timestamp per spec 021 FR-025. Null = active install; set = uninstalled (read-only, write-rejected with "app uninstalled" error including cascade to child agents/memories/nodes per FR-026). Reinstall after soft-uninstall is allowed via the partial unique index
apps_urn_active_uniq WHERE uninstalled_at IS NULL(declared in the 021 migration; supersedes Prisma's @unique on urn at the DB level).urn_normalized_atOnline URN-shape migration gate (spec 021 FR-032, repurposed for the #697 v1→v2 emission flip; transient). Null = pre-normalization row (read through the legacy parser); set = row normalized onto the flat v2 form (
<root>:<slug>, single colon). Dropped once 100% of rows are migrated. TODO(spec-021-cleanup): drop this column once the FR-032 migration completes — tracked under the spec-021 cleanup follow-up. Theapps_urn_migration_pending_idxpartial index goes with it.urn_migration_failed_reasonSpec 021 sentinel for data-defect rows during the FR-032 migration. Non-null = the row's urn failed normalization; the migration job skips it on subsequent passes (avoids infinite retry). Transient; dropped with urnNormalizedAt. TODO(spec-021-cleanup): drop alongside urnNormalizedAt.
legacy_urn697 stored alias map: the pre-v2 (v1) URN this row was migrated FROM¶
(see Memory.legacyUrn). Null until the online migration flips
urnto v2.
app_keys¶
An API key for an App, independently revocable; multiple keys per
App. The raw key carries the canonical hdr_app_ prefix and is
shown once at creation; only the SHA-256 hash is stored. (SHA-256,
not bcrypt — bcrypt's per-hash salt would prevent the
constant-time keyHash lookup the auth resolver does. The raw key
is a 256-bit random value, so dictionary attacks against the hash
are not a concern.)
Auth path: the resolver at src/middleware/auth.ts accepts
hdr_app_-prefixed bearer tokens, looks up the AppKey by SHA-256
hash, and resolves the principal to the parent App. Memory access
is then gated by src/mcp/access-control.ts, which reads
App.agentId directly (the legacy AppAgent join was dropped in
008-agent-installation) and folds the Agent's systemMemoryId +
memoryItems into the access map.
Client config field: hadron-client reads the AppKey value from
the appKey field of .hadron/config.json. The env var name
HADRON_CLIENT_KEY describes location (the client process), not
the entity.
Properties as follows:
id:app_id:key_hash: SHA-256 hash of the rawhdr_app_<…>key.key_preview: Last 4 characters of the raw key for masked display.label: Human-friendly name for this key.created_at:created_by:last_used_at:revoked_at:
app_members¶
The User↔App membership join with a per-member role. Spec: 008-agent-installation FR-004.
Per-User lifecycle is reserved for a future spec — there is no
revokedAt / expiresAt column. Removing a member is a row delete;
the user's personal-class Memory at (app_id, user.id) is
retained by default as an orphan (FR-015) and re-attaches
automatically if the user later rejoins the same App.
No deletedAt — soft-delete is meaningless for a pure join.
Properties as follows:
app_id:user_id:roleFree-form string validated at write-time against the parent Agent's
installation_policy.memberRoles(per 008 R-7). Examples:'owner','guide','cook'.installation_policy.maxMembersis enforced at invite-acceptance time (per FR-016).created_at:created_by:updated_at:updated_by:
agent_org_grants¶
The Org↔Agent license — authorizes an Org to (a) create Apps that
deploy this Agent, and (b) bundle this Agent in AgentImport edges
of Agents the Org owns. Spec: 008-agent-installation FR-006 / R-2.
Sibling to AgentSubscription (User↔Agent, from 005). Both share
the lifecycle field shape (activatedAt, expiresAt, revokedAt,
revokedBy) and the same active-status predicate. The two-table
split (rather than a polymorphic subjectType + subjectId)
preserves typed FK integrity and per-table indexing clarity.
For same-org installs (an Org deploying its own Agent), the
grant is auto-provisioned by ensureAgentOrgGrant on first
contact. Cross-org installs require an explicit grant; the v1
surface auto-provisions on first contact (the marketplace UX for
explicit subscribe lands in a future spec).
No deletedAt — soft-delete is redundant with the
revokedAt lifecycle.
Active-status predicate (shared with AgentSubscription via
the isLifecycleActive helper):
activatedAt IS NOT NULL AND revokedAt IS NULL AND (expiresAt IS NULL OR expiresAt > now())
Properties as follows:
org_id:agent_id:activated_at: v1 default:now()on creation since billing is out of scope.expires_at:revoked_atSet by an ADMIN/OWNER of the Agent's owning org via
revokeAgentOrgGrant.revoked_by: fk to User.id — audit trail for who revoked.created_at:created_by:updated_at:updated_by:
agent_imports¶
A directed dependency edge between two Agents — "Agent A imports Agent B as a dep." Spec: 008-agent-installation FR-005 / User Story 3.
Installing the parent (a row in apps with agentId = parent)
cascade-installs required deps (creates additional Apps in the
same org, one per dep). Optional deps install only if the caller
passes installOptional.
v1 enforces 1-level imports only — the dep itself must NOT be
a parent of any AgentImport. Cycles are trivially blocked at write
time (parentAgentId != importedAgentId).
Properties as follows:
parent_agent_idThe Agent that does the importing (the "Suite"). ON DELETE CASCADE.
imported_agent_idThe dep Agent. ON DELETE RESTRICT — deleting it would invalidate every Suite that depends on it; operators must remove the import edge first.
positionSparse-int ordering (per 008 R-8). Builders set values like 100, 200, 300 to allow easy reordering without renumbering.
required: Hard dep vs optional. Defaulttrue.agent_org_grant_org_idComposite fk to
AgentOrgGrant(orgId, agentId)(ON DELETE RESTRICT) — the Builder grant authorizing this import. Spec FR-005 named this columnagent_subscription_id; renamed per 008 R-2 since the universal-grant role for org-level grants lives on the newAgentOrgGrantsibling, notAgentSubscription.agent_org_grant_agent_id:created_at:created_by:
agent_subscriptions¶
The User-side license: "this user is authorized to use this agent."
Auto-provisioned on first user-attributed contact (per
controller/operations/ensureAgentSubscription.ts); revocable by
an ADMIN/OWNER of the Agent's owning org. Authorizes the cross-org
write from the Agent into the user's personal-class Memory.
Sibling to AgentOrgGrant (Org↔Agent, added in
008-agent-installation). Both share the lifecycle field shape and
the same isLifecycleActive predicate; AgentSubscription
continues to express User↔Agent grants while AgentOrgGrant covers
Org↔Agent (App-creation + import-bundling) grants.
Spec: 005-agent-subscription (2026-04-30). Composite primary key
on (userId, agentId).
No deletedAt — soft-delete is redundant with revokedAt.
Active-status predicate (used by the access-resolver):
activatedAt IS NOT NULL AND revokedAt IS NULL AND (expiresAt IS NULL OR expiresAt > now())
Revocation behavior (per FR-028 / FR-029): when revoked, the
user's personal Memory of the Agent is retained by default —
the user owns it, may want to keep their notes after a class ends.
The Agent loses write access (per FR-019); the user retains read
access via direct authenticated queries. The
Memory.userMemoryOfAgentId pivot is preserved so the memory
remains identifiable as "originally from Agent X." Single
exception: if the personal Memory has zero nodes at revocation
time, the platform may hard-delete it alongside the
AgentSubscription (avoids orphan accumulation).
Properties as follows:
user_id:agent_id:activated_atv1 default:
now()on creation since billing is out of scope; future billing introduces a deferred-activation path (activatedAt = null until payment confirms).expires_at: Time-bounded subscriptions; NULL for non-expiring.revoked_atSet by an ADMIN/OWNER of the Agent's owning org via
revokeAgentSubscription.revoked_by: fk to User.id — audit trail.created_at:created_by:updated_at:updated_by:
app_log¶
Log entry for a specific App, surfaced in hadron-portal for troubleshooting.
Properties as follows:
id:app_id:level:memory_id:session_id:message: Human-readable log message.detail: Structured context.created_at:created_by:updated_at:updated_by:
app_agents¶
N:M join between App and Agent, reintroduced by spec 023.
No role column — system memory is read-only to every App
regardless (the pre-008 OPERATE/DEVELOP distinction stays deleted
per spec 023 FR-003). No trainingMode column — training mode
is a per-App setting and lives on App.trainingMode (spec 023
FR-001; deviates from the 2026-05-12-user-installing-app summary
§3 to avoid per-Agent conflicts on an App-wide setting).
Transition note: App.agentId was dropped and consumers
retrofitted in Phase 2b (hadron-server#129,
2026-05-17). Install flows go through the installAgentIntoApp
mutation (spec 023 US1, hadron-server#130)
which writes an AppAgent row.
Cascade behavior: hard App or Agent deletion cascades to
the AppAgent row. Uninstalling an Agent from an App (deleting
the row) does NOT cascade-delete the (app_id, agent_id, *) memories
(FR-005; orphan retention preserved). Duplicate inserts for the
same (app_id, agent_id) are rejected by the composite PK.
Properties as follows:
app_id:agent_id:created_at:created_by:updated_at:updated_by:
workers¶
974 — a Worker: the named casting of an installed Agent into an App¶
(cor:dmo:050:11, cor:agt:020:01). The Agent carries the reusable persona
dressing (personaRole + the personaPrompt template); the Worker is the
local named identity ("Iris") that does attributable work. Casting is
opt-in (an installed agent with no Worker row is just an agent) and
requires the agent installed in the App at cast time; multiple Workers may
cast the SAME agent in one App (Iris and Henry, both backend-engineer).
Name uniqueness: unique per App, case-insensitively — the functional
unique workers_app_name_uniq on (app_id, lower(name)) in post-push.sql
(Prisma cannot express functional uniques). Deliberately NOT filtered on
retirement: a name binds to one casting forever (cor:agt:020:02 — PR
trailers and chat archives reference it, and a recycled name would
silently re-attribute old work).
Lifecycle: workers RETIRE (retiredAt), never soft-delete — the row
must survive the agent's uninstall and keep resolving for history. There
is no deletedAt; the only removal escape is the hard delete of a
never-used miscast (no sessions ever bound, empty working memory).
agentId is Restrict so a referenced casting can never lose its agent
silently (agent deletion is soft everywhere; the hard-delete compensation
paths only ever remove agents without castings).
Properties as follows:
id:app_id:agent_id:name: The worker's name ("Iris") — the identity every surface renders.slug991 — the URN atom:
hrn:worker:<root>:<app-slug>:<slug>. DERIVED from¶nameat cast time (deriveSlugFromName, then-2/-3… until free in the App) and permanent thereafter — there is no rename mutation, and the name it was derived from is itself permanent (cor:agt:020:02).This is a SECOND stored uniqueness (
workers_app_slug_uniq), which cor:agt:020:02 otherwise forbids — the distinction that makes it legal is that it NEVER REFUSES: the mint iterates instead of erroring, so the name collision stays the single allocation primitive a caller can observe. That is exactly what the reverted mention-token unique (PR #979) did not do. The index exists so a race between two casts deriving one base slug cannot produce two workers sharing a URN.roleThe cast-list role this filling answers ("backend-engineer"). Free string with conventions; defaults to the agent's
personaRoleat cast time when the caller names none.prompt_overridePer-worker individuality layered over the agent's template prompt (cor:agt:020:01) — nullable; two castings of one agent share the template and nothing else.
memory_id974 — the worker-scoped working memory in the App's container¶
(
hrn:mem:<owner>:<app-slug>:<worker-slug>), provisioned at cast. Identity is THIS column, never the memory's URN (the #723 rule).created_at:created_by:updated_at:updated_by:retired_atRetirement instant — the name stays reserved forever (cor:agt:020:02). A retired worker stops authoring (team chat's worker-App pin checks retirement at post time) and refuses new session bindings.
retired_by:
memory_shares¶
Asymmetric cross-user grant for personal-class memory (spec 023
US3 / FR-017–FR-022). Composite PK (memoryId, granteeId) — one
row per (Memory, grantee).
Scope: applies ONLY to personal-class Memories (FR-018).
Application-layer guard in createMemoryShare.ts enforces this.
Grantor semantics (FR-019): grantorId is the principal
(Memory.userId), recorded explicitly so the row is
self-describing without a JOIN to memories at read time. The
API actor (which may be an App backend acting on the principal's
behalf) is recorded in createdBy.
No time-window (FR-021): isolation between grantees over time is modeled by creating a new personal Memory per pairing — not by time-slicing one Memory.
Properties as follows:
memory_id:grantee_id:grantor_id:role:created_at:created_by:updated_at:updated_by:
memory_members¶
Symmetric team membership for group-class memory (spec 023 US4 /
FR-026–FR-031). Composite PK (memoryId, userId) — one row per
(Memory, member).
Scope: applies ONLY to group-class Memories (FR-027).
Application-layer guard in addMemoryMember.ts enforces this.
Role capabilities:
reader — read.
writer — read + write nodes within the Memory.
owner — read + write + manage member list, delete/rename the
Memory, change Memory configuration (FR-036).
Last-owner protection (FR-038): the platform rejects removal
or demotion of the sole remaining owner. The MemoryMember row
can still be deleted; the operation-layer guard enforces the
invariant (removeMemoryMember.ts, updateMemoryMemberRole.ts).
FR-031: removing the last non-owner does NOT delete the Memory.
Properties as follows:
memory_id:user_id:role:created_at:created_by:updated_at:updated_by:
user_api_keys¶
User-owned API key — the credential primitive for user-scoped MCP
access (spec 025-oauth-for-mcp; D-2026-05-13-001, D-2026-05-17-001).
Mirrors AppKey but resolves to a User instead of an App. The raw key is
hdr_user_<…>; we store SHA-256 (not bcrypt) for the same reason
AppKey does — direct hash lookup needs a deterministic hash, and the
256-bit raw key (32 bytes / 64 hex chars) already makes dictionary
attacks irrelevant.
Tokens issued by the OAuth /token endpoint share this shape and storage
path so a single resolveUserApiKey middleware validates both
portal-minted and OAuth-issued tokens (FR-015). The path the key
arrived through is recorded in issuedVia (not createdBy); see the
column docstrings.
Audit fields omitted (matches AppKey precedent): no updatedAt /
updatedBy / deletedAt / deletedBy. UserApiKey rows are
append-once with a single terminal state transition (revoked);
revokedAt carries the lifecycle info, and there is no edit
operation. Soft-delete is meaningless for a revocable credential.
Properties as follows:
id:user_id:key_hash: SHA-256 hash of the rawhdr_user_<…>key.key_preview: Last 4 characters of the raw key for masked display.label: Human-friendly name for this key.created_at:created_byActor that minted this key. Schema-wide convention:
User.idof the actor. For self-service v1 this equalsuserId; the column exists for future admin-issued paths (where actor != owner).issued_viaIssuance-path discriminator:
'portal'for createUserApiKey GraphQL mutation;'oauth:<client_id>'for /oauth/token-issued tokens. Distinct fromcreatedByso the audit-actor convention is preserved.last_used_at:revoked_at:
impersonation_sessions¶
Admin-impersonation audit record AND revocation source of truth. One row per impersonation session: an org ADMIN/OWNER (or platform admin) viewing the platform read-only as the target user, scoped to the single organization shared by admin and target. The target's other organizations and all personal-class resources (personal/private memories, chats, mailbox, assets, personal credentials) stay invisible.
A session is LIVE iff endedAt IS NULL AND expiresAt > now(). Expiry is
derived from expiresAt at read time and never written back.
endedAt and endedBy together distinguish how a session finished:
- endedAt set, endedBy set — a person explicitly stopped it.
- endedAt set, endedBy NULL — the system REVOKED it (#878). Every
request re-derives both parties' current org roles, so losing the
admin role, a promotion that breaches peer-or-below, the target
leaving the org, or the admin's account being deleted ends it at once.
- endedAt NULL, past expiresAt — it simply lapsed.
Rows are never deleted by the stop path — the table is the durable audit trail (who impersonated whom, where, when).
Properties as follows:
id:admin_user_id: Actor (the admin). Schema-wide audit-actor convention:User.id.target_user_id: The member being impersonated.organization_id: The shared org that scopes the impersonated session's reads.reasonOptional support/diagnostic reason supplied at start. Capped at the resolver (USER_API_KEY_LABEL_MAX_LEN precedent).
created_at:expires_at:ended_atExplicit-stop timestamp. NULL while live or when the session merely lapsed by TTL.
ended_byUser.idof whoever stopped it early (the admin in v1 — never the target: the impersonated context's self-stop records the actor).
oauth_clients¶
OAuth 2.1 client registry (spec 025-oauth-for-mcp; D-2026-05-17-002).
Rows are populated by POST /oauth/register (RFC 7591 DCR) or seeded
explicitly. Public clients only (no clientSecret); PKCE S256 is the
auth proof. Per Q-025-001, only the 'test' row will be seeded by
prisma/seed.ts (spec 025 task T009 — not yet in this PR);
'claude-desktop' and other real clients self-register via DCR.
PK note: clientId is the PK (no separate cuid id) because
clientId is the natural key — it's the public, immutable identifier
the client sends on every /authorize / /token request and is
referenced by AuthCode.clientId as the FK target. Precedent for a
non-cuid PK: Session.id is caller-generated. Diverges from PR 137's
reference (which carried a separate cuid id) — see
spec-kits/specs/025-oauth-for-mcp/data-model.md §OAuthClient.
Carries the full standard audit-field set per the schema-top convention because edit / delete affordances are anticipated (admin "rename a client", "rotate redirect URIs", etc.); v1 has no such surface, but the columns are cheap to add now and would otherwise need a backfill later.
Properties as follows:
client_idThe public client identifier. DCR-registered:
dcr_<32 hex>(16 bytes / 128 bits). Seeded fixtures use stable identifiers like'test'. Primary key.client_name:redirect_urisAllowed redirect URIs. /authorize must reject any redirect_uri not present here (OAuth 2.1 §3.1.2).
created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
auth_codes¶
Short-lived OAuth authorization code (spec 025-oauth-for-mcp;
RFC 6749 §4.1). Issued by POST /oauth/authorize on
decision=approve, redeemed once at POST /oauth/token. Stores PKCE
S256 challenge + RFC 8707 resource indicators for verification at
redemption. Single-use via race-safe updateMany-with-conditional
redeemedAt: null pattern (FR-013). PKCE / resource mismatch at
/token also marks redeemedAt to prevent probing.
Code storage: the raw code value is hashed (SHA-256) before
storage, matching AppKey.keyHash and UserApiKey.keyHash — auth
codes are credentials in their 10-minute redemption window and should
not be plaintext in the DB. /oauth/authorize returns the raw code
to the client; /oauth/token hashes the supplied code and looks up
by hash. Diverges from PR 137's reference (which stored plaintext)
— see spec-kits/specs/025-oauth-for-mcp/contracts/oauth-endpoints.md
§POST /oauth/token.
Audit fields omitted (matches AppKey precedent): no updatedAt /
updatedBy / deletedAt / deletedBy. AuthCode rows are append-once
with two terminal state transitions (redeemed / expired); redeemedAt
and expiresAt carry the lifecycle info. Soft-delete is meaningless
for a single-use, expiring credential.
Properties as follows:
id:code_hashSHA-256 hash of the raw code value handed to the client. Looked up by hash at /token to avoid plaintext credentials in the DB.
client_id:user_id:redirect_uri:code_challenge: PKCE S256 challenge value (base64url, no padding).resourceRFC 8707 resource indicator(s) captured at /authorize. Multi-valued per RFC 8707 §2; v1 MCP usage populates a single element (the MCP endpoint URL). Application enforces ≥1 element.
expires_at:redeemed_at:created_at:created_byActor that initiated the /authorize request that minted this code. Resolves to the signed-in User; equals
userIdin self-service v1.
auth_handoff_codes¶
Short-lived, single-use handoff codes that keep the session JWT out of
browser-visible URLs (#477). Two kinds share the same shape:
- 'login' — minted after GitHub/email auth; the portal's server
load redeems it at POST /auth/session for the JWT,
which it sets as an HttpOnly cookie. For the GitHub
flow clientStateHash binds the finishing browser to
the login it started (session-fixation defense); the
email flow leaves it null (magic links open cross-device).
- 'exchange_start' — minted by POST /auth/microsoft/start (Bearer-authed,
server-to-server) so the long-lived JWT never rides in
the /auth/microsoft redirect URL.
The raw code is only ever handed out once; the DB stores its SHA-256 hash.
Single use is enforced by an atomic updateMany(redeemedAt: null) claim,
mirroring AuthCode. DB-backed (not in-memory) so it survives multi-worker
deploys, unlike the legacy pendingStates maps.
Properties as follows:
id:code_hash: SHA-256 hash of the raw code handed to the browser/portal.kind: 'login' | 'exchange_start'.user_id:redirect: Post-redemption destination path (login: portal path; exchange: return path).is_new: login only — whether the user was just provisioned (drives /app/setup).org_id: exchange_start only — the org the Exchange connection is being attached to.client_state_hashlogin+GitHub only — SHA-256 of the portal-set browser nonce; the redeemer must present the matching raw nonce (constant-time compared).
expires_at:redeemed_at:created_at:
pending_login_states¶
Durable pending login state for the first-party auth flows (GitHub /
Google / Apple, src/auth/pendingLoginState.ts). Carries the OAuth
state-CSRF token minted at the start route until the provider callback
claims it (atomic delete-by-PK — single use). Replaces the three
in-process pendingStates Maps in server.ts, which died on every
deploy/restart: the provider callback then 400d mid-login — the failure
mode behind the Apple App Review "the app failed to login with Apple"
rejection (Apple's callback is a cross-site form POST after a flow that
can involve slow 2FA). DB-backed rows survive restarts and multi-worker
deploys. No user FK: the row exists BEFORE we know who is logging in.
Properties as follows:
stateThe OAuth
stateCSRF token (random hex) — handed to the IdP at the start route and echoed back on the callback.provider'github' | 'google' | 'apple' — a state minted for one provider can never be claimed by another provider's callback.
redirect: Portal-local path to land on after login (portal-handoff flow).return_to: Spec 025 FR-007 Option-C server-local return path (cookie flow).client_state477 session-fixation binding — the portal's browser nonce, stamped¶
onto the login handoff code at the callback.
expires_at:created_at:
app_runs¶
One headless run (cor:agt:010:02). Simultaneously the audit record AND — v1, D-2026-07-04-D — the activation: created only after the launch gate passes; every action inside the run re-checks status + policy and decrements the live budgets; zeroing a budget or setting CANCELLED halts the run at its next action (the kill switch). Also the outbox row for run.requested.v1 (publishedAt NULL ⇒ sweep republishes).
Properties as follows:
id:organization_id:app_id:agent_id:user_idOn-behalf-of user (cor:agt:010:01, D-2026-07-04-C). NULL ⇒ pure App service identity — no personal/private reach.
trigger_kind:trigger_id:entry_node_urn:event_data:dataRun envelope (D-2026-05-09-013): the mutable bag of fields extracted by flow nodes as the walker advances. Bare field names — conditions address them through the evaluator's
message.data.*scope prefix (plan-multi-node D-MN-1).eventDatastays the immutable trigger payload; on key collision the envelope wins.hopsPer-hop trail (#538): the walked path, one element per COMPLETED hop, appended in the same checkpoint/finish UPDATE the runner already writes.
[{node, edgeOut, startedAt, finishedAt, tokensSpent}]— edgeOut is the routing edge taken FROM the hop (null on the last). Bounded by RUN_MAX_HOPS. A hop that FAILS mid-execution has no element;curNodeUrn+failureidentify it.spawnPARENT-side fan-out state (plan-spawn D-SP-1): set by hadron_spawn — {processItem, itemKey, callback?, itemCallback?, total, dispatching?, callbackFiredAt?}. Its own column, NOT
data: the parent's envelope checkpoint would clobber a mid-hop write there. callbackFiredAt is the exactly-once claim (D-SP-4, atomic SQL).policy: Run-layer allow-list snapshot (the narrowest chain link; plan §4).budget_tokens: Remaining LLM tokens — live-decrement kill switch.budget_actions: Remaining tool/write actions — live-decrement kill switch.budget_tokens_initial832 — the budget this run was MINTED with, so a client can render¶
"spent vs budget".
budgetTokensabove is what REMAINS. NULL on runs minted before the column existed.budget_actions_initial:tokens_spent832 — cumulative spend, incremented in the same write as the budget¶
decrement. Deliberately NOT derived: * a
hopssum under-reports — a hop that failed mid-execution has no element, which is exactly the run an operator is auditing; *initial - remainingunder-reports too — the decrement is CLAMPED (Math.min(spent, budget)) so the remaining budget never goes negative, and an overspending run would report exactly its budget. These counters are unclamped and therefore truthful in overage.NULLABLE with a default: new runs start at 0, while a run minted BEFORE the columns existed is genuinely UNKNOWN rather than zero — reporting 0 for a run whose already-decremented budget proves otherwise would be a false total (PR #854 review, Codex P2).
tokens_spentis backfilled from the hop trail where one exists;actions_spenthas no such signal.KNOWN GAP: a hop aborted by the tool loop (ToolDenied / ToolExecution / AuthorFail) does not increment. The provider has already billed that call, but generateWithTools discards the accumulated usage when a tool throws, so the number is not available at the failure site. Tracked separately; see the GraphQL field docs.
actions_spent:status:cur_node_urn:parent_run_id:failure:timeout_ms:attempts:published_at:started_at:finished_at:created_at:created_by:
agent_schedules¶
Recurring trigger (cor:agt:010, D-2026-07-04-E) — per-kind table mirroring the decided AgentWebhook shape. The scheduler claims rows by (enabled, nextRunAt) and mints an AppRun per due tick.
Properties as follows:
id:organization_id:app_id:agent_id:user_id: On-behalf-of user, captured at creation (cor:agt:010:01).name:cron5-field cron expression, evaluated in
timezone— recurring schedules. XOR withrunAt(chk_agent_schedule_cron_xor_run_at).timezone:run_atOne-shot: run once at this instant (#510). XOR with
cron; the scheduler claim flipsenabledto false instead of advancingnextRunAt. Misfire policy unchanged (overdue runs once).entry_node_urn:event_data:policy: Trigger-layer allow-list (chain link; plan §4).ai_config_name: Per-trigger AI-config override (name into the spec-036 walk).enabled:last_run_at:next_run_atNext due time — the scheduler's work signal. A skipped overlap STILL advances this (plan §7; prevents back-to-back storms).
created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
quota_counters¶
Continuous quota tier (cor:acl:050:01/:04): windowed counters per App and per org for run.execute + llm.tokens. Checked at the launch gate and decremented live during runs.
Properties as follows:
id:scope_kind:scope_id:action:window:used:limit:reset_at:
action_tickets¶
Consumable quota tier (cor:acl:050:01/:04): TAN-style tickets, v1 required only for the outbound-communication action class. The ledger answers "where did they all go?" — every consumption names the run.
Properties as follows:
id:organization_id:app_id:action:minted_by:note:consumed_by_run_id:consumed_at:expires_at:created_at:
ai_model_prices¶
Provider price row for one (provider, model, tier) triple — the rate
source for UsageEvent.costMicroCents AND the debit markup (spec 046
FR-002/FR-003). Effective-dated: lookup takes the latest
effectiveAt <= eventTime with deletedAt IS NULL; price changes
append a new row, never mutate an old one (historical costs stay
auditable — stamped costs are snapshots, so soft-deleting a mispriced
row is safe and is the "retire" lever). Prices are in micro-cents
(1e-8 USD) per MILLION tokens — e.g. $3/MTok input = 30_000_000.
Operator-managed via setAiModelPrice (platform admin); an absent
triple means "no cost computed", never a blocked call.
String columns, no enums — validation lives in src/lib/metering.
Properties as follows:
id:provider:model:tierPricing tier 1-3 (Holger, 2026-07-11): reserved room for future product tiers carrying different markups per (provider, model) WITHOUT another AiModelPrice migration. Everything defaults to and resolves tier 1 until tiers become a real product concept; the semantics of 2/3 are deliberately undefined today. Validated 1-3 in code (setAiModelPrice / getAiModelPrice), no CHECK by convention.
input_micro_cents_per_mtok: Micro-cents per 1M input tokens.output_micro_cents_per_mtok: Micro-cents per 1M output tokens.markup_basis_pointsMarkup applied when debiting platform-key spend at THIS tier, in basis points (2500 = 25%) — integer-exact, no float money. NULL falls back to the CREDIT_MARKUP_PERCENT env (which this column supersedes). Effective-dated with the rest of the row.
effective_at: Row applies to events at/after this instant (until a later row).created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
credit_ledger_entries¶
Append-only monetary ledger for an organization's AI credits (spec 046
FR-005). Signed amounts: grants +, debits −, adjustments ±; balance is
SUM(amountMicroCents). Rows are NEVER mutated, deleted, or reset — this
is the durable billing record (unlike QuotaCounter's daily counters).
expiresAt on grants is stored policy only in Phase 0 (jurisdictions
restrict prepaid expiry — see the spec's legal notes); FIFO expiry
enforcement is a later phase. Automatic usage debits are gated by
BILLING_DEBIT_ENABLED and apply only to platform-key spend (FR-004/006).
Audit columns follow the schema-wide convention; the SANCTIONED
correction is an adjustment row — soft-delete is break-glass ops only
(balance and reads exclude soft-deleted rows, codebase-wide semantic).
Properties as follows:
id:organization_id:kind: 'grant' | 'debit' | 'adjustment' (validated in src/lib/metering).amount_micro_cents: Signed micro-cents (1e-8 USD). Grants positive, debits negative.expires_at: Grant expiry policy field (no enforcement in Phase 0).source_typeProvenance: e.g. ('usage-event', <UsageEvent.id>), ('admin', <userId>), later ('payment', <processor ref>).
source_id:note:created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
agent_webhooks¶
Webhook trigger (D-2026-05-02, spec 040 plan §8) — per-kind table like AgentSchedule. URL shape: /hooks/<secret>/<name>?hpt=<platform JWT>. The opaque secret is the lookup key (no app/agent IDs in the URL); the time-limited platform JWT is the second factor — a leaked URL is useless without it (D-2026-05-02-006).
Properties as follows:
id:organization_id:app_id:agent_id:user_id: On-behalf-of user, captured at creation (cor:agt:010:01).name:secret: Opaque URL secret — unique lookup key, never reused across webhooks.args_schema: JSON Schema for POST args (stored in v1; Ajv enforcement is follow-on).entry_node_urn:event_data:policy:ai_config_name:enabled:last_called_at:created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
mcp_session_state¶
1012 — a client's DURABLE MCP selections (active App, active memory).¶
The in-process selection maps in src/mcp/server.ts live for exactly one MCP
TRANSPORT session, and clients re-initialize transparently after a server
restart — so a selection silently evaporated mid-conversation (#1005). #1009
made that loss legible and recoverable for callers holding a Hadron session
id; this table is the remedy for everyone else, including plain memory work
with no session at all.
Keyed on (userId, clientKey), NOT on the MCP session id — which is exactly
the thing that does not survive. clientKey is the caller's own stable
instance id, supplied per request via the X-Hadron-Client-Id header. A
client that sends none is not persisted at all and keeps the pre-#1012
in-memory behaviour, so nothing regresses for clients that never opt in.
Per-CLIENT rather than per-USER on purpose: one person may drive Claude Desktop, Cursor and the CLI at once, and a per-user row would let one switch the others' active App mid-conversation — turning a lost selection into a silently wrong one, which is worse than the bug being fixed.
This is a CACHE of a choice, never a grant. Both pointers are re-authorized
on read (isAppParticipant for the App, the memory read gate for the
memory), because a stored selection outlives the membership that justified
it (#475 / the #1007 review finding). Both are nullable and SetNull, so a
deleted App or memory degrades to "nothing selected" rather than dangling.
Properties as follows:
id:user_id:client_keyCaller-supplied stable client-instance id (
X-Hadron-Client-Id). Opaque to the server; length-capped and trimmed at the boundary.app_id: The active App selection, or NULL if none. Re-authorized on every read.memory_idThe active memory selection, stored as the PK rather than the URN so a rename cannot strand it. The URN is resolved on read.
created_at:updated_at:
_AppToUser¶
Pair relationship table between apps and users
Properties as follows:
A:B: