Skip to content

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:
  • handle

    Unique username, e.g. "holger"; displayed as @holger in UI and node frontmatter author field, 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 via generateUniqueHandle; the DB-generated random default is a backstop for handle-omitting inserts (test fixtures) so those need no churn. Case-insensitive uniqueness is the users_handle_lower_uniq functional index (post-push.sql); the @unique here is the plain case-sensitive one.

  • policy

    User-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_id

    Google account id — the OIDC sub claim (#581). A string of digits, but stored as text: Google documents it as an opaque string and it can exceed 2^53.

  • apple_id

    Apple account id — the Sign-in-with-Apple sub claim (#584). An opaque team-scoped string (e.g. "001234.abcd1234ef….0102").

  • identity_provider:
  • external_id: App-scoped external user id. Composite-unique with externalAppId.
  • external_app_id

    The App that minted this externalId. Forms a (App, externalId) idempotency key for chatbot user provisioning.

  • linked_at

    When 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_referrals

    Maximum 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).
  • config

    Widget-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.
  • config

    The 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:
  • urn

    The 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_marketplace

    Opt-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 isVisible listing flag with the auto-personal org — every org now lists.)

  • github_installation_id

    GitHub 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_encrypted

    AES-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:
  • policy

    Org-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:
  • name

    The canonical root name, stored lowercase. @unique is 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 the findUnique availability probe. Spans ALL statuses (no status filter), so a released tombstone 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_invite

    Whether 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 encryptionsourceTokenEncrypted 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_id

    The user TENANT owner of a user-owned Memory (rooted on the bare <handle> — #697 v2, no @ sigil). DISTINCT from userId (the personal/private strict-owner). The exactly-one-of organizationId/ownerUserId XOR is ENFORCED — chk_memory_owner_xor in 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_id

    The App this memory belongs to. Required when class is app; optional for knowledge/group/personal/private (set when app-scoped, NULL when free-standing); NULL for system. Enforced by chk_memory_class_app_id (#653 allowed knowledge + group to attach to an App).

  • user_id

    Owning 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_id

    Legacy 005 Agent-pivot for personal memories. Retained for backward-compat queries (008 R-3); not consulted by post-008 access-control, which uses appId directly. A future cleanup spec may collapse it.

  • shared_memory_of_agent_id

    723 — the IDENTITY of an App's shared app-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-addressable createMemoryInApp app-class memories), so @@unique([appId, sharedMemoryOfAgentId]) is the find-or-create key for findOrCreateAppClassMemory — 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 through splitNodeUrn, 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' AND app_id IS NOT NULL).

  • urn

    Globally 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.
  • data

    Free-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.

  • schema

    725 — 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). Node properties/objectType are 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_encrypted

    AES-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_at

    Expiry 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").
  • visibility

    035-visibility-enum-cleanup: nullable, no default. Set only for knowledge (PUBLIC/ORGANIZATION) and group (GROUP); NULL for system/app/personal/private (enforced by chk_memory_visibility_class).

  • listed_on_marketplace

    Opt-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_encrypted

    Whether the memory's contents (including asset bytes) are encrypted at rest using AES-256-GCM under HADRON_ENCRYPTION_KEY.

  • class

    Per-class governance — see MemoryClass enum doc for the four classes and their addressing rules.

  • anonymous_expires_at

    When 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_count

    Number of unresolved PendingEdge records (denormalized for dashboard display).

  • accepts_uploads

    Whether this memory accepts asset uploads. system-class memories typically opt out.

  • vector_index_enabled

    Opt-in: when true, this memory's nodes are vector-indexed. DB default is false; the class-derived default is applied at createMemory — a knowledge-class, non-encrypted memory is created with true, all other classes and any encrypted memory stay false (FR-001). Flippable per memory.

  • embedding_source

    What the vector index is built from. Memory property, NOT a query parameter (FR-002). Only meaningful when vectorIndexEnabled.

  • chunk_tokens

    Per-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_count

    621 — 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_salt

    Spec 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_verifier

    enc: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_at

    Timestamp 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_at

    Online 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_reason

    Spec 021 sentinel for data-defect rows during the FR-032 migration. TODO(spec-021-cleanup): drop alongside urnNormalizedAt.

  • legacy_urn

    697 stored alias map: the pre-v2 (v1) URN this row was migrated FROM.

    Null until the online migration flips urn to the flat v2 form; then a legacy input (wikilink, export, external link) resolves to this row by matching legacyUrn. 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_id

    The Memory this asset belongs to; NULL while the asset is staged (uploaded but not yet attached). Janitor sweeps staged assets past the 24h TTL.

  • filename

    Original filename as supplied by the user. Never appears in storageKey.

  • mime_type: RFC 6838 type/subtype.
  • size_bytes: Verified at complete-upload time against HeadObject.contentLength.
  • storage_key

    Object-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_signature

    Engine 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_attempts

    Number of scan attempts so far (inline at complete-upload plus sweep retries). Drives the retry sweep's exponential backoff.

  • scanned_at

    When 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_at

    When 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_by

    fk 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_at

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

  • id

    UUIDv7 from node.yaml when supplied by the importer (stable across moves); falls back to cuid() for inserts that omit id.

  • memory_id:
  • node_type: See the node-types list in the model doc. Default info.
  • object_type

    725 — 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:
  • abstract

    Paragraph-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-fallback marker on hadron_get_node content-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_hash

    SHA256(yaml+md)[0:8] for change detection. Set by external producers (YAML frontmatter via importer, Git sync). UNCHANGED by spec 032 — that spec adds abstractOriginHash below as a separate column with its own domain (SHA256(content)[0:8]); the two are never compared against each other.

  • abstract_origin_hash

    Spec 032 — fingerprint of content value at the time abstract was authored. SHA-256 of plaintext content truncated to 8 hex chars (same width as contentHash, different domain). At read time, compared against computeContentHash(node.content) (via src/lib/contentHash.ts) to detect staleness; when the two values differ AND abstractOriginHash is non-null, the abstract may not reflect current content. Surfaced by hadron_get_node's Source: abstract-stale marker, hadron_validate's [stale-abstract] warning, and the upcoming RAG layer. System-managed: never settable via GraphQL NodeInput.

  • embedding_pending_at

    Set 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_at

    Set 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.
  • data

    Structured JSON on the node; used for template-variable resolution, user profiles, extracted facts, reference metadata. Returned by hadron_get_node; written by hadron_update_node (replaces the bag) or hadron_update_node_data (shallow merge).

  • seq

    Sort order among sibling nodes (lower values come first); used by hadron_get_next_node and 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_model

    LLM model that created or last modified this node (e.g. "claude-opus-4-6", "gpt-4o").

  • ai_agent

    AI agent tool that created or last modified this node (e.g. "claude-code", "cursor", "hadron-portal").

  • rev_seq

    696: 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_id

    Denormalized 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".
  • model

    Stored 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_id

    Denormalized from the SOURCE node's memory so loc can 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:
  • loc

    Colon-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 (see src/lib/edgeLoc.ts). Mirrors Node.loc.

  • name

    Free-form relationship name (e.g. imports, depends_on, abstract-of). Optional since spec-037 — loc is now the identity. (Was label.)

  • description: Human-readable description of the edge (parity with Node.description).
  • is_runnable

    If true, this edge can be executed/run (task automation, conversation transitions). Mirrors Node.isRunnable.

  • condition

    JSONLogic 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/.

  • priority

    Resolution-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.

  • data

    Free-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_id

    Intended target node id — NOT a foreign key; resolved at promotion time.

  • loc

    Edge 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 (was label).
  • 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_id

    The User this session is attributed to (typically the end-user in a chatbot/automation session; the developer in a DEVELOPER session).

  • agent_id

    The Agent driving the session (NULL for free-form developer sessions not tied to a specific Agent). When workerId is set this records the ROLE-agent behind the casting (cor:agt:020:03).

  • worker_id

    974 — 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_id

    The active write-memory for this session, when there's an unambiguous one. Resolves the multi-writable-memory ambiguity.

  • expires_at

    Hard 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_path

    928 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_id

    Chain 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_at

    Set 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 with endedAt and to the same instant, so "active" stays the single predicate endedAt IS NULL and 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_id

    Memory 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 all SetNull, and memory-scoped analytics used to reach the memory by joining through node, so deleting a node silently erased the event from its memory's history. A SetNull FK 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 when type = 'action-run'.
  • model: LLM model used for this specific event.
  • tokens_in: Input tokens consumed.
  • tokens_out: Output tokens generated.
  • user_id

    The on-behalf-of user (billing attribution). Nullable — pure-App service runs have no user. Distinct from the audit createdBy string.

  • organization_id

    The billable organization. Set on every metered event so ledger attribution never needs a join through App.

  • provider

    LLM provider that served the call — model alone is ambiguous across providers (e.g. claude via anthropic vs bedrock).

  • key_owner_type

    AiConfigOwnerType 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_cents

    Provider 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_id

    User owner of a user-owned Agent (rooted on the bare <handle> — #697 v2, no @ sigil). The org/owner XOR is ENFORCED — chk_agent_owner_xor in post-push.sql: (organization_id IS NULL) <> (owner_user_id IS NULL) — so exactly one of organizationId/ownerUserId is non-null.

  • urn

    Globally 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_id

    Memory 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.

  • visibility

    PUBLIC | ORGANIZATION | PERSONAL (035-visibility-enum-cleanup gave agents their own enum). PERSONAL is the creator-only draft state.

  • listed_on_marketplace

    Opt-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.

  • type

    Coarse behavior hint — see AgentType enum doc. Largely legacy post-008.

  • surfaces

    Surfaces 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_loc

    Loc of the currently-published revision of the Agent's system memory. Lets the Builder ship draft revisions without affecting running Apps.

  • editor_lock_user_id

    Optimistic 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).
  • properties

    Agent working state (per-user data like family budget, preferences); automatically available to the LLM as context.

  • memory_provisioning

    Declares how this Agent expects per-App memory to be provisioned. Shape: { appMemory: 'shared' | 'user' | 'none' }. personalMemory provisioning is implicit (lazy-create on first user-attributed write). 008-agent-installation FR-008.

  • installation_policy

    Declares membership constraints for any App that installs this Agent. Shape: { maxMembers: number | 'unlimited', memberRoles: string[] }. maxMembers enforced at AppMember invite-acceptance time. 008-agent-installation FR-009.

  • persona_role

    Persona role — free string with conventions ("backend-engineer"). A dedicated column so roster/casting queries can filter without parsing the prompt.

  • persona_prompt

    The 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_at

    Online 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_reason

    Spec 021 sentinel for data-defect rows during the FR-032 migration. TODO(spec-021-cleanup): drop alongside urnNormalizedAt.

  • legacy_urn

    697 stored alias map: the pre-v2 (v1) URN this row was migrated FROM

    (see Memory.legacyUrn). Null until the online migration flips urn to 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_type

    Column 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.
  • activated

    If 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:
  • role

    Column type is the full Role enum, but the intended grant set is CONTRIBUTOR | READER (reader = read-only, contributor = read-write). Tightening reserved for a future cleanup spec.

  • activated

    If 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_id

    The 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:
  • provider

    Provider 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_encrypted

    AES-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.
  • params

    Provider-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).
  • role

    Column type is the full Role enum, but the intended grant set is ADMIN | 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_id

    fk 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_role

    Column type is the full Role enum, but the intended set is ADMIN | null (most invites grant no global role).

  • member_role

    Column type is the full Role enum, but the intended grant set is ADMIN | CONTRIBUTOR | READER.

  • new_user_id

    fk to User.id — set when the invitation is accepted and a User is created.

  • name: Recipient's name.
  • email:
  • github_username:
  • phone_number:
  • max_activations

    Maximum number of times this invitation can be accepted by a new recipient; current count is determined from UserInvitationActivation rows.

  • expires_at:
  • accepted_at

    When 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_hash

    505 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_id

    Memory 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:
  • abstract

    Paragraph summary at the time of the snapshot. Spec 031. Added in the US1 implementation when the live Node.abstract column landed — revision history would otherwise silently drop abstract edits.

  • abstract_origin_hash

    Spec 032 — snapshot of Node.abstractOriginHash at the time of revision capture. Restored verbatim by restoreNodeRevision; 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_by

    620: 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 to editedByInfo, rationales to revLabel, and backfilled from createdBy where that held a User id.

  • edited_by_info

    620: 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_label

    620: user-settable label for this revision (updateNodeRevision

    mutation; REV_LABEL_MAX_LEN cap). Also captures the #88 reason arg at snapshot time — the edit rationale that previously squatted in editedBy (legacy reasons were migrated here).

  • changes

    620: which node fields the edit that took this snapshot changed

    (e.g. ["abstract","tags"]). The snapshot itself is the PRE-edit state, so changes describes the delta between this snapshot and the state that replaced it. Empty for legacy rows (NULL reads as []).

  • approved_by

    620: reserved — User who approved this revision. No write surface

    yet; the approve flow lands later.

  • rev_no

    696: per-node MONOTONIC revision number (1, 2, 3, …) — the stable atom

    in the hrn:noderev:<root>:<mem>:<loc...>:<rev> URN (rendered r<N>). Minted race-safely from Node.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_email

    The 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:
  • provider

    Email provider backing this connection — the emailClient dispatch key (#396: one capability tool per provider; 'ms-exchange' today).

  • tool_connection_id

    The 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_encrypted

    LEGACY (#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_enabled

    Soft 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_at

    Subscription 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 (never findUnique).
  • 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).
  • scopes

    Operation 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:
  • actions

    Allowed 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_id

    The 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_id

    The 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_id

    The resolved Secret row 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:
  • slug

    Run-tool name segment (mcp__<slug>__<tool>); lowercase [a-z0-9-], unique per org. Keep it short — provider tool-name caps are ~64 chars.

  • name:
  • url

    Streamable-HTTP MCP endpoint URL. The conduit refuses private-network hosts unless explicitly allowed (SSRF guard, dev only).

  • headers_encrypted

    AES-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.
  • enabled

    Disabled 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:
  • metadata

    The 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_encrypted

    encryptToken(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_id

    Key-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:
  • slug

    Run-tool name segment (ha__<slug>__<op>); lowercase [a-z0-9-], unique per org. Keep it short — provider tool-name caps are ~64 chars.

  • name:
  • url

    Home 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_encrypted

    AES-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_allowlist

    Exposed-op allowlist over the closed HA_OPS catalog; empty = every op. Lets an admin register a READ-ONLY instance (no call_service).

  • enabled

    Disabled 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_address

    Fixed sender identity accepted by Resend, e.g. Hadron Agent <agent@example.com>. Not secret.

  • api_key_encrypted

    AES-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.
  • policy

    App-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.

  • urn

    Globally 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 @unique declaration 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; the findUnique callers in resolvers.ts were migrated to this pattern in PR #124.

  • organization_id:
  • owner_user_id

    User owner of a user-owned App (rooted on the bare <handle> — #697 v2, no @ sigil). The org/owner XOR is ENFORCED — chk_app_owner_xor in post-push.sql: (organization_id IS NULL) <> (owner_user_id IS NULL) — so exactly one of organizationId/ownerUserId is non-null.

  • create_user_permission:
  • identify_user_method:
  • session_timeout_seconds:
  • anonymous_ttl_days:
  • app_type

    Deprecated — superseded by surfaces. Scheduled for removal in a follow-up cleanup spec (per 008 R-4); kept in place to avoid spurious portal churn.

  • role

    Default member role applied when the Agent's installation_policy.memberRoles doesn't apply.

  • description:
  • system_prompt

    Deprecated — 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_mode

    Migrated 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.

  • surfaces

    Free-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 reads surfaces.includes('chat'). 008-agent-installation FR-014.

  • created_at:
  • created_by:
  • updated_at:
  • updated_by:
  • deleted_at:
  • deleted_by:
  • uninstalled_at

    Soft-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_at

    Online 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. The apps_urn_migration_pending_idx partial index goes with it.

  • urn_migration_failed_reason

    Spec 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_urn

    697 stored alias map: the pre-v2 (v1) URN this row was migrated FROM

    (see Memory.legacyUrn). Null until the online migration flips urn to 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 raw hdr_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:
  • role

    Free-form string validated at write-time against the parent Agent's installation_policy.memberRoles (per 008 R-7). Examples: 'owner', 'guide', 'cook'. installation_policy.maxMembers is 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_at

    Set 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_id

    The Agent that does the importing (the "Suite"). ON DELETE CASCADE.

  • imported_agent_id

    The dep Agent. ON DELETE RESTRICT — deleting it would invalidate every Suite that depends on it; operators must remove the import edge first.

  • position

    Sparse-int ordering (per 008 R-8). Builders set values like 100, 200, 300 to allow easy reordering without renumbering.

  • required: Hard dep vs optional. Default true.
  • agent_org_grant_org_id

    Composite fk to AgentOrgGrant(orgId, agentId) (ON DELETE RESTRICT) — the Builder grant authorizing this import. Spec FR-005 named this column agent_subscription_id; renamed per 008 R-2 since the universal-grant role for org-level grants lives on the new AgentOrgGrant sibling, not AgentSubscription.

  • 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_at

    v1 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_at

    Set 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.
  • slug

    991 — the URN atom: hrn:worker:<root>:<app-slug>:<slug>. DERIVED from

    name at 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.

  • role

    The cast-list role this filling answers ("backend-engineer"). Free string with conventions; defaults to the agent's personaRole at cast time when the caller names none.

  • prompt_override

    Per-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_id

    974 — 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_at

    Retirement 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 raw hdr_user_<…> key.
  • key_preview: Last 4 characters of the raw key for masked display.
  • label: Human-friendly name for this key.
  • created_at:
  • created_by

    Actor that minted this key. Schema-wide convention: User.id of the actor. For self-service v1 this equals userId; the column exists for future admin-issued paths (where actor != owner).

  • issued_via

    Issuance-path discriminator: 'portal' for createUserApiKey GraphQL mutation; 'oauth:<client_id>' for /oauth/token-issued tokens. Distinct from createdBy so 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.
  • reason

    Optional support/diagnostic reason supplied at start. Capped at the resolver (USER_API_KEY_LABEL_MAX_LEN precedent).

  • created_at:
  • expires_at:
  • ended_at

    Explicit-stop timestamp. NULL while live or when the session merely lapsed by TTL.

  • ended_by

    User.id of 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_id

    The public client identifier. DCR-registered: dcr_<32 hex> (16 bytes / 128 bits). Seeded fixtures use stable identifiers like 'test'. Primary key.

  • client_name:
  • redirect_uris

    Allowed 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_hash

    SHA-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).
  • resource

    RFC 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_by

    Actor that initiated the /authorize request that minted this code. Resolves to the signed-in User; equals userId in 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_hash

    login+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:

  • state

    The OAuth state CSRF 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_state

    477 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_id

    On-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:
  • data

    Run 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). eventData stays the immutable trigger payload; on key collision the envelope wins.

  • hops

    Per-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 + failure identify it.

  • spawn

    PARENT-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_initial

    832 — the budget this run was MINTED with, so a client can render

    "spent vs budget". budgetTokens above is what REMAINS. NULL on runs minted before the column existed.

  • budget_actions_initial:
  • tokens_spent

    832 — cumulative spend, incremented in the same write as the budget

    decrement. Deliberately NOT derived: * a hops sum under-reports — a hop that failed mid-execution has no element, which is exactly the run an operator is auditing; * initial - remaining under-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_spent is backfilled from the hop trail where one exists; actions_spent has 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:
  • cron

    5-field cron expression, evaluated in timezone — recurring schedules. XOR with runAt (chk_agent_schedule_cron_xor_run_at).

  • timezone:
  • run_at

    One-shot: run once at this instant (#510). XOR with cron; the scheduler claim flips enabled to false instead of advancing nextRunAt. 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_at

    Next 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:
  • tier

    Pricing 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_points

    Markup 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_type

    Provenance: 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_key

    Caller-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_id

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