Fix api_keys time columns: BIGINT epoch, not TIMESTAMPTZ
#26 in Riparion/arium — merged 2026-06-10
Problem
api_keys (migration 0007) declared created_at / last_used_at / revoked_at as TIMESTAMPTZ (Postgres) / TIMESTAMP (SQLite), but the token code treats them as i64 unix-epoch seconds everywhere — unix_now() on write, i64 columns + format_unix_date on read — the same convention every other arium *_at column already uses (expires_at, occurred_at, mfa_enabled_at, deleted_at, …).
On Postgres this broke the entire token lifecycle at runtime:
create_for_userbound a bigint into a timestamptz column →column "created_at" is of type timestamp with time zone but expression is of type bigintlist_for_usercouldn't decode timestamptz intoi64
SQLite slipped through because its loose column affinity stored the bound i64 as-is, and CI never exercises the Postgres query path (migrations apply, but no queries run), so it went unnoticed until a token mint was attempted against a real Postgres deployment.
Fix
- New migration
0009_api_keys_epochconverts the three columns toBIGINT. Postgres usesALTER … TYPE BIGINT USING EXTRACT(EPOCH FROM …)::BIGINT, preserving each existing row's instant; SQLite normalizes any legacy TEXT timestamps (written by the oldCURRENT_TIMESTAMPbump) to epoch integers. Both are no-ops on fresh DBs. authenticate_tokennow writeslast_used_at = unix_now()instead of SQLCURRENT_TIMESTAMP, so writes match thei64reads on both backends (this was the one remaining non-epoch write).unix_now()madepub(crate)soapi_keycan share it.- Regression test
used_token_lists_with_populated_last_used_at: authenticates a token (triggering thelast_used_atbump) then assertslist_for_userreturns it with a populatedlast_used_at— the read path that previously failed to decode.
Validation
cargo test -p arium --features sqlite,tokens,mfa --test api_tokens→ 7/7 pass.- Compiles clean on both
sqliteandpostgres;cargo fmt --checkand libclippyclean. - Migration 0009 applied against a live QA Postgres (real rows) inside a rolled-back transaction: columns become
bigint, existing rows convert to correct epochs.
🤖 Generated with Claude Code
Last updated 2026-06-11
Links to this note
Merged pull requests, newest first.