officer kiosk is the product name; OAS was a placeholder that mimicked expo's
application services and is gone.
the rename is total because nothing has shipped: appName, permission key, all
four table prefixes, every index and check constraint name, the exported drizzle
symbols, and the secret-store purpose. no rows exist anywhere, so there is no
migration and no compatibility window - which is exactly why doing it now cost
nothing and doing it after the first publish would have cost a manual migration
on every install.
the one remaining mention of OAS is dated history in the manifest, naming what
this was ported from.
the schema is deliberately over-specified. there are no migrations - install
generates and creates, swallowing 'already exists', so a column added in v2 is
silently absent on every machine that installed v1. email survived that only
because its table had never held a row; oas_builds holds real rows from the
first publish.
legal values are CHECK constraints rather than free text, per the house rule:
platform, signing, device kind and status. oneOf() uses sql.raw because an
interpolated value binds as a parameter and postgres refuses IN ($1, $2).
two decisions worth naming:
version is TEXT despite being numeric. it is an identifier, it appears in a
path, and 2026081801 is already at the edge of a 32-bit int. fixed width means
lexicographic sort is correct.
oas_user_keys stores the key twice - a hash to check against, and an encrypted
copy because the mobile app must READ the key back to build an install URL. the
platform's own api_keys needs no such thing because nothing re-displays them.
udid is uniquely indexed only WHERE NOT NULL: many rows sit pending without one,
but a captured identifier is one physical device and must not be claimable twice.