Data Models: Arktos Wallet
From cognokratos/arktos-wallet · docs/data-models.md · pinned revision 92650a034799
This document describes the data models used within the Arktos Wallet application, primarily focusing on how wallets and their associated accounts are structured and stored.
Storage Mechanism
Arktos stores data in a single local SQLite database encrypted with
SQLCipher (rusqlite, bundled-sqlcipher-vendored-openssl). The schema is
defined by versioned SQL migrations in migrations/
(V1 initial schema, V2 account network) and applied
automatically at startup (see
Architecture — Data Architecture).
All tables are STRICT, foreign keys are enforced, and timestamps are UTC
ISO-8601 strings generated by SQLite (e.g. 2026-10-03T21:14:02.628Z).
Tables
api_keys
Client credentials. Only the HMAC of each key is stored.
| Column | Type | Constraints | Description |
|---|---|---|---|
id | INTEGER | PRIMARY KEY | |
key_hash | TEXT | NOT NULL, UNIQUE, 64 chars | HMAC-SHA256 (hex) of the API key |
key_name | TEXT | NOT NULL, 1–255 chars | Display name |
is_revoked | INTEGER | 0 or 1, default 0 | Revoked keys cannot authenticate |
created_at | TEXT | NOT NULL, default now |
wallets
One BIP39 wallet owned by one API key.
| Column | Type | Constraints | Description |
|---|---|---|---|
id | INTEGER | PRIMARY KEY | |
key_id | INTEGER | NOT NULL, FK → api_keys.id (RESTRICT) | Owner |
name | TEXT | NOT NULL, 1–255 chars | Wallet name |
encrypted_passphrase | TEXT | NOT NULL | Recovery phrase in an AES-256-GCM envelope (the only secret column) |
created_at | TEXT | NOT NULL, default now |
UNIQUE (key_id, name): wallet names are unique per owner; different owners
may use the same name. Every wallet query is scoped by key_id.
accounts
Public data of a derived account. Private keys are not stored; they are re-derived from the wallet's recovery phrase when needed.
| Column | Type | Constraints | Description |
|---|---|---|---|
id | INTEGER | PRIMARY KEY | |
wallet_id | INTEGER | NOT NULL, FK → wallets.id (RESTRICT) | |
chain_type | TEXT | Bitcoin or Ethereum | |
network | TEXT | Bitcoin: mainnet/testnet/signet/regtest; Ethereum: evm | Address space the account was derived for |
account_index | INTEGER | 0 … 2³¹−1 (non-hardened) | BIP32 child index |
derivation_path | TEXT | NOT NULL, must equal the canonical path | BIP86 m/86'/0'/0'/0/{index} (Bitcoin mainnet), m/86'/1'/0'/0/{index} (Bitcoin test networks) or BIP44 m/44'/60'/0'/0/{index} (Ethereum) |
public_key | TEXT | NOT NULL | Compressed SEC1 public key, 0x-hex |
address | TEXT | NOT NULL; Ethereum must be lowercase | Bitcoin Taproot (bc1p…/tb1p…/bcrt1p…) or Ethereum (0x…, canonical lowercase; EIP-55 checksum applied in responses) |
created_at | TEXT | NOT NULL, default now |
UNIQUE (wallet_id, chain_type, network, account_index): each account is
derived and stored once per network; concurrent first requests return the same
row, and changing BITCOIN_NETWORK never returns an account of another network.
The Ethereum chain ID is not stored (the address is chain-independent); it is
taken from configuration when responding.
Relationships
api_keys 1 ── * wallets 1 ── * accounts
Deletes are restricted (no cascading); Arktos currently never deletes rows — API keys are revoked, not removed.
Encryption
Two independent layers protect sensitive data:
- SQLCipher (
DATABASE_KEY) encrypts the entire database file. - Field encryption (AES-256-GCM) additionally encrypts the
Passphrase(wallets.encrypted_passphrase) with the wallet-seed key derived fromMASTER_KEYvia HKDF-SHA256, so someone who can read the opened database still sees only ciphertext. No private keys are stored.
Values are stored as a versioned envelope {"v":1,"alg":"A256GCM","nonce":…,"ct":…}. API keys are stored only as HMAC-SHA256 hashes (api_keys.key_hash). See Architecture — Key Hierarchy & Secret Storage.