Skip to content
NeuralRepo
Get Support

Data Model

NeuralRepo uses Cloudflare D1 (SQLite) as its primary database. The schema is designed for single-user data isolation, efficient full-text search, and flexible idea relations.

hashashasownshashashassourcetargetsourcetargethasparenthashashasusersauth_accountssessionsapi_keysideasbilling_eventsidea_tagsidea_linksidea_relationsduplicate_detectionsattachmentstagsoauth_authorization_codesoauth_refresh_tokens

magic_links and schema_migrations stand outside this graph: magic links are keyed by email address rather than by user (the account may not exist yet), and migrations track the schema itself.

The central user record. All data queries are scoped by user_id.

ColumnTypeDescription
idTEXTPrimary key — 32 lowercase hex characters from randomblob(16), not a dashed UUID
emailTEXTUnique email address
display_nameTEXTDisplay name
avatar_urlTEXTProfile avatar URL
planTEXTfree or pro
stripe_customer_idTEXTStripe customer reference
stripe_subscription_idTEXTStripe subscription reference
anthropic_api_key_encryptedTEXTEncrypted Anthropic API key (BYOK)
openai_api_key_encryptedTEXTEncrypted OpenAI API key (BYOK)
openrouter_api_key_encryptedTEXTEncrypted OpenRouter API key (BYOK)
settings_jsonTEXTUser settings (JSON string, default {})
trial_ends_atTEXTWhen the Pro trial lapses; the daily cron downgrades past this
trial_usedINTEGER1 once a trial has been taken, so it cannot be taken twice
created_atTEXTYYYY-MM-DD HH:MM:SS UTC
updated_atTEXTYYYY-MM-DD HH:MM:SS UTC

Links external sign-in providers to a user.

ColumnTypeDescription
idTEXTPrimary key
user_idTEXTFK to users
providerTEXTgithub, google, apple, or email
provider_account_idTEXTExternal account ID; unique together with provider
provider_emailTEXTEmail from the provider
created_atTEXTCreation timestamp

Active user sessions. Tokens are SHA-256 hashed before storage.

ColumnTypeDescription
idTEXTPrimary key
user_idTEXTFK to users
token_hashTEXTSHA-256 hash of the session token (unique)
expires_atTEXTRolling 30-day expiry, pushed forward on every authenticated request
created_atTEXTCreation timestamp

API keys for programmatic access. Keys are hashed with SHA-256.

ColumnTypeDescription
idTEXTPrimary key
user_idTEXTFK to users
key_hashTEXTSHA-256 hash of the nrp_ prefixed key (unique)
labelTEXTUser-assigned label, default default
scopesTEXTSpace-separated scope list. NULL — full access — on every key you create; scopes only appear on MCP OAuth tokens.
sourceTEXTKey source, default manual
last_used_atTEXTTimestamp of most recent use
created_atTEXTCreation timestamp

One-time email sign-in tokens, keyed by email rather than user.

ColumnTypeDescription
idTEXTPrimary key
emailTEXTRecipient address
token_hashTEXTSHA-256 hash of the emailed token (unique)
expires_atTEXT15 minutes after issue
used_atTEXTSet on redemption — the row is marked, not deleted
created_atTEXTCreation timestamp

The core content table.

ColumnTypeDescription
idINTEGERAuto-increment primary key — globally unique, and the identifier every API, CLI, and MCP call takes
user_idTEXTFK to users
numberINTEGERPer-user display number, unique with user_id. This is the #42 the UI and CLI print. Nothing resolves a number back to an idea.
titleTEXTIdea title (max 200 chars)
bodyTEXTMarkdown body (max 50,000 chars)
statusTEXTcaptured, exploring, building, shipped, shelved
sourceTEXTOrigin: web, cli, claude-mcp, siri, email, api, shortcut, ios
source_urlTEXTOptional URL reference
source_summaryTEXTSummary from source
parent_idINTEGERSelf-referencing FK; ON DELETE SET NULL
vectorize_idTEXTCloudflare Vectorize embedding ID (idea_<id>). Null until the queue has processed the idea.
is_archivedINTEGERSoft delete flag (0 = active, 1 = archived)
created_atTEXTCreation timestamp
updated_atTEXTLast update timestamp

Unique tag definitions scoped by user.

ColumnTypeDescription
idINTEGERPrimary key
user_idTEXTFK to users; unique together with name
nameTEXTTag name (lowercase, unique per user)
colorTEXTOptional hex color
vectorize_idTEXTVector ID for the tag’s own embedding (tag-<user_id>-<tag_id>)
created_atTEXTCreation timestamp

Many-to-many join between ideas and tags. The composite primary key is (idea_id, tag_id), so the same tag cannot be attached twice.

ColumnTypeDescription
idea_idINTEGERFK to ideas, cascade delete
tag_idINTEGERFK to tags, cascade delete

URLs attached to ideas.

ColumnTypeDescription
idINTEGERPrimary key
idea_idINTEGERFK to ideas
urlTEXTLink URL
titleTEXTOptional display title
link_typeTEXTurl, claude-chat, github-repo, github-issue, attachment
created_atTEXTCreation timestamp

Typed directional links between ideas — the mind map’s edges.

ColumnTypeDescription
idINTEGERPrimary key
user_idTEXTFK to users
source_idea_idINTEGERFK to ideas
target_idea_idINTEGERFK to ideas
relation_typeTEXTrelated, parent, blocks, inspires, supersedes — plus duplicate, which the constraint permits but nothing writes
scoreREALSimilarity score (null for manual relations)
noteTEXTOptional description
created_byTEXTuser or system
created_atTEXTCreation timestamp

Uniqueness is on (source_idea_id, target_idea_id, created_by). That third column is load-bearing: it lets a manual link and an auto-detected one coexist between the same pair, and it is why a second manual link between two ideas fails while the system can still add its own.

Records when the system detects similar ideas.

ColumnTypeDescription
idINTEGERPrimary key
user_idTEXTFK to users
idea_idINTEGERThe newer/suspected duplicate
duplicate_of_idINTEGERThe existing original idea
similarity_scoreREALCosine similarity (0.0 to 1.0)
statusTEXTpending, merged, dismissed
created_atTEXTCreation timestamp

A unique index on (idea_id, duplicate_of_id) means re-running detection cannot create the same detection twice.

File metadata, with the object itself in R2 under r2_key.

ColumnTypeDescription
idTEXTPrimary key
idea_idINTEGERFK to ideas
user_idTEXTFK to users
filenameTEXTOriginal filename
content_typeTEXTMIME type
size_bytesINTEGERFile size
r2_keyTEXTObject key in the R2 bucket
created_atTEXTCreation timestamp

An append-only log of Stripe webhook events: stripe_event_id (unique, which is what makes webhook delivery idempotent), event_type, amount_cents, currency, the Stripe customer, subscription, and invoice ids, and a metadata_json blob.

oauth_authorization_codes and oauth_refresh_tokens support the MCP OAuth flow. Both store SHA-256 hashes rather than the credential. Authorization codes are short-lived (10 minutes) and single-use via used_at; refresh tokens expire after 90 days and carry a revoked_at column. Both default their scopes column to ideas:read ideas:write.

One row per applied migration file (version, applied_at), so migrations are not re-run.

The ideas_fts table is an FTS5 virtual table that mirrors the title and body columns of the ideas table:

CREATE VIRTUAL TABLE ideas_fts USING fts5(
title, body, content='ideas', content_rowid='id'
);

Three triggers keep it in sync — ideas_ai after insert, ideas_ad after delete, and ideas_au after update (a delete row followed by a fresh insert).

Key indexes for query performance:

TableIndexColumns
ideasidx_ideas_useruser_id
ideasidx_ideas_user_statususer_id, status
ideasidx_ideas_user_createduser_id, created_at DESC
tagsidx_tags_useruser_id
idea_tagsidx_idea_tags_tagtag_id
idea_relationsidx_relations_sourcesource_idea_id
idea_relationsidx_relations_targettarget_idea_id
idea_linksidx_idea_links_ideaidea_id
duplicate_detectionsidx_dupes_useruser_id, status
duplicate_detectionsidx_dupes_pair (unique)idea_id, duplicate_of_id
sessionsidx_sessions_tokentoken_hash
api_keysidx_api_keys_hashkey_hash
auth_accountsidx_auth_accounts_useruser_id
attachmentsidx_attachments_ideaidea_id

Ideas use a soft delete pattern. The is_archived column is 0 for active ideas and 1 for archived ideas, and every listing query filters is_archived = 0. This preserves relation integrity: an archived idea’s rows in idea_relations and idea_tags survive rather than cascading away.