Changelog
A reverse-chronological record of every shipped change that touches
business-logic code or user-facing behaviour. Each entry is authored
by the developer (with the docs-update skill’s
help) and reviewed in the same PR as the code change.
See the docs-automation spec
for how this page is maintained, and the covers: / owns: convention
for how /business-logic/ pages declare which code they document.
2026-06-29 — 2FA passcode SMS uses domain-bound codes for reliable autofill
The 2FA passcode SMS now appends an Apple domain-bound code line
(@www.tunnelflight.com #<code>) as its final line, and the website serves the
matching Apple App Site Association (AASA) file. Combined with the mobile app’s
new Associated Domains entitlement, this pins iOS QuickType autofill to the
exact current passcode so it can no longer surface a stale code from an earlier
message in the thread — the root cause of members entering the wrong code.
-
Code generation, storage, expiry, and validation are unchanged — only the SMS body gains the trailing domain-bound line.
-
Uses
www.tunnelflight.com, not the apex: the apex 301-redirects to www and Apple does not follow redirects when fetching the association file. -
GET /.well-known/apple-app-site-associationnow returns{"webcredentials":{"apps":["QV957DP83B.com.app.iba"]}}asapplication/json. -
Ship order matters: the app build carrying the Associated Domains entitlement must be live before this deploys, or domain-bound codes are offered to an app that isn’t yet associated. (The 1.0.7 build is already on TestFlight/Play.)
-
Jira: TNBUGS-1732
2026-06-01 — 2FA SMS passcode uses the stored dialing code directly
The 2FA passcode SMS path previously looked up a member’s dialing code by
passing members.country (the numeric dialing code) into a query that
matches on countries.iso_code (an ISO string). That lookup never matched,
so the recipient number fell back to members.country anyway. The dead
lookup is removed and members.country is now used directly to build the
+<dialing-code><phone> recipient.
-
No change to the number sent for correctly-configured members — only a redundant per-send database query was removed.
-
Related production incident: members whose
members.countrywas0received an invalid+0…number and silently got no SMS. That bad data was corrected separately in production (see the Jira ticket); this change hardens the code path. -
Jira: TNBUGS-1729
-
PR: #664
2026-05-31 — Parent-skill auto-approval: race fix + reconciliation
Members who passed every child skill under a parent could be left without the parent skill — permanently, with no self-heal — when concurrent child approvals raced the synchronous auto-approval. Fixed on three layers:
- Serialised the live path — a per-member named lock on a single
pooled connection (the previous lock leaked across pool connections and
never actually serialised). That lock is the real serialisation
guarantee: the logbook keeps every skill re-attempt, so there’s no
unique key to lean on. The logbook-create duplicate guard was also
folded into a single
INSERT … WHERE NOT EXISTS, which narrows the race window for the routine’s other (unlocked) callers. - Serialised the UIs — admin bulk approvals run sequentially (not
Promise.all) and per-row actions are disabled while one is in flight; the member portal blocks a second submit until the first resolves. - Reconciliation safety net — a one-shot back-fill of currently-stuck
members plus an hourly cron (
APP_RECONCILE_PARENT_SKILLS) that re-derives and assigns any missed parent skills, cascading up the tree.
Ships the API’s first forward-only SQL migrations (api/sql/migrations/),
applied by the runner from #649/#650. The runner tracks the API’s
migrations in a dedicated api_schema_migrations table so it can’t
collide with the assistant’s schema_migrations in the shared IBA schema.
See member lifecycle → parent-skill auto-approval.
- Jira: TNBUGS-1728
- PR: #648
2026-05-31 — Forward-only DB migrations + deploy safety
The API now applies schema/data changes through a tracked, forward-only migration runner instead of by hand, with CI and deploy-time guardrails:
- Forward-only runner (
api/scripts/migrate.js,npm run migrate/migrate:pretend) — appliesapi/sql/migrations/*.sqlin filename order, records each inschema_migrationswith a SHA-256 checksum, and aborts if an applied file is later edited. See Database migrations. - Deploy-time safety — migrations run as a DO App Platform Pre-Deploy command before traffic cuts over; a failure keeps the current version live and fires the Slack failure alert.
- Pre-merge validation — PRs labelled
deploy-preview-with-dbdry-run the branch’s migrations against an isolated preview DB (real-schema copy from the latest S3 backup), failing the PR check before a bad migration can reach production.
Runs as a dedicated DB_MIGRATE_USER with DDL privileges, kept separate
from the app’s least-privilege runtime user; credentials live in Infisical
per environment.
- Jira: —
- PR: #650
2026-05-21 — Docs automation shipped: docs-update skill + safety nets
Three pieces of tooling that keep /business-logic/ pages in sync with
the code they document, plus a deterministic safety net so drift can’t
slip through:
docs-updateClaude Code skill (invoke via/docs-updateornpm run docs:update) — reads your branch diff, classifies touched files viacovers:/owns:frontmatter, proposes doc edits and a changelog entry into your working tree for review. Never commits, never writes outsidedocs/.covers:/owns:frontmatter convention on every/business-logic/page +npm run docs:lint-coversrunning on every CI PR. See the convention page for details.- Pre-push git hook + GitHub Actions safety nets: a deterministic path-overlap check fires both locally and on PRs. Business-logic changes without doc updates are flagged. The PR-title check posts a non-blocking sticky comment if no Jira ref is found.
Recognises Jira refs from both TUN- (main IBA backend) and TMA-
(mobile app) projects by default; adding a third project is a one-line
config change in docs/.docs-update.config.json.
- Jira: [ticket?]
- PR: [#?]