Signal types were folded into the help-tag vocabulary (helpNeededTags[0] is now a case's category — see docs/SPECIFICATION.md §4.4). The client stopped writing signalType, but 6.0.2+129 and earlier still write it and no tags, and keep doing so for as long as they stay installed. Three pieces of compatibility code exist purely for those builds. This issue is to delete them once that installed base is gone.
They are not a migration. They are a live code path, which is why they went in at all: without them a Blood-donation report from a shipped build pushes as "Rescue needed", because helpNeededTagsOf substitutes the fallback for anything untagged — the server discarding a category the client still sends and still means.
What to delete
1. functions/src/tags.ts
RETIRED_SIGNAL_TYPE_TAGS
- the legacy branch of
primarySignalTag() — it collapses to helpNeededTagsOf(data)[0]
signalHeadline() can then be inlined back to helpTagHeadline(helpNeededTagsOf(data)[0])
2. functions/src/index.ts
- the
signalType mirror written onto inbox entries in handleSignalCreated
- the
signalType?: number field on the InboxEntry interface
Shipped builds render the inbox row from signalType and fall back to the stored English body without it, so removing this early shows Bulgarian users English text.
3. firestore.rules — users/{userId}/notifications/{notificationId} create
- drop
'signalType' from the keys().hasOnly([...]) allow-list
- drop the
!('signalType' in request.resource.data) || ... is int clause
helpNeededTags can become required rather than optional-if-present
⚠️ This one is the dangerous half. That rule is the client write path for the arrival catch-up, and NearbySignalChecker swallows a denial — so tightening it while old builds are live makes inbox entries silently stop appearing, with no error anywhere. It is why the allow-list accepts both shapes today.
4. Tests that pin the shim (they should fail loudly when it goes, and be deleted with it)
firestore-tests/rules.test.js: accepts the shape the currently-shipped build writes, accepts an entry carrying neither field, accepts both fields at once, as the server mirrors them
functions/src/__tests__/recipientSelection.test.ts: the signalHeadline — the phased-release path and primarySignalTag suites
5. docs/SPECIFICATION.md §4.4 — the "live path, not a migration" table describes exactly these three; remove it and the deploy-order note that goes with it.
Also removable, separately
lib/src/widgets/my_notifications_page.dart falls back to the stored English body for inbox rows written before the merge (they carry a signalType int and no tags). Those rows expire on the 90-day inbox TTL, so this can go one quarter after the change shipped — independently of the installed base.
When
When telemetry shows no meaningful traffic from builds predating the tag release. Crashlytics/analytics by app version is the signal; the fan-out's structured log line (functions/src/index.ts, the one with tier counts) will also show untagged signals falling to zero.
Do the client-facing rules tightening (item 3) as its own deploy, after the rest, and re-read the live ruleset afterwards — a rules deploy replaces the whole ruleset, and we have already lost a block that way once (see the 2026-08-15 entry in docs/SPECIFICATION.md).
Verified working
The shim was device-verified on 6.0.2+129 against production: a signalType: 2 signal with no tags pushed as "Blood donation needed — …", not "Rescue needed". Worth re-running that check in reverse when removing it — the same signal should then read "Rescue needed", confirming there is nothing left depending on the old field.
Signal types were folded into the help-tag vocabulary (
helpNeededTags[0]is now a case's category — seedocs/SPECIFICATION.md§4.4). The client stopped writingsignalType, but 6.0.2+129 and earlier still write it and no tags, and keep doing so for as long as they stay installed. Three pieces of compatibility code exist purely for those builds. This issue is to delete them once that installed base is gone.They are not a migration. They are a live code path, which is why they went in at all: without them a Blood-donation report from a shipped build pushes as "Rescue needed", because
helpNeededTagsOfsubstitutes the fallback for anything untagged — the server discarding a category the client still sends and still means.What to delete
1.
functions/src/tags.tsRETIRED_SIGNAL_TYPE_TAGSprimarySignalTag()— it collapses tohelpNeededTagsOf(data)[0]signalHeadline()can then be inlined back tohelpTagHeadline(helpNeededTagsOf(data)[0])2.
functions/src/index.tssignalTypemirror written onto inbox entries inhandleSignalCreatedsignalType?: numberfield on theInboxEntryinterfaceShipped builds render the inbox row from
signalTypeand fall back to the stored English body without it, so removing this early shows Bulgarian users English text.3.
firestore.rules—users/{userId}/notifications/{notificationId}create'signalType'from thekeys().hasOnly([...])allow-list!('signalType' in request.resource.data) || ... is intclausehelpNeededTagscan become required rather than optional-if-presentNearbySignalCheckerswallows a denial — so tightening it while old builds are live makes inbox entries silently stop appearing, with no error anywhere. It is why the allow-list accepts both shapes today.4. Tests that pin the shim (they should fail loudly when it goes, and be deleted with it)
firestore-tests/rules.test.js:accepts the shape the currently-shipped build writes,accepts an entry carrying neither field,accepts both fields at once, as the server mirrors themfunctions/src/__tests__/recipientSelection.test.ts: thesignalHeadline — the phased-release pathandprimarySignalTagsuites5.
docs/SPECIFICATION.md§4.4 — the "live path, not a migration" table describes exactly these three; remove it and the deploy-order note that goes with it.Also removable, separately
lib/src/widgets/my_notifications_page.dartfalls back to the stored English body for inbox rows written before the merge (they carry asignalTypeint and no tags). Those rows expire on the 90-day inbox TTL, so this can go one quarter after the change shipped — independently of the installed base.When
When telemetry shows no meaningful traffic from builds predating the tag release. Crashlytics/analytics by app version is the signal; the fan-out's structured log line (
functions/src/index.ts, the one with tier counts) will also show untagged signals falling to zero.Do the client-facing rules tightening (item 3) as its own deploy, after the rest, and re-read the live ruleset afterwards — a rules deploy replaces the whole ruleset, and we have already lost a block that way once (see the 2026-08-15 entry in
docs/SPECIFICATION.md).Verified working
The shim was device-verified on 6.0.2+129 against production: a
signalType: 2signal with no tags pushed as "Blood donation needed — …", not "Rescue needed". Worth re-running that check in reverse when removing it — the same signal should then read "Rescue needed", confirming there is nothing left depending on the old field.