Appearance
Data & Storage
Database location
All subscription data is stored in a single SQLite database file at:
~/.config/subtrack/subtrack.dbThe database is created automatically on first use. No database server or configuration is required.
Database structure
Eight tables — three for subscriptions with a many-to-many relationship, plus tables for LLM API usage tracking, free trials, price change history, subscription suggestions, and the audit log:
subscriptions
├── id INTEGER PRIMARY KEY AUTOINCREMENT
├── name TEXT NOT NULL
├── price INTEGER NOT NULL
├── currency TEXT NOT NULL
├── cycle TEXT NOT NULL
├── status TEXT NOT NULL DEFAULT 'active'
├── billing_day INTEGER
├── created_at TEXT NOT NULL DEFAULT (date('now'))
├── notes TEXT
├── payment_method TEXT
├── contract_start TEXT
├── contract_end TEXT
├── auto_renewal INTEGER NOT NULL DEFAULT 1
├── vendor_name TEXT
├── vendor_url TEXT
├── plan_tier TEXT
├── discount_amount INTEGER
└── discount_type TEXT
tags
├── id INTEGER PRIMARY KEY AUTOINCREMENT
└── name TEXT NOT NULL UNIQUE
subscription_tags
├── subscription_id INTEGER NOT NULL (FK → subscriptions.id)
└── tag_id INTEGER NOT NULL (FK → tags.id)
│ PRIMARY KEY (subscription_id, tag_id)
llm_usage
├── id INTEGER PRIMARY KEY AUTOINCREMENT
├── provider TEXT NOT NULL
├── model TEXT NOT NULL
├── input_tokens INTEGER NOT NULL DEFAULT 0
├── output_tokens INTEGER NOT NULL DEFAULT 0
├── cost REAL NOT NULL
├── date TEXT NOT NULL
├── description TEXT
└── generation_id TEXT (nullable, unique index for dedup)
trials
├── id INTEGER PRIMARY KEY AUTOINCREMENT
├── name TEXT NOT NULL
├── expires_at TEXT NOT NULL
├── price INTEGER
├── currency TEXT
├── cycle TEXT
├── notes TEXT
└── created_at TEXT NOT NULL DEFAULT (date('now'))
price_history
├── id INTEGER PRIMARY KEY AUTOINCREMENT
├── subscription_id INTEGER NOT NULL (FK → subscriptions.id)
├── old_price INTEGER
├── new_price INTEGER NOT NULL
├── old_currency TEXT
├── new_currency TEXT NOT NULL
└── changed_at TEXT NOT NULL DEFAULT (datetime('now'))
suggestions
├── id INTEGER PRIMARY KEY AUTOINCREMENT
├── name TEXT NOT NULL
├── price INTEGER
├── currency TEXT
├── cycle TEXT
├── vendor_name TEXT
├── vendor_url TEXT
├── plan_tier TEXT
├── payment_method TEXT
├── source TEXT NOT NULL DEFAULT 'email'
├── source_detail TEXT
├── email_subject TEXT
├── email_from TEXT
├── email_date TEXT
├── confidence REAL DEFAULT 0.0
├── status TEXT NOT NULL DEFAULT 'pending' (pending|dismissed|added)
├── matched_sub_id INTEGER (FK → subscriptions.id, ON DELETE SET NULL)
└── created_at TEXT NOT NULL DEFAULT (datetime('now'))
audit_log
├── id INTEGER PRIMARY KEY AUTOINCREMENT
├── action TEXT NOT NULL
├── target_type TEXT
├── target_id INTEGER
├── details TEXT
└── created_at TEXT NOT NULL DEFAULT (datetime('now'))Contract, vendor & discount fields
contract_start / contract_end store contract dates (YYYY-MM-DD), auto_renewal stores whether the subscription renews automatically, vendor_name / vendor_url record the vendor, plan_tier the plan name, and discount_amount + discount_type (percentage or fixed) any active discount. Set them with subtrack add / subtrack edit flags such as --contractEnd, --vendorName, or --planTier. subtrack list --contract --vendor shows them as extra columns, and subtrack review surfaces upcoming contract expirations.
Suggestions
The suggestions table stores candidate subscriptions detected by the scanner or imported with subtrack receipt. Each row moves through pending → added (accepted via subtrack suggest add <id>) or pending → dismissed.
Config-backed data
Some data lives in ~/.config/subtrack/config.json rather than the database: saved filter profiles (profiles, activeProfile), reusable templates (templates), budgets, and display settings. See the Configuration page.
Status
Each subscription has a status field: active, paused, cancelled, or archived. Only active and paused subscriptions are included in payment calculations and upcoming bills. Archived subscriptions are preserved for long-term record-keeping but excluded from totals. Cancelled subscriptions are also excluded.
Billing day
The billing_day column stores the day of month (1–31) when the subscription is billed. If not set, the creation date is used as the anchor for billing calculations. This is particularly relevant for the upcoming command, which predicts when your next bill is due.
Notes & Payment method
The notes column stores free-form text notes for a subscription (e.g. "family plan", "shared with friends"). The payment_method column records how the subscription is paid (e.g. credit_card, paypal, debit_card).
Deletion behavior
Deleting a subscription automatically removes its tag associations via ON DELETE CASCADE. Orphaned tags (with no subscriptions) can be cleaned up with subtrack tag prune.
Price history
Every time a subscription's price or currency is changed via subtrack edit, the previous values are recorded in the price_history table. View this history with subtrack history <id> or subtrack history --all.
Audit log
All mutating operations (add, edit, delete, clone, archive, import, bulk operations, tag changes, config changes, usage changes) are recorded in the audit_log table. View the audit trail with subtrack audit list and prune old entries with subtrack audit prune.
Prices use major currency units
Subscription prices are represented in major currency units (for example, 14.99 USD). They are validated and displayed with the appropriate currency symbol and decimal places. LLM usage costs are the exception: llm_usage.cost is stored in USD cents.
Encryption
subtrack automatically encrypts the database file on disk using AES-256-GCM. The encryption key is either:
- Auto-generated and stored at
~/.config/subtrack/.key(on first run) - Derived from
SUBSC_CLI_DB_PASSPHRASEenvironment variable via scrypt
This means your subscription data is encrypted at rest. Backups can also be encrypted with subtrack backup --encrypt.
Key integrity verification
subtrack maintains a SHA-256 integrity hash for the encryption key file at ~/.config/subtrack/.key.sha256. On every startup, the key's SHA-256 hash is compared against the stored hash:
- Hash matches — key is intact, operation proceeds normally
- No sidecar file (first run or migration) — sidecar is created automatically
- Hash mismatch — an error is shown and the operation is aborted
This detects file corruption and tampering before any encryption/decryption operation. If the integrity check fails, restore the .key file from a backup, or delete it to generate a new key (note: this will make encrypted backups unrecoverable).
⚠️ Back up your key file or remember your passphrase. Without it, the database and encrypted backups cannot be recovered. Consider backing up both
.keyand.key.sha256together.
Backup
Use the backup command to create a timestamped copy of your database:
bash
# Backup to the default directory (~/.config/subtrack/backups/)
subtrack backup
# Creates: ~/.config/subtrack/backups/subtrack_20260617_143000.db.gz
# Backup to a custom directory
subtrack backup ~/backups
# Creates: ~/backups/subtrack_20260617_143000.db.gz
# Encrypted backup
subtrack backup --encrypt
# Creates: ~/.config/subtrack/backups/subtrack_20260617_143000.db.encIf no destination is specified, backups are saved to ~/.config/subtrack/backups/ (created automatically). Backups use exclusive file creation, so they will never overwrite an existing file. Each backup has a SHA-256 hash sidecar (<backup>.sha256) for integrity verification. See the Commands reference for full details.
Restore from backup
Use the restore command to restore from a backup. The backup file must be given explicitly, and --force is required to proceed — without it the command takes no action and the current database is left unchanged:
bash
# Restore from a specific file
subtrack restore ~/backups/subtrack_20260617_143000.db.gz --forceBefore restoring, the current database is automatically backed up as a safety measure (timestamped with _before_restore.db.enc suffix, encrypted). You can also restore manually:
bash
# Stop subtrack (close all running instances)
cp ~/backups/subtrack_20260617_143000.db.gz ~/.config/subtrack/subtrack.db.gz
gunzip -k ~/.config/subtrack/subtrack.db.gzMake sure subtrack is not running when you restore, as changes are written to the database file on every command.
Custom database directory
Override the default ~/.config/subtrack directory with the SUBSC_CLI_DB_DIR environment variable:
bash
SUBSC_CLI_DB_DIR=/path/to/custom/dir subtrack listSee Configuration for more details.
