Skip to content

Storage & Search ​

Cortexa uses SQLite (as SQLCipher, so the whole database file is encrypted at rest) as its embedded database, with AES-256-GCM encrypted blob files on disk, FTS5 for full-text keyword search, and PRAGMA-based schema migrations. No external database server is required. See Encryption for the key design and the one-time migration of older plaintext vaults.

Data Directory Layout ​

All data lives under the platform-specific app data directory:

OSPath
Windows%APPDATA%\com.cortexa.app\
macOS~/Library/Application Support/com.cortexa.app/
Linux~/.local/share/com.cortexa.app/
com.cortexa.app/
  cortexa.db          # SQLCipher-encrypted SQLite database (items, jobs, theses, links, etc.)
  salt.bin            # 16-byte KDF salt
  dbkey.bin           # the DB key, AES-256-GCM wrapped under the vault key (back up with salt.bin)
  settings.json       # Non-secret configuration
  blobs/
    <uuid>.bin        # AES-256-GCM encrypted item content
  audios/
    <uuid>.bin        # AES-256-GCM encrypted briefing audio

Building with SQLCipher ​

src-tauri enables rusqlite's bundled-sqlcipher feature. The crypto provider differs by target:

TargetSQLCipher cryptoBuild needs
macOS (arm64, and x86_64 cross-compiled on arm64)CommonCrypto (system framework)nothing extra
Windows MSVC (x64, ARM64)vendored OpenSSL (bundled-sqlcipher-vendored-openssl), statically linkeda Windows-native perl (Strawberry Perl). Git for Windows' MSYS perl fails OpenSSL's Configure. Point OPENSSL_SRC_PERL at it if another perl comes first on PATH. NASM is optional (without it OpenSSL builds with no-asm)
Linuxvendored OpenSSLperl and make (present on stock distros and GitHub's Ubuntu runners)

The first build compiles OpenSSL (several minutes); later builds reuse it. The Hive relay (hive/) depends on plain rusqlite with bundled; it is built on its own (-p cortexa-hive) for release, so it stays plain SQLite. In a cargo test --workspace run Cargo unifies features and the relay links the SQLCipher build too, which behaves exactly like SQLite for a database that is never keyed.

SQLite Schema ​

The database is opened with rusqlite and all tables are created in Store::open(). The schema is organized into core tables, auxiliary tables, and virtual tables.

Core Tables ​

items ​

The primary table for all captured thoughts.

ColumnTypeDescription
idTEXT PRIMARY KEYUUID v4
kindTEXT NOT NULLItem type (currently always "text")
blob_pathTEXT NOT NULLAbsolute path to the encrypted blob file
previewTEXT NOT NULLFirst ~200 characters of content (plaintext, for list display)
created_atTEXT NOT NULLRFC 3339 timestamp
updated_atTEXT NOT NULLRFC 3339 timestamp
summaryTEXTLLM-generated summary (plaintext)
embeddingBLOBRaw float vector (unencrypted, see Encryption)
deletedINTEGER DEFAULT 0Soft-delete flag
pinnedINTEGER DEFAULT 0Pin flag for priority items
archived_atTEXTTimestamp when archived (NULL if active)
trashed_atTEXTTimestamp when trashed (NULL if active)
project_idTEXTAssociated project ID
confidence_scoreREALNumerical confidence (0.0–1.0), decays over time
last_validated_atTEXTWhen confidence was last affirmed (user interaction resets decay)

Indexes: items_created_at, items_updated_at, items_archived_at, items_trashed_at, items_pinned, items_project, items_confidence.

theses ​

Stores the Deep Think pipeline state and output for each item.

ColumnTypeDescription
item_idTEXT PRIMARY KEYReferences items.id
statusTEXT NOT NULLOne of: pending, expanding, critiquing, synthesizing, done, failed
expansionBLOBEncrypted expansion output
counterpointsBLOBEncrypted counterpoints output
thesisBLOBEncrypted structured JSON brief
errorTEXTError message if failed
created_atTEXT NOT NULLRFC 3339 timestamp
updated_atTEXT NOT NULLRFC 3339 timestamp

The knowledge graph edges connecting related items.

ColumnTypeDescription
source_idTEXT NOT NULLOrigin item ID
target_idTEXT NOT NULLTarget item ID
weightREAL NOT NULLSimilarity score (0.0 - 1.0)
kindTEXT NOT NULLLink type (e.g., "auto" for HNSW-generated)
created_atTEXT NOT NULLRFC 3339 timestamp

Primary key: (source_id, target_id, kind). Indexes: links_target, links_kind.

briefings ​

Audio briefing metadata and scripts.

ColumnTypeDescription
idTEXT PRIMARY KEYUUID v4
scopeTEXT NOT NULLrecent or item
source_idsTEXT NOT NULLJSON array of item IDs included
statusTEXT NOT NULLpending, scripting, voicing, done, failed
scriptBLOBEncrypted briefing script text
audio_pathTEXTPath to encrypted audio file
audio_formatTEXTe.g., "mp3"
voiceTEXTTTS voice used
errorTEXTError message if failed
created_atTEXT NOT NULLRFC 3339 timestamp
updated_atTEXT NOT NULLRFC 3339 timestamp

reflections ​

Background reflection pass metadata and output (added in migration v5).

ColumnTypeDescription
idTEXT PRIMARY KEYUUID v4
statusTEXT NOT NULLpending, running, done, failed
source_idsTEXT NOT NULLJSON array of item IDs analyzed
outputBLOBEncrypted reflection output
themesTEXTJSON array of extracted themes
item_countINTEGERNumber of items analyzed
errorTEXTError message if failed
created_atTEXT NOT NULLRFC 3339 timestamp
updated_atTEXT NOT NULLRFC 3339 timestamp

Index: reflections_created.

tool_invocations ​

Persistent log of every LLM, embedding, and tool execution (added in migration v6).

ColumnTypeDescription
idTEXT PRIMARY KEYUUID v4
tool_nameTEXT NOT NULLName of the tool/operation
categoryTEXT NOT NULLllm, embed, search, tts, job
statusTEXT NOT NULLsuccess, failure
duration_msINTEGERExecution time
token_usageINTEGERTokens consumed (LLM calls)
model_usedTEXTWhich model was used
item_idTEXTAssociated item
job_idTEXTAssociated job
errorTEXTError message on failure
created_atTEXT NOT NULLRFC 3339 timestamp

Indexes: tool_inv_name, tool_inv_created.

confidence_audit ​

Append-only ledger tracking every confidence score change with reason (added in migration v6).

ColumnTypeDescription
idINTEGER PRIMARY KEYAuto-increment
item_idTEXT NOT NULLItem whose score changed
old_scoreREALPrevious score (NULL for initial)
new_scoreREAL NOT NULLNew score
reasonTEXT NOT NULLinitial, thesis_complete, decay, contradiction
sourceTEXTAdditional context
created_atTEXT NOT NULLRFC 3339 timestamp

Index: conf_audit_item.

peer_profiles ​

Per-peer statistics from Hive interactions (added in migration v6).

ColumnTypeDescription
client_idTEXT PRIMARY KEYPeer's Hive client ID
display_nameTEXT NOT NULLPeer's display name
item_countINTEGERTotal shares received
imported_countINTEGERShares imported to local vault
thesis_countINTEGERShares with thesis data
avg_thesis_confidenceREALAverage quality of peer's theses
topic_affinityTEXTJSON map of topic → count
trust_scoreREALSynthesized credibility (0.0–1.0)

procedure_log / learned_procedures ​

Orchestration execution history and learned patterns (added in migration v6).

procedure_log records each orchestrator run with item_id, thought_type, tool_sequence (JSON array), success, duration_ms.

learned_procedures aggregates patterns: thought_type + tool_sequence → execution_count, success_count, avg_duration_ms, confidence.

Auxiliary Tables ​

tags ​

ColumnTypeDescription
item_idTEXT NOT NULLReferences items.id
tagTEXT NOT NULLLowercased tag string
created_atTEXT NOT NULLRFC 3339 timestamp

Primary key: (item_id, tag). Indexes: tags_item, tags_tag.

notes ​

Conversation threads attached to items.

ColumnTypeDescription
idTEXT PRIMARY KEYUUID v4
item_idTEXT NOT NULLReferences items.id
roleTEXT NOT NULL"user" or "assistant"
contentTEXT NOT NULLNote content
created_atTEXT NOT NULLRFC 3339 timestamp

Index: notes_item on (item_id, created_at).

attachments ​

File attachments on items.

ColumnTypeDescription
idTEXT PRIMARY KEYUUID v4
item_idTEXT NOT NULLReferences items.id
filenameTEXT NOT NULLOriginal filename
mime_typeTEXT NOT NULLMIME type string
sizeINTEGER NOT NULLFile size in bytes
blob_pathTEXT NOT NULLPath to encrypted attachment blob
created_atTEXT NOT NULLRFC 3339 timestamp

Index: attachments_item.

item_meta ​

Structured metadata for agent-captured items (thought type, project, session).

ColumnTypeDescription
item_idTEXT PRIMARY KEYReferences items.id
agentTEXT NOT NULLCapturing agent identifier
thought_typeTEXT NOT NULLe.g., "decision", "architecture", "debug"
projectTEXTProject name
file_contextTEXTFile path context
languageTEXTProgramming language if applicable
session_idTEXTGrouping session identifier
confidenceTEXT"speculation", "partially-vetted", "well-researched"
created_atTEXT NOT NULLRFC 3339 timestamp

Indexes: item_meta_agent, item_meta_thought_type, item_meta_session, item_meta_project.

api_tokens ​

Per-integration bearer tokens.

ColumnTypeDescription
tokenTEXT PRIMARY KEYRandom 32-byte hex string
labelTEXT NOT NULLHuman-readable label
created_atTEXT NOT NULLRFC 3339 timestamp
last_used_atTEXTUpdated on each successful auth
revoked_atTEXTSet when revoked (non-NULL = inactive)

hive_feed ​

Local cache of items received from the Hive relay.

ColumnTypeDescription
share_idTEXT PRIMARY KEYHive share identifier
from_client_idTEXT NOT NULLSending peer's client ID
from_display_nameTEXT NOT NULLSending peer's display name
previewTEXT NOT NULLShort preview text
contentTEXT NOT NULLFull content
summaryTEXTSummary if available
tagsTEXT DEFAULT '[]'JSON array of tag strings
shared_atTEXT NOT NULLWhen originally shared
received_atTEXT NOT NULLWhen received locally
statusTEXT DEFAULT 'feed_only'feed_only, imported, dismissed
local_item_idTEXTIf imported, the local item ID
metaTEXTJSON structured metadata (thesis, notes, etc.)

Indexes: hive_feed_status, hive_feed_received.

jobs ​

The agent job queue (shared database file with items).

ColumnTypeDescription
idTEXT PRIMARY KEYUUID v4
kindTEXT NOT NULLJob type string
payloadTEXT NOT NULLJSON-serialized Job enum
statusTEXT NOT NULLpending, running, done, dead
attemptsINTEGER DEFAULT 0Number of execution attempts
max_attemptsINTEGER DEFAULT 3Maximum retry count
next_run_atTEXT NOT NULLEarliest time this job can run
last_errorTEXTError from most recent failure
created_atTEXT NOT NULLRFC 3339 timestamp
updated_atTEXT NOT NULLRFC 3339 timestamp

Indexes: jobs_pending on (status, next_run_at), jobs_created_at.

Blob Files ​

Item content is stored as encrypted files in the blobs/ directory, one file per item. The filename is <uuid>.bin. Each file contains:

[12-byte nonce][ciphertext + 16-byte GCM tag]

When reading an item's full content, the store reads the blob file from disk, decrypts it with the in-memory key, and returns the plaintext string. The preview column in SQLite holds the first ~200 characters for display without requiring decryption.

Briefing audio files follow the same pattern in the audios/ directory.

Cortexa creates a virtual FTS5 table for keyword search:

sql
CREATE VIRTUAL TABLE items_fts USING fts5(
    id UNINDEXED,
    preview,
    summary,
    content_body,
    tokenize='porter unicode61'
);

Indexed Columns ​

ColumnSourcePurpose
iditems.idUNINDEXED -- stored for joining, not searchable
previewitems.previewFirst ~200 chars, searchable
summaryitems.summaryLLM summary text, searchable
content_bodyDecrypted blobFull item content, searchable

The content_body column is populated when an item is ingested or updated. On FTS schema migration (user_version < 2), existing items are backfilled with preview and summary only (content_body is left empty and populated on next re-index).

Tokenizer ​

The porter unicode61 tokenizer provides:

  • Unicode normalization via unicode61 -- handles accented characters, CJK, etc.
  • Porter stemming -- "running" matches "run", "argued" matches "argue"
  • Prefix search -- error* matches "error", "errors", "ErrorHandler"

BM25 Scoring ​

FTS5 queries use the built-in bm25() ranking function with custom column weights:

sql
SELECT id, bm25(items_fts, 0.0, 1.0, 2.0, 5.0) as rank
FROM items_fts
WHERE items_fts MATCH ?
ORDER BY rank

The weight arguments correspond to (id, preview, summary, content_body):

ColumnWeightRationale
id0.0Not searchable (UNINDEXED)
preview1.0Baseline weight
summary2.0Summaries are distilled -- a keyword match here is more relevant
content_body5.0Full content match is the strongest signal

Higher weights mean a match in that column contributes more to the final BM25 score. The search system then blends these keyword scores with semantic scores from the HNSW index using a configurable weight (default: 0.3 keyword, 0.7 semantic). When confidence scoring is enabled, high-confidence items receive a boost in FTS search ranking.

Schema Migrations ​

Cortexa uses PRAGMA user_version for schema versioning. Migrations run automatically on startup in Store::open():

Column Migrations ​

Before the versioned migrations, individual column additions are handled idempotently:

rust
migrate_add_column(&conn, "summary", "TEXT")?;
migrate_add_column(&conn, "embedding", "BLOB")?;
migrate_add_column_with_default(&conn, "updated_at", "TEXT", "''")?;
migrate_add_column_with_default(&conn, "deleted", "INTEGER", "0")?;
migrate_add_column_with_default(&conn, "pinned", "INTEGER", "0")?;
migrate_add_column(&conn, "archived_at", "TEXT")?;
migrate_add_column(&conn, "trashed_at", "TEXT")?;

Each migrate_add_column call checks whether the column already exists before adding it, making upgrades from any version safe.

Versioned Migrations ​

VersionMigration
0 -> 2Drop and recreate items_fts table with content_body column. Backfill FTS from existing items.
2 -> 3Decrypt embeddings from AES-GCM encrypted BLOBs to raw float bytes. Removes the encryption layer from embedding vectors.

Crash Recovery ​

On startup, the store also runs recovery operations:

  • Jobs: Any jobs left in running status (from a crash) are reset to pending.
  • Theses: Any theses stuck in expanding, critiquing, or synthesizing are marked failed with "reset after crash".

Adding New Migrations ​

To add a new migration:

  1. Read the current PRAGMA user_version.
  2. If it is less than your target version, run the migration.
  3. Set PRAGMA user_version = <new_version>.

The pattern ensures each migration runs exactly once, even across version jumps.

Store Implementation ​

The Store struct (src-tauri/src/storage.rs) owns:

  • A rusqlite::Connection for all SQL operations
  • A PathBuf for the blobs directory
  • A PathBuf for the database file (used for size reporting)
  • An EncryptionKey for encrypt/decrypt operations

Key methods:

MethodPurpose
open(data_dir, key)Create tables, run migrations, return store
ingest_text(content)Encrypt content, write blob, insert row, populate FTS
read_item_content(id)Read blob from disk, decrypt, return plaintext
stats()Count items, embedded items, DB size, blob directory size
rekey(new_key)Re-encrypt all blobs with a new key
build_local_manifest()Export entries for sync
apply_remote_item(entry, blob)Import a synced item
search_fts(query, limit)FTS5 keyword search with BM25 scoring
upsert_fts(id, preview, summary, content)Insert or update FTS entry

Database Maintenance ​

  • VACUUM: Available in Settings > Retention > "Compact database". Rebuilds the SQLite file to reclaim space after many deletes.
  • Retention: Auto-archive after N days, trash grace period before hard delete, manual "Run retention now" button.
  • Blob cleanup: When an item is hard-deleted, its blob file is removed from disk.

Built with Rust, Tauri, and a lot of captured thoughts.