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.
Hadron Data Model¶
Generated by
prisma-markdown
default¶
erDiagram
"users" {
String id PK
String handle UK "nullable"
Int github_id UK "nullable"
String github_username 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"
}
"organizations" {
String id PK
String name
String urn UK
Boolean is_visible "nullable"
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"
}
"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
String app_id FK "nullable"
String user_id FK "nullable"
String user_memory_of_agent_id FK "nullable"
String urn UK
String name
String short_description "nullable"
String description "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 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
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"
}
"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 description "nullable"
DateTime uploaded_at
String uploaded_by
DateTime deleted_at "nullable"
}
"nodes" {
String id PK
String memory_id FK
String node_type
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"
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 source_id FK
String target_id FK
String label
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 label "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 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"
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 session_id FK "nullable"
String app_id FK "nullable"
Json action_args "nullable"
String model "nullable"
Int tokens_in "nullable"
Int tokens_out "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
String urn UK
String name
String description "nullable"
String system_prompt "nullable"
String system_memory_id "nullable"
AgentVisibility visibility
AgentType type
String surfaces
String ai_provider "nullable"
String ai_model "nullable"
String ai_api_key_encrypted "nullable"
String ai_api_key_preview "nullable"
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
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"
}
"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"
}
"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
}
"node_versions" {
String id PK
String node_id FK
String loc
String name
String alias "nullable"
String description "nullable"
String abstract "nullable"
String abstract_origin_hash "nullable"
String content "nullable"
String tags
String edited_by "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 refresh_token_encrypted
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"
}
"waiting_list" {
String id PK
String email
String requested_features "nullable"
DateTime created_at
}
"apps" {
String id PK
String name
String urn
String organization_id FK
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
String ai_provider "nullable"
String ai_model "nullable"
String ai_api_key_encrypted "nullable"
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"
}
"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"
}
"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"
}
"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"
}
"users" }o--o| "apps" : externalApp
"org_members" }o--|| "organizations" : organization
"org_members" }o--|| "users" : user
"memories" }o--|| "organizations" : organization
"memories" }o--o| "agents" : userMemoryOfAgent
"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--|| "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| "memories" : memory
"usage_events" }o--o| "nodes" : node
"usage_events" }o--o| "sessions" : session
"usage_events" }o--o| "apps" : app
"agents" }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
"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_versions" }o--|| "nodes" : node
"pending_setups" |o--|| "users" : user
"pending_setups" }o--|| "apps" : app
"exchange_connections" }o--|| "organizations" : organization
"exchange_connections" }o--|| "users" : user
"apps" }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
"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
"auth_codes" }o--|| "users" : user
"auth_codes" }o--|| "oauth_clients" : client
users¶
A person who uses Hadron, authenticated via GitHub OAuth (or email in future). N:N to Organization via OrgMember. Every user has a personal organization (auto-created on signup, isVisible=false).
Invitation chain: UserInvitationActivation links each User back to their UserInvitation, whose senderUserId points to the inviter, recursively up to the root (senderUserId IS NULL = system seed from Baragaun).
Referral quota (maxReferrals): the maximum number of new
platform users this user is allowed to onboard. Unaccepted
invitations don't count; the count is on accepted activations only.
Quota is tracked transitively up the chain — if A invites B and B
invites C, both A's and B's quotas decrement when C joins. When
sending an invite, UserInvitation.maxActivations must not exceed
the minimum free quota across the inviter's referral chain. There
is a known race condition: if a referral starts onboarding and the
"create account" request would push the chain over quota, the
account is still created (don't block a real human on a counter).
Properties as follows:
id:handleUnique username, e.g. "holger"; displayed as
@holgerin UI and node frontmatterauthorfield. Defaults to githubUsername on signup.github_id: GitHub numeric account id.github_username: GitHub login.identity_provider:external_id: App-scoped external user id. Composite-unique withexternalAppId.external_app_idThe App that minted this externalId. Forms a (App, externalId) idempotency key for chatbot user provisioning.
linked_atWhen this app-scoped external identity was linked to the platform User (i.e. when the chatbot user "graduated" to a real account).
name:email: Used for invite matching and email-based auth.avatar_url:roles: Global platform roles (not org-scoped). Default[READER].max_referralsMaximum number of new platform users this user can onboard. See the model-level doc for the referral-quota semantics.
created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
organizations¶
A company, team, or group that owns memories and has members.
Must have at least one OrgMember with role OWNER (can have many).
Every user has a personal organization (auto-created on signup,
isVisible: false) — see
hadron-concept/hadron-specs/General/Personal Organizations.md (sister repo).
Properties as follows:
id:name:urnThe organization's domain name, e.g. "baragaun.com", "micromentor.org". Serves as the namespace for all resources owned by this org (URN prefix).
is_visibleWhether this org appears in public listings.
falsefor auto-created personal orgs.github_installation_idGitHub App installation id. Used to mint ephemeral tokens (1-hour expiry) for memory git-sync against repos within this installation's scope.
github_app_id:github_app_private_key_encryptedAES-256-GCM-encrypted GitHub App private key. Plaintext never stored or shown after initial entry. Encryption key: HADRON_ENCRYPTION_KEY env var.
created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
org_members¶
Associates a User with an Organization. To remove a User from an Organization, delete the OrgMember row. Must NOT delete the OrgMember if it is the only one with role OWNER (enforced at the resolver, not the schema).
Properties as follows:
id:organization_id:user_id:role:can_inviteWhether this member can invite other existing platform users to the organization. Inviting a new platform user (UserInvitation) is governed separately by User.maxReferrals.
created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
memories¶
A Hadron knowledge graph, owned by an Organization. The four-way
class field (system / app / knowledge / personal) governs
addressing and access — see the MemoryClass enum doc.
Git-backed memories — the root node.yaml in the repo is the
memory manifest, declaring the memory's urn and marketplace
metadata. On import the server reads this manifest and creates or
updates the Memory row. The org portion of the URN must match an
existing Organization on the server.
Git authentication — the server first tries the owning
Organization's githubInstallationId to mint ephemeral tokens
(1-hour expiry). If the repo is outside the GitHub App's scope
(different GitHub org, personal repo, GitLab, etc.), the server
falls back to sourceTokenEncrypted. The GitHub App's private key
(used to sign JWT requests for installation tokens) lives in the
server's environment/secrets manager — not in the database. For
the SaaS server, Baragaun manages this key.
Token encryption — sourceTokenEncrypted uses AES-256-GCM
with a symmetric key from the server's environment
(HADRON_ENCRYPTION_KEY). Plaintext never stored or shown after
initial entry. To replace an expired/revoked token, the user
pastes a new one (overwrites the old encrypted value). Key
rotation: re-encrypt all stored tokens with the new key in a
single migration.
Properties as follows:
id:organization_id:app_idThe App this memory belongs to. Required when
classisapp; optional forpersonal/private(set when app-scoped, NULL when free-standing); NULL forsystem/knowledge/group. Enforced by chk_memory_class_app_id.user_idOwning user for personal/private-class memories. Bidirectional invariant (CHECK chk_personal_visibility_user):
class IN ('personal','private')⇔userId IS NOT NULL.user_memory_of_agent_idLegacy 005 Agent-pivot for personal memories. Retained for backward-compat queries (008 R-3); not consulted by post-008 access-control, which uses
appIddirectly. A future cleanup spec may collapse it.urnGlobally unique identifier, e.g. "baragaun.com:secureid". Derived from
org.urn + ":" + slug.name: Human-friendly display name, e.g. "SecureID Service".short_description: One-line summary for marketplace listings and search results.description: Full description (markdown); shown when expanding a listing.tags: Classification tags for search and filtering.license: License identifier (e.g. "MIT", "CC-BY-4.0", "proprietary").category_0: Primary category (e.g. "Developer Tools").category_1: Secondary category (e.g. "Backend").category_2: Tertiary category (e.g. "Node.js").icon_url: Square icon/logo URL for listings.hero_url: Banner image URL for the detail page.home_url: External homepage or documentation URL.source: Git clone URL; null = DB-only mode (no git backing).source_token_encryptedAES-256-GCM-encrypted access token for repos not covered by the org's GitHub App installation (e.g. personal repos, GitLab, Bitbucket). Plaintext never stored or displayed after initial entry.
source_token_expires_atExpiry of the stored token; server warns via MemoryLogEntry before expiration.
read_branch: Branch to clone/pull from (default: repo default branch).write_branch: Branch for mutations (default: "hadron-updates").visibility035-visibility-enum-cleanup: nullable, no default. Set only for
knowledge(PUBLIC/ORGANIZATION) andgroup(GROUP); NULL for system/app/personal/private (enforced by chk_memory_visibility_class).is_encryptedWhether the memory's contents (including asset bytes) are encrypted at rest using AES-256-GCM under HADRON_ENCRYPTION_KEY.
classPer-class governance — see MemoryClass enum doc for the four classes and their addressing rules.
anonymous_expires_atWhen an anonymous (pre-account-creation) memory expires and is reaped by the janitor. NULL for non-anonymous memories.
requires_license: If true, any subscription must have a valid MemoryLicense.last_synced_at: Last successful git sync.sync_status:sync_error: Error message from the last failed sync.pending_edge_countNumber of unresolved PendingEdge records (denormalized for dashboard display).
accepts_uploadsWhether this memory accepts asset uploads. system-class memories typically opt out.
vector_index_enabledOpt-in: when true, this memory's nodes are vector-indexed. DB default is
false; the class-derived default is applied at createMemory — aknowledge-class, non-encrypted memory is created withtrue, all other classes and any encrypted memory stayfalse(FR-001). Flippable per memory.embedding_sourceWhat the vector index is built from. Memory property, NOT a query parameter (FR-002). Only meaningful when vectorIndexEnabled.
chunk_tokensPer-memory chunking dials (FR-003). NULL ⇒ platform default (512 / 64). Consulted only when embeddingSource includes chunks.
chunk_overlap:force_fixed_size: Force fixed-size chunking, bypassing structure-aware (FR-015).vector_index_encrypted_ack_atTimestamp the owner acknowledged the embedding-inversion disclosure for an ENCRYPTED memory (FR-026). NULL ⇒ not acknowledged. Gates encrypted-memory indexing via chk_memory_vector_encrypted_ack.
created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:urn_normalized_atSpec 021 FR-032 online URN-shape migration gate (transient). Null = pre-canonical row; set = canonical-form row. Dropped post-migration. TODO(spec-021-cleanup): drop once the FR-032 migration completes.
urn_migration_failed_reasonSpec 021 sentinel for data-defect rows during the FR-032 migration. TODO(spec-021-cleanup): drop alongside urnNormalizedAt.
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: <memory.urn>:assets:<asset.id> resolves to this row.
Cross-references from nodes are reserved for a future spec —
Node.attachedAssetId? (one-asset case) or a node_assets join
(many-to-many). Not in v1.
Properties as follows:
id:memory_idThe Memory this asset belongs to; NULL while the asset is staged (uploaded but not yet attached). Janitor sweeps staged assets past the 24h TTL.
filenameOriginal filename as supplied by the user. Never appears in
storageKey.mime_type: RFC 6838 type/subtype.size_bytes: Verified at complete-upload time againstHeadObject.contentLength.storage_keyObject-store key shape:
{orgUrn}/{memoryUrn-segment}/{assetId}.{ext}. Encrypted at rest when the memory is encrypted; plaintext otherwise.scan_status: Virus scan state. Downloads gated on CLEAN.description: Optional; agent- or user-provided.uploaded_at:uploaded_by: fk to User.id — who uploaded.deleted_atSoft-delete marker. Janitor sweeps R2 object + hard-deletes after 24h.
nodes¶
A single node in a Memory. loc is the colon-delimited path
within the memory (e.g. auth:tokens), memory-relative. The full
URN is computed memory.urn + ":" + loc (e.g.
baragaun.com:secureid:auth:tokens) and not stored. id is
optional in YAML; the server assigns it on import if missing.
Node types define the node's role in the memory:
- info (default) — living knowledge: specs, guides, facts.
Shown in tree, normal search priority.
- abstract — condensed knowledge: summaries, TL;DRs,
paper abstracts. Highest search priority.
May be the only representation if the source
is archived.
- reference — external source: papers, URLs, legislation,
books. Carries metadata (DOI, ISBN, URL) in
data.
- record — immutable history: chat messages, session
logs. Hidden in tree by default, low search
priority.
- system — agent configuration: conversation designs,
stages, prompt templates, partials. Hidden
in tree, excluded from search.
(The earlier asset node-type was promoted to its own Asset table
in spec 004-asset-upload.)
Column type is String (not enum) because the running schema
pre-dates the formalization; the intended-values list above is the
contract — tightening to a Prisma enum is reserved for a future
cleanup spec.
Chat-root data.scope (spec 017, 2026-05-12) — chat-root nodes
(loc chats:<id>, depth 2, nodeType record) carry a scope field
in data recording the chat's visibility tier (private | shared).
Set at chat creation and immutable thereafter (FR-002).
Denormalized from Memory.class for chat-list query performance;
MUST stay consistent with the holding memory's class per the
invariant validator in src/api/graphql/schema/chat-scope.ts.
Properties as follows:
idUUIDv7 from
node.yamlwhen supplied by the importer (stable across moves); falls back tocuid()for inserts that omitid.memory_id:node_type: See the node-types list in the model doc. Defaultinfo.name: Human-friendly display name of this node.alias: Alternative name.loc: Colon-delimited path within the memory (e.g. "auth:tokens").is_link:description:abstractParagraph-length summary of this node. Cap is 2000 characters, enforced at the API boundary (not as a DB CHECK — caps are validation, not invariants). Surfacing rules across MCP tools (find/read/list/validate), the
Source: abstract-fallbackmarker onh-read-nodecontent-scope fallback, and the RAG-substrate motivation are documented in spec 031 (FR-005 + edge cases). That is the load-bearing reference; this comment is the cold-read pointer for someone scanning the schema.content: The topic content (topic.md full text).content_hashSHA256(yaml+md)[0:8] for change detection. Set by external producers (YAML frontmatter via importer, Git sync). UNCHANGED by spec 032 — that spec adds
abstractOriginHashbelow as a separate column with its own domain (SHA256(content)[0:8]); the two are never compared against each other.abstract_origin_hashSpec 032 — fingerprint of
contentvalue at the timeabstractwas authored. SHA-256 of plaintext content truncated to 8 hex chars (same width ascontentHash, different domain). At read time, compared againstcomputeContentHash(node.content)(viasrc/lib/contentHash.ts) to detect staleness; when the two values differ ANDabstractOriginHashis non-null, the abstract may not reflect current content. Surfaced byh-read-node'sSource: abstract-stalemarker,h-validate's[stale-abstract]warning, and the upcoming RAG layer. System-managed: never settable via GraphQLNodeInput.embedding_pending_atSet when this node needs (re-)embedding; cleared on success. The single work signal the embedding worker drains (FR-006/FR-007). Operational state only — intentionally NOT versioned on NodeVersion.
embedding_failed_atSet when an embed attempt failed (record, not a work signal). Transient failures keep pending set for retry; permanent failures clear pending (FR-009).
embedding_attempts: Attempt counter for backoff / give-up. Non-null; reset to 0 on success/revoke.embedding_error: Last embed error (diagnosability; surfaced by h-validate).tokens: Number of input tokens.tags: Classification tags.properties: Typed key-value structured data; queryable via the API.dataStructured JSON on the node; used for template-variable resolution, user profiles, extracted facts, reference metadata. Returned by
h-get-data, set byh-set-data.seqSort order among sibling nodes (lower values come first); used by
h-read-next-nodeand portal drag-and-drop reordering.owner_repo: GitHub repo slug (e.g. "micromentor/mmdata").is_runnable: If true, this node can be executed as a task.llm_modelLLM model that created or last modified this node (e.g. "claude-opus-4-6", "gpt-4o").
ai_agentAI agent tool that created or last modified this node (e.g. "claude-code", "cursor", "hadron-portal").
created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
node_embeddings¶
033-rag-vector-retrieval: a stored embedding vector for a node's abstract
or one of its content chunks. Vectors are computed on plaintext at write
time (FR-027) and stored plaintext (pgvector). The embedding column is
Unsupported("vector(768)") — Prisma has no native vector type, so it is
NEVER read/written through the generated client; all access is raw SQL
($queryRaw/$executeRaw). The HNSW index + abstract partial-unique index
live in the migration + post-push.sql (not expressible in Prisma schema).
Properties as follows:
id:node_id:memory_idDenormalized for fast per-memory similarity filtering (search filters by memory before ORDER BY distance).
kind:provider: Embedding provider — "local" | "openai" | "voyage" … v1 is always "local".modelStored model id (FR-014) — e.g. "nomic-embed-text-v1.5". Forward-compat for a deliberate re-embed when the platform model changes.
dim: Vector dimension (sanity / forward-compat guard).chunk_index:char_start:char_end:chunk_text:created_at:
edges¶
Directed edge between two Nodes. The label describes the
relationship — e.g. "imports", "depends_on", "related-to", "cites",
"abstract-of". An edge whose target is not yet synced lives in
PendingEdge until the target arrives.
Properties as follows:
id:source_id:target_id:label:conditionJSONLogic gating expression evaluated by the shared edge-condition evaluator at edge-resolution time. NULL means the edge always fires. Validated against the v1 operator subset and the five variable scopes (
memory.*,chat.*,agent.*,message.data.*,now()/today()). Spec:hadron-concept/spec-kits/specs/001-edge-condition-spec/spec.md(sister repo). User docs: https://docs.hadronmemory.com/reference/edge-conditions/.priorityResolution-order hook for edges sharing a source node; lower fires first, ties broken by insertion order. Reserved space for the future agent-exceptions feature to slot handler edges at the top.
dataFree-form structured payload on the edge (e.g. transition metadata for conversation-design edges).
created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
pending_edges¶
An edge whose target Node has not been synced yet; stored without a foreign key to the target. When the target Node is synced, the PendingEdge is converted to a regular Edge.
Properties as follows:
id:source_id:target_idIntended target node id — NOT a foreign key; resolved at promotion time.
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_idThe User this session is attributed to (typically the end-user in a chatbot/automation session; the developer in a DEVELOPER session).
agent_idThe Agent driving the session (NULL for free-form developer sessions not tied to a specific Agent).
memory_idThe active write-memory for this session, when there's an unambiguous one. Resolves the multi-writable-memory ambiguity.
expires_at: Hard expiry; the janitor reaps sessions past this point.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").input_tokens: Total prompt tokens across the session.output_tokens: Total completion tokens across the session.turn_count: Number of human-AI exchanges.error_count: Failed tool calls / action runs.parent_session_id: Sub-agent vertical relation (this session was spawned by a parent).prev_session_idChain horizontal relation (linked journey — this session continues from a previous one).
summary: AI-composed summary at session end.outcome: 0.0-1.0, reported externally.outcome_ref: GUID or URL of authoritative outcome record.outcome_meta: Domain-specific outcome detail.started_at:ended_at:auto_expired_atSet if the session was auto-expired due to inactivity (vs. explicitly 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.session_id: Set null on session deletion.app_id: The App that authored this event.action_args: Key-value args whentype = 'action-run'.model: LLM model used for this specific event.tokens_in: Input tokens consumed.tokens_out: Output tokens generated.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:urnGlobally unique identifier (e.g. "micromentor.org:juno"); derived from
org.urn + ":" + slug.name: Display name of this agent.description:system_prompt: System prompt sent to the LLM at session start.system_memory_idMemory containing conversation designs (stages, prompts, partials). Universally read-only from any App context post-008 (DEVELOP-from-App is dropped per R-10); edits happen in the Agent ownership surface.
visibilityPUBLIC | ORGANIZATION | PERSONAL (035-visibility-enum-cleanup gave agents their own enum). PERSONAL is the creator-only draft state.
typeCoarse behavior hint — see AgentType enum doc. Largely legacy post-008.
surfacesSurfaces this Agent is designed for (free-form per 008 R-9; v1 known values: 'chat', 'slack', 'mcp', 'web'). Distinct from the per-App
surfaces— this is the design-time declaration.ai_providerAI provider for the Agent's default AI config (e.g. "anthropic", "openai"). An App can override at deploy time via
App.aiProvider.ai_model:ai_api_key_encrypted: AES-256-GCM-encrypted API key for the Agent's default AI config.ai_api_key_preview: Last 4 characters of the raw API key for masked display.published_revision_locLoc of the currently-published revision of the Agent's system memory. Lets the Builder ship draft revisions without affecting running Apps.
editor_lock_user_idOptimistic editor-lock owner (one Builder at a time can hold the edit lock on the system memory).
editor_lock_expires_at: When the editor lock expires (auto-released on stale).propertiesAgent working state (per-user data like family budget, preferences); automatically available to the LLM as context.
memory_provisioningDeclares how this Agent expects per-App memory to be provisioned. Shape:
{ appMemory: 'shared' | 'user' | 'none' }.personalMemoryprovisioning is implicit (lazy-create on first user-attributed write). 008-agent-installation FR-008.installation_policyDeclares membership constraints for any App that installs this Agent. Shape:
{ maxMembers: number | 'unlimited', memberRoles: string[] }.maxMembersenforced at AppMember invite-acceptance time. 008-agent-installation FR-009.created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:urn_normalized_atSpec 021 FR-032 online URN-shape migration gate (transient). Null = pre-canonical row; set = canonical-form row. Dropped post-migration. TODO(spec-021-cleanup): drop once the FR-032 migration completes.
urn_migration_failed_reasonSpec 021 sentinel for data-defect rows during the FR-032 migration. TODO(spec-021-cleanup): drop alongside urnNormalizedAt.
agent_memory_items¶
Associates a Memory with an Agent, with a per-memory access level.
The Agent controls which memories are read-only vs read-write
per-attached-memory. After 005-agent-subscription,
AgentMemoryItem.role is the sole per-knowledge-memory write cap
(the AppAgent-level cap was first relaxed in 005, then the
AppAgent join itself was dropped in 008-agent-installation).
Properties as follows:
id:agent_id:memory_id:role"read"or"read-write". Default"read". Controls whether the Agent can write to this memory.created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
memory_licenses¶
Commercial usage grant of a Memory; defines capacity and terms. Consumed via MemorySubscription.licenseId.
Properties as follows:
id:memory_id:license_typeColumn type is
String(not enum) — intended values:user | app | organization.seats: Number of consumers allowed to use this license.valid_from:valid_until:license_keys: One or more license keys.terms: Usage terms & conditions.activatedIf true, the memory will be merged into the blended graph for the consumer.
created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
memory_subscriptions¶
An Organization subscribes to another Organization's Memory. Governed by a MemoryLicense when the memory requires one.
Access evaluation order (cross-org chain — extended in
005-agent-subscription with the App↔Agent and User↔Agent gates;
008-agent-installation added AgentOrgGrant; 023-app-shape replaced the
direct App.agentId with the AppAgent join (an App can install N Agents)):
1. App ↔ Agent — the App has the Agent installed (an AppAgent
row links them) AND Agent.visibility allows the App's org.
Cross-org installs (App.organizationId != Agent.organizationId)
additionally require an active AgentOrgGrant(App.organizationId,
agentId). Same-org installs auto-pass.
2. Agent ↔ Memory (knowledge / app / system) — current
implementation (src/mcp/access-control.ts#foldAgentIntoAccessMap):
AgentMemoryItem(agent, memory) exists OR memory is the
Agent's systemMemoryId. When cross-org for knowledge:
MemorySubscription(orgB, memory) AND any required
MemoryLicense valid (not expired, seats available).
Known gap (tracked in
issue #115):
this gate does NOT auto-grant access to app-class memories
where memory.appId = App.id AND memory.class = 'app'.
Today an explicit AgentMemoryItem row is required for
app-class memories to be reachable through the MCP/GraphQL
gates. The spec called for auto-grant; the implementation
currently does not.
3. User ↔ Agent (personal-class memory only) —
AgentSubscription(user, agentId) exists AND active per
the predicate, AND Memory.appId = App.id (the per-App
isolation that User Story 2 hangs on — a session through App
A cannot read the personal memory at (B.id, user.id) for
some other App B of the same Agent).
4. Effective role — most restrictive of
AgentMemoryItem.role and MemorySubscription.role. (The
legacy AppAgent.role is dropped post-008 — system memory is
universally read-only from any App.) personal/private-class
memories enforce strict ownership additionally: even an
ADMIN/OWNER who would otherwise satisfy gate 2 cannot read a
personal/private memory they do not own (FR-016).
Worked example (the four-org Juno scenario, 008 vocabulary):
an App in OrgA deploys Juno (Agent in OrgB with
visibility=ORGANIZATION since Alice is a Micromentor org-member;
or PUBLIC for marketplace shape) which references a knowledge
Memory in OrgC. The cross-org App↔Agent gate is satisfied by an
active AgentOrgGrant(OrgA, juno). End-user Alice (personal org
personal-alice) chats through that App; her records land in a
personal Memory at (App.id, alice.id) (Memory.userId = alice,
Memory.appId = App.id, class = personal, visibility = null).
The cross-org write into Alice's
personal memory is granted by AgentSubscription(alice, juno) —
without it, Juno has zero access. If Alice installs Juno into a
second App (different Org), her records there go to a separate
(B.id, alice.id) row — fully isolated from the OrgA installation.
Each gate above must clear in order; failure denies at the right
layer with a typed error.
Properties as follows:
id:memory_id:organization_id: The subscribing organization (not the owner).license_id:roleColumn type is the full
Roleenum, but the intended grant set isCONTRIBUTOR | READER(reader = read-only, contributor = read-write). Tightening reserved for a future cleanup spec.activatedIf true, the memory will be merged into the blended graph for the subscriber.
created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
hadron_server¶
Represents a running Hadron server instance (SaaS or self-hosted). Each organization connects to one server; a server serves one or many organizations.
Properties as follows:
id:organization_idThe organization that owns/operates this server; NULL for the Baragaun SaaS server.
url: Base URL of this server (e.g. "https://server.hadrongraph.io").log_level: Minimum level for ServerLogEntry records.version: Deployed software version.last_heartbeat_at: Last health-check timestamp.created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
server_log¶
Log entry for the Hadron server, surfaced in hadron-portal for troubleshooting. Examples: memory sync failures, GitHub App errors, system-level events.
Properties as follows:
id:server_id:level:memory_id: Set if the log relates to a specific memory.message: Human-readable log message.detail: Structured context (stack trace, request/response, etc.).created_at:created_by:updated_at:updated_by:
memory_log¶
Log entry for a specific memory, surfaced in hadron-portal for troubleshooting. Examples: sync failures, license expirations, pending edge resolution, branch conflicts.
Properties as follows:
id:memory_id:level:event_type: Categorizes the log for filtering.message: Human-readable log message.detail: Structured context (sync diff, expired license details, etc.).created_at:created_by:updated_at:updated_by:
org_member_invitations¶
An invitation sent to an existing platform User to join an
Organization. The role cannot be higher than that of the sender.
(Contrast with UserInvitation, which is for inviting new platform
users.)
Properties as follows:
id:member_user_id: fk to OrgMember.id — the sender of the invitation.recipient_user_id: fk to User.id — the recipient (an existing platform user).roleColumn type is the full
Roleenum, but the intended grant set isADMIN | CONTRIBUTOR | READER(you can't invite someone as OWNER — promotion happens via a separate transfer flow).expires_at: When this invitation expires.accepted_at: When the recipient accepted the invitation.created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
user_invitations¶
Invitation sent out to one or more persons that do not yet have a
user account. If maxActivations = 1 this invitation is
single-use. If email or githubUsername is given, the new user
is validated against this — which essentially locks the identity
of the new person and sets maxActivations to 1.
Quota is tracked on User.maxReferrals, not on the invitation.
When a new user is created from this invitation, their
maxReferrals is set by the onboarding process (e.g. from the
sender's remaining quota or a system default).
Race conditions: if a referral arrives at the landing page
after activations exceed maxActivations, the page shows
"The invitation has expired. Please contact the sender." If
onboarding has started and the create-account request would push
the count above the cap, the account is still created (don't block
a real human on a counter).
Role bounds: userRole cannot be higher than the sender's;
memberRole cannot be higher than the sender's OrgMember role in
the target org.
Properties as follows:
id:sender_user_idfk to User.id — the sender; NULL for system/seed invitations from Baragaun.
organization_id: If the new user should be added to an organization on signup.slug: Random string to get a unique invite URL.user_roleColumn type is the full
Roleenum, but the intended set isADMIN | null(most invites grant no global role).member_roleColumn type is the full
Roleenum, but the intended grant set isADMIN | CONTRIBUTOR | READER.new_user_idfk to User.id — set when the invitation is accepted and a User is created.
name: Recipient's name.email:github_username:phone_number:max_activationsMaximum number of times this invitation can be accepted by a new recipient; current count is determined from UserInvitationActivation rows.
expires_at:accepted_atWhen this invitation was accepted and the new User was created. (For multi-use invites this is set on the first acceptance.)
created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
user_invitation_activations¶
One row per User created in response to a UserInvitation. Role
bounds match UserInvitation's: userRole and memberRole cannot
exceed the sender's at activation time.
Properties as follows:
id:invitation_id: The invitation that was accepted.user_id: The newly created User.created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
email_verification_tokens¶
Temporary token for email verification during signup.
Properties as follows:
id:email:token:expires_at:used_at:created_at:
node_versions¶
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:loc:name:alias:description:abstractParagraph summary at the time of the snapshot. Spec 031. Added in the US1 implementation when the live
Node.abstractcolumn landed — version history would otherwise silently drop abstract edits.abstract_origin_hashSpec 032 — snapshot of
Node.abstractOriginHashat the time of version capture. Restored verbatim byrestoreNodeVersion; legacy versions (pre-spec) have NULL here, which short-circuits the staleness check on the live row after restore (no false positives).content:tags:edited_byFree-form author label (typically a User.id, but may be an agent identifier for AI edits).
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 linked Microsoft Exchange / Outlook mailbox for an
Organization-scoped user. Used by the email-ingestion pipeline to
pull message bodies into memory nodes. refreshTokenEncrypted is
AES-256-GCM-encrypted under HADRON_ENCRYPTION_KEY.
Properties as follows:
id:organization_id:user_id:mailbox_emailThe mailbox email address; (organizationId, mailboxEmail) is unique.
display_name:refresh_token_encrypted: AES-256-GCM-encrypted MS Graph refresh token.sync_enabledSoft toggle — operator can pause sync without removing the connection.
sync_status:last_sync_at:last_error: Error message from the last failed sync.webhook_subscription_id: MS Graph change-notification subscription id.webhook_expires_atSubscription expiry — MS Graph webhooks are short-lived; the renewer cron job watches this column.
created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
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.urnGlobally unique identifier; derived from
org.urn + ":" + slug.Spec 021 (FR-027) — uniqueness is now PARTIAL. The DB-level constraint is
apps_urn_active_uniq WHERE uninstalled_at IS NULL(declared in the 021 migration). The same URN can legitimately appear on multiple rows: one active (uninstalledAt IS NULL) plus N uninstalled history rows. Prisma's field-level@uniquedeclaration was REMOVED because it would generate a non-partial global unique index that fights with the migration.Callers MUST use
findFirst({ where: { urn, uninstalledAt: null } })for active-row resolution per FR-028 resolution precedence; thefindUniquecallers inresolvers.tswere migrated to this pattern in PR #124.organization_id:create_user_permission:identify_user_method:session_timeout_seconds:anonymous_ttl_days:app_typeDeprecated — superseded by
surfaces. Scheduled for removal in a follow-up cleanup spec (per 008 R-4); kept in place to avoid spurious portal churn.roleDefault member role applied when the Agent's
installation_policy.memberRolesdoesn't apply.description:system_promptDeprecated — superseded by
Agent.systemMemoryId+Agent.systemPrompt. Scheduled for removal in a follow-up cleanup spec (per 008 R-4).agent_tools: Tools this App's agent can access; default[].ai_providerOptional AI override on the App; if NULL, sessions resolve from the Agent's default AI config.
ai_model:ai_api_key_encrypted:expires_at:training_modeMigrated from the dropped
app_agents.training_mode. Sessions through this App are flagged as training data for the Agent's owner to review. 008-agent-installation FR-022.surfacesFree-form per 008 R-9; v1 known values:
'chat','slack','mcp','web'. Surface enforcement (which features are gated on which surfaces) is handled by the portal/app code; chat-tab gating in hadron-portal#187 readssurfaces.includes('chat'). 008-agent-installation FR-014.created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:uninstalled_atSoft-uninstall timestamp per spec 021 FR-025. Null = active install; set = uninstalled (read-only, write-rejected with "app uninstalled" error including cascade to child agents/memories/nodes per FR-026). Reinstall after soft-uninstall is allowed via the partial unique index
apps_urn_active_uniq WHERE uninstalled_at IS NULL(declared in the 021 migration; supersedes Prisma's @unique on urn at the DB level).urn_normalized_atSpec 021 FR-032 online URN-shape migration gate (transient). Null = pre-canonical row (read through the legacy parser); set = canonical- form row (read through the new parser). 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_idxpartial index goes with it.urn_migration_failed_reasonSpec 021 sentinel for data-defect rows during the FR-032 migration. Non-null = the row's urn failed normalization; the migration job skips it on subsequent passes (avoids infinite retry). Transient; dropped with urnNormalizedAt. TODO(spec-021-cleanup): drop alongside urnNormalizedAt.
app_keys¶
An API key for an App, independently revocable; multiple keys per
App. The raw key carries the canonical hdr_app_ prefix and is
shown once at creation; only the SHA-256 hash is stored. (SHA-256,
not bcrypt — bcrypt's per-hash salt would prevent the
constant-time keyHash lookup the auth resolver does. The raw key
is a 256-bit random value, so dictionary attacks against the hash
are not a concern.)
Auth path: the resolver at src/middleware/auth.ts accepts
hdr_app_-prefixed bearer tokens, looks up the AppKey by SHA-256
hash, and resolves the principal to the parent App. Memory access
is then gated by src/mcp/access-control.ts, which reads
App.agentId directly (the legacy AppAgent join was dropped in
008-agent-installation) and folds the Agent's systemMemoryId +
memoryItems into the access map.
Client config field: hadron-client reads the AppKey value from
the appKey field of .hadron/config.json. The env var name
HADRON_CLIENT_KEY describes location (the client process), not
the entity.
Properties as follows:
id:app_id:key_hash: SHA-256 hash of the rawhdr_app_<…>key.key_preview: Last 4 characters of the raw key for masked display.label: Human-friendly name for this key.created_at:created_by:last_used_at:revoked_at:
app_members¶
The User↔App membership join with a per-member role. Spec: 008-agent-installation FR-004.
Per-User lifecycle is reserved for a future spec — there is no
revokedAt / expiresAt column. Removing a member is a row delete;
the user's personal-class Memory at (app_id, user.id) is
retained by default as an orphan (FR-015) and re-attaches
automatically if the user later rejoins the same App.
No deletedAt — soft-delete is meaningless for a pure join.
Properties as follows:
app_id:user_id:roleFree-form string validated at write-time against the parent Agent's
installation_policy.memberRoles(per 008 R-7). Examples:'owner','guide','cook'.installation_policy.maxMembersis enforced at invite-acceptance time (per FR-016).created_at:created_by:updated_at:updated_by:
agent_org_grants¶
The Org↔Agent license — authorizes an Org to (a) create Apps that
deploy this Agent, and (b) bundle this Agent in AgentImport edges
of Agents the Org owns. Spec: 008-agent-installation FR-006 / R-2.
Sibling to AgentSubscription (User↔Agent, from 005). Both share
the lifecycle field shape (activatedAt, expiresAt, revokedAt,
revokedBy) and the same active-status predicate. The two-table
split (rather than a polymorphic subjectType + subjectId)
preserves typed FK integrity and per-table indexing clarity.
For same-org installs (an Org deploying its own Agent), the
grant is auto-provisioned by ensureAgentOrgGrant on first
contact. Cross-org installs require an explicit grant; the v1
surface auto-provisions on first contact (the marketplace UX for
explicit subscribe lands in a future spec).
No deletedAt — soft-delete is redundant with the
revokedAt lifecycle.
Active-status predicate (shared with AgentSubscription via
the isLifecycleActive helper):
activatedAt IS NOT NULL AND revokedAt IS NULL AND (expiresAt IS NULL OR expiresAt > now())
Properties as follows:
org_id:agent_id:activated_at: v1 default:now()on creation since billing is out of scope.expires_at:revoked_atSet by an ADMIN/OWNER of the Agent's owning org via
revokeAgentOrgGrant.revoked_by: fk to User.id — audit trail for who revoked.created_at:created_by:updated_at:updated_by:
agent_imports¶
A directed dependency edge between two Agents — "Agent A imports Agent B as a dep." Spec: 008-agent-installation FR-005 / User Story 3.
Installing the parent (a row in apps with agentId = parent)
cascade-installs required deps (creates additional Apps in the
same org, one per dep). Optional deps install only if the caller
passes installOptional.
v1 enforces 1-level imports only — the dep itself must NOT be
a parent of any AgentImport. Cycles are trivially blocked at write
time (parentAgentId != importedAgentId).
Properties as follows:
parent_agent_idThe Agent that does the importing (the "Suite"). ON DELETE CASCADE.
imported_agent_idThe dep Agent. ON DELETE RESTRICT — deleting it would invalidate every Suite that depends on it; operators must remove the import edge first.
positionSparse-int ordering (per 008 R-8). Builders set values like 100, 200, 300 to allow easy reordering without renumbering.
required: Hard dep vs optional. Defaulttrue.agent_org_grant_org_idComposite fk to
AgentOrgGrant(orgId, agentId)(ON DELETE RESTRICT) — the Builder grant authorizing this import. Spec FR-005 named this columnagent_subscription_id; renamed per 008 R-2 since the universal-grant role for org-level grants lives on the newAgentOrgGrantsibling, notAgentSubscription.agent_org_grant_agent_id:created_at:created_by:
agent_subscriptions¶
The User-side license: "this user is authorized to use this agent."
Auto-provisioned on first user-attributed contact (per
controller/operations/ensureAgentSubscription.ts); revocable by
an ADMIN/OWNER of the Agent's owning org. Authorizes the cross-org
write from the Agent into the user's personal-class Memory.
Sibling to AgentOrgGrant (Org↔Agent, added in
008-agent-installation). Both share the lifecycle field shape and
the same isLifecycleActive predicate; AgentSubscription
continues to express User↔Agent grants while AgentOrgGrant covers
Org↔Agent (App-creation + import-bundling) grants.
Spec: 005-agent-subscription (2026-04-30). Composite primary key
on (userId, agentId).
No deletedAt — soft-delete is redundant with revokedAt.
Active-status predicate (used by the access-resolver):
activatedAt IS NOT NULL AND revokedAt IS NULL AND (expiresAt IS NULL OR expiresAt > now())
Revocation behavior (per FR-028 / FR-029): when revoked, the
user's personal Memory of the Agent is retained by default —
the user owns it, may want to keep their notes after a class ends.
The Agent loses write access (per FR-019); the user retains read
access via direct authenticated queries. The
Memory.userMemoryOfAgentId pivot is preserved so the memory
remains identifiable as "originally from Agent X." Single
exception: if the personal Memory has zero nodes at revocation
time, the platform may hard-delete it alongside the
AgentSubscription (avoids orphan accumulation).
Properties as follows:
user_id:agent_id:activated_atv1 default:
now()on creation since billing is out of scope; future billing introduces a deferred-activation path (activatedAt = null until payment confirms).expires_at: Time-bounded subscriptions; NULL for non-expiring.revoked_atSet by an ADMIN/OWNER of the Agent's owning org via
revokeAgentSubscription.revoked_by: fk to User.id — audit trail.created_at:created_by:updated_at:updated_by:
app_log¶
Log entry for a specific App, surfaced in hadron-portal for troubleshooting.
Properties as follows:
id:app_id:level:memory_id:session_id:message: Human-readable log message.detail: Structured context.created_at:created_by:updated_at:updated_by:
app_agents¶
N:M join between App and Agent, reintroduced by spec 023.
No role column — system memory is read-only to every App
regardless (the pre-008 OPERATE/DEVELOP distinction stays deleted
per spec 023 FR-003). No trainingMode column — training mode
is a per-App setting and lives on App.trainingMode (spec 023
FR-001; deviates from the 2026-05-12-user-installing-app summary
§3 to avoid per-Agent conflicts on an App-wide setting).
Transition note: App.agentId was dropped and consumers
retrofitted in Phase 2b (hadron-server#129,
2026-05-17). Install flows go through the installAgentIntoApp
mutation (spec 023 US1, hadron-server#130)
which writes an AppAgent row.
Cascade behavior: hard App or Agent deletion cascades to
the AppAgent row. Uninstalling an Agent from an App (deleting
the row) does NOT cascade-delete the (app_id, agent_id, *) memories
(FR-005; orphan retention preserved). Duplicate inserts for the
same (app_id, agent_id) are rejected by the composite PK.
Properties as follows:
app_id:agent_id:created_at:created_by:updated_at:updated_by:
memory_shares¶
Asymmetric cross-user grant for personal-class memory (spec 023
US3 / FR-017–FR-022). Composite PK (memoryId, granteeId) — one
row per (Memory, grantee).
Scope: applies ONLY to personal-class Memories (FR-018).
Application-layer guard in createMemoryShare.ts enforces this.
Grantor semantics (FR-019): grantorId is the principal
(Memory.userId), recorded explicitly so the row is
self-describing without a JOIN to memories at read time. The
API actor (which may be an App backend acting on the principal's
behalf) is recorded in createdBy.
No time-window (FR-021): isolation between grantees over time is modeled by creating a new personal Memory per pairing — not by time-slicing one Memory.
Properties as follows:
memory_id:grantee_id:grantor_id:role:created_at:created_by:updated_at:updated_by:
memory_members¶
Symmetric team membership for group-class memory (spec 023 US4 /
FR-026–FR-031). Composite PK (memoryId, userId) — one row per
(Memory, member).
Scope: applies ONLY to group-class Memories (FR-027).
Application-layer guard in addMemoryMember.ts enforces this.
Role capabilities:
reader — read.
writer — read + write nodes within the Memory.
owner — read + write + manage member list, delete/rename the
Memory, change Memory configuration (FR-036).
Last-owner protection (FR-038): the platform rejects removal
or demotion of the sole remaining owner. The MemoryMember row
can still be deleted; the operation-layer guard enforces the
invariant (removeMemoryMember.ts, updateMemoryMemberRole.ts).
FR-031: removing the last non-owner does NOT delete the Memory.
Properties as follows:
memory_id:user_id:role:created_at:created_by:updated_at:updated_by:
user_api_keys¶
User-owned API key — the credential primitive for user-scoped MCP
access (spec 025-oauth-for-mcp; D-2026-05-13-001, D-2026-05-17-001).
Mirrors AppKey but resolves to a User instead of an App. The raw key is
hdr_user_<…>; we store SHA-256 (not bcrypt) for the same reason
AppKey does — direct hash lookup needs a deterministic hash, and the
256-bit raw key (32 bytes / 64 hex chars) already makes dictionary
attacks irrelevant.
Tokens issued by the OAuth /token endpoint share this shape and storage
path so a single resolveUserApiKey middleware validates both
portal-minted and OAuth-issued tokens (FR-015). The path the key
arrived through is recorded in issuedVia (not createdBy); see the
column docstrings.
Audit fields omitted (matches AppKey precedent): no updatedAt /
updatedBy / deletedAt / deletedBy. UserApiKey rows are
append-once with a single terminal state transition (revoked);
revokedAt carries the lifecycle info, and there is no edit
operation. Soft-delete is meaningless for a revocable credential.
Properties as follows:
id:user_id:key_hash: SHA-256 hash of the rawhdr_user_<…>key.key_preview: Last 4 characters of the raw key for masked display.label: Human-friendly name for this key.created_at:created_byActor that minted this key. Schema-wide convention:
User.idof the actor. For self-service v1 this equalsuserId; the column exists for future admin-issued paths (where actor != owner).issued_viaIssuance-path discriminator:
'portal'for createUserApiKey GraphQL mutation;'oauth:<client_id>'for /oauth/token-issued tokens. Distinct fromcreatedByso the audit-actor convention is preserved.last_used_at:revoked_at:
oauth_clients¶
OAuth 2.1 client registry (spec 025-oauth-for-mcp; D-2026-05-17-002).
Rows are populated by POST /oauth/register (RFC 7591 DCR) or seeded
explicitly. Public clients only (no clientSecret); PKCE S256 is the
auth proof. Per Q-025-001, only the 'test' row will be seeded by
prisma/seed.ts (spec 025 task T009 — not yet in this PR);
'claude-desktop' and other real clients self-register via DCR.
PK note: clientId is the PK (no separate cuid id) because
clientId is the natural key — it's the public, immutable identifier
the client sends on every /authorize / /token request and is
referenced by AuthCode.clientId as the FK target. Precedent for a
non-cuid PK: Session.id is caller-generated. Diverges from PR 137's
reference (which carried a separate cuid id) — see
spec-kits/specs/025-oauth-for-mcp/data-model.md §OAuthClient.
Carries the full standard audit-field set per the schema-top convention because edit / delete affordances are anticipated (admin "rename a client", "rotate redirect URIs", etc.); v1 has no such surface, but the columns are cheap to add now and would otherwise need a backfill later.
Properties as follows:
client_idThe public client identifier. DCR-registered:
dcr_<32 hex>(16 bytes / 128 bits). Seeded fixtures use stable identifiers like'test'. Primary key.client_name:redirect_urisAllowed redirect URIs. /authorize must reject any redirect_uri not present here (OAuth 2.1 §3.1.2).
created_at:created_by:updated_at:updated_by:deleted_at:deleted_by:
auth_codes¶
Short-lived OAuth authorization code (spec 025-oauth-for-mcp;
RFC 6749 §4.1). Issued by POST /oauth/authorize on
decision=approve, redeemed once at POST /oauth/token. Stores PKCE
S256 challenge + RFC 8707 resource indicators for verification at
redemption. Single-use via race-safe updateMany-with-conditional
redeemedAt: null pattern (FR-013). PKCE / resource mismatch at
/token also marks redeemedAt to prevent probing.
Code storage: the raw code value is hashed (SHA-256) before
storage, matching AppKey.keyHash and UserApiKey.keyHash — auth
codes are credentials in their 10-minute redemption window and should
not be plaintext in the DB. /oauth/authorize returns the raw code
to the client; /oauth/token hashes the supplied code and looks up
by hash. Diverges from PR 137's reference (which stored plaintext)
— see spec-kits/specs/025-oauth-for-mcp/contracts/oauth-endpoints.md
§POST /oauth/token.
Audit fields omitted (matches AppKey precedent): no updatedAt /
updatedBy / deletedAt / deletedBy. AuthCode rows are append-once
with two terminal state transitions (redeemed / expired); redeemedAt
and expiresAt carry the lifecycle info. Soft-delete is meaningless
for a single-use, expiring credential.
Properties as follows:
id:code_hashSHA-256 hash of the raw code value handed to the client. Looked up by hash at /token to avoid plaintext credentials in the DB.
client_id:user_id:redirect_uri:code_challenge: PKCE S256 challenge value (base64url, no padding).resourceRFC 8707 resource indicator(s) captured at /authorize. Multi-valued per RFC 8707 §2; v1 MCP usage populates a single element (the MCP endpoint URL). Application enforces ≥1 element.
expires_at:redeemed_at:created_at:created_byActor that initiated the /authorize request that minted this code. Resolves to the signed-in User; equals
userIdin self-service v1.