Skip to content

Database schema

The schema lives in packages/infra/db/src/schema/. One file per entity bundle; schema/index.ts is the barrel. Migrations live in packages/infra/db/src/migrations/ and are registered in meta/_journal.json.

Every relevant concept also has its own page in Concepts. This page is the dense schema-only summary.

TablePurposeConcept
usersPer-tenant user rows.
sessionsBetter-Auth sessions.
user_integration_credentialsEncrypted exchange / brokerage API keys. AES-256-GCM with ENCRYPTION_KEY.Tier model
TablePurposeConcept
institution_typesbank, broker, crypto_exchange, crypto_wallet, investment_fund, private_equity, real_estate, other.Accounts
institutionsCatalogue of financial entities — shared across users (manual institution is per-user).
institution_blockchain_mappingsMaps blockchain-typed institutions to (chainId, chainType).
account_typeschecking, savings, investment, wallet, etc.
accountsPer-user container for holdings at one institution. metadata jsonb holds wallet addresses / chain data.
TablePurposeConcept
token_typesfiat, crypto, public-stock, private-company, other.Tokens
tokensTradeable assets. Unique key: (symbol, typeId, marketSegment) (migration 0055). providerMetadata jsonb is namespaced per provider. unpriceableUntil is the price-backfill cooldown gate.
token_pricesHistorical prices. Unique key: (tokenId, baseTokenId, timestamp, granularity). Granularity: daily, intraday, tx-exact. No USD-canonical column.Pricing
token_price_edit_historyAppend-only log of manual price edits for private-company / other tokens.Manual assets
TablePurposeConcept
holdingsAtomic position. balance decimal string. source (manual / blockchain / provider name). externalId for sync matching. isHidden / isActive flags.Holdings
holding_transactionsAppend-only ledger. Every economic event. Loose kind (buy, sell, deposit, withdraw, transfer_in, transfer_out, swap_in, swap_out, fee, reward, interest, airdrop, opening_balance, unknown). Signed quantity. priceNative in native quote currency. transferGroupId / swapGroupId for pair-linking. Dedup key: (holdingId, source, externalId).Transactions
holding_balance_observationsAppend-only point-in-time balance anchors. Sources: sync-capture, statement-close, screenshot, user-entered, manual-correction. Dedup: (holdingId, observedAt, source).Observations
holding_coveragePer-holding metadata. First/last tx and observation, txSources, hasCompleteTxHistory, openingBalanceQuantity (synthesised by reconciliation).Observations
holding_apy_configsPer-holding yield rules. annualRatePct, payoutFrequency, day-of-week/month/year. Drives the apy-payouts cron.APY & yield
holding_exclusionsUser-managed exclusion rules per holding.Holdings
TablePurposeConcept
vaultsSavings goals. targetAmount + currencyId. currentAmount denormalised sum.Vaults
vault_holdingsJunction. percentage (1–100). Unique (vaultId, holdingId).
groupsUser-defined tags. Hex color, displayOrder.Groups
holding_groupsJunction. Unique (holdingId, groupId).
account_groupsJunction. Unique (accountId, groupId).
TablePurposeConcept
portfolio_value_dailyDaily cache. PK (userId, scopeKind, scopeId, snapshotDate, baseCurrencyId). scopeKind: user, institution, account, holding. coverageQuality: full / partial / estimated / unknown. costBasis / realizedPnl / unrealizedPnl decimal strings.Rollup
TablePurpose
user_jobsUser-initiated async job state (screenshot parses, imports, deletes).
job_heartbeatsPer-job heartbeat rows; drives the job-heartbeat-probe.
admin_audit_logOperator actions on api admin endpoints.
user_walletsUser-tracked blockchain wallet addresses.
cloud_api_keys, cloud_usage_eventsTier 2/3 cloud-management (CLOUD_MANAGEMENT_ENABLED=true).

The schema uses three ON DELETE behaviours deliberately:

BehaviourUsed forRationale
CASCADEuserId references; accountId from holdings; holdingId from ledger / observations / coverage / APY configs.Deleting a user / account should remove their data.
RESTRICTtokenId from holdings; baseTokenId from token_prices.Refuse to delete a token / base currency that still has live references.
SET NULLInformational token references on transactions: priceNativeTokenId, counterTokenId, counterPriceNativeTokenId, feeTokenId.If a token is dedup-merged (migrations 0006 / 0007), null the reference rather than block the merge.

These are never updated, never deleted in normal operation:

  • holding_transactions (one documented exception: the synthesised opening_balance row, updated in place by the reconciliation flow — at most one per holding per cycle).
  • holding_balance_observations.
  • token_price_edit_history.
  • admin_audit_log.
  • cloud_usage_events.

See Why an append-only ledger.

Every table file exports its row type via Drizzle’s inference:

export type Holding = typeof holdings.$inferSelect;
export type NewHolding = typeof holdings.$inferInsert;

These are the types you reach for in services and repositories. The codebase deliberately never hand-writes a row interface.

  • packages/infra/db/src/schema/ — all schema files + the index.ts barrel.
  • packages/infra/db/src/migrations/ — generated and hand-written SQL.
  • packages/infra/db/src/migrate.ts — the runner.
  • packages/infra/db/src/connection.ts — postgres.js client setup.
  • packages/infra/db/src/BaseRepository.ts — shared repository helpers.