Commits: 2bcc763f (macro migration) · 322bc5f8 (wire format fix)
This codebase originally used a runtime code-generation system to produce TypeScript-
compatible Codable implementations for Swift enums with associated values. The generated
files (Enums+Codable.swift, *+Codable.swift) emitted a cross-language-friendly format
where an enum case with a payload serializes as {"case": "foo", "value": <payload>}.
In early 2026 I migrated to a @TSCodable macro that generates the same output at
compile time instead of via a codegen script. The macro migration (2bcc763f) deleted
hundreds of lines of generated boilerplate and the build step that produced them.
FlowType is an enum used in iOS block rules. Before the macro, it had a hand-written
encode(to:) that emitted the old Swift auto-synthesis format: {"browser": {}} (a
keyed container where the case name is the key and the value is an empty object). This was
intentional at the time — the iOS app's decoder expected that format.
When @TSCodable replaced the hand-written encoder, FlowType started encoding as the
string "browser" instead. That's the correct cross-language format, but it broke
backward compatibility with iOS apps in the wild that only knew how to decode
{"browser": {}}.
Rather than roll back, I froze the wire format at each compatibility boundary:
Tier 1 — Pre-1.4.x apps (BlockRule.Legacy, endpoints BlockRules_v2 +
DefaultBlockRules): uses FlowType.Legacy, a plain non-String-backed enum that
auto-synthesizes the old {"browser": {}} keyed-container format.
Tier 2 — 1.4.x–1.7.x apps (BlockRule.Frozen, endpoint ConnectedRules_v2): uses
@TSCodable FlowType but with the old object payload, i.e.
{"case": "flowTypeIs", "value": {"browser": {}}}. This is what iOS apps built before the
string change understand.
Tier 3 — Internal / future (BlockRule with string-backed FlowType): the correct
format, {"case": "flowTypeIs", "value": "browser"}, used in the dashboard, stored in the
DB post-migration-068, and to be served by future endpoints once the frozen ones are
deprecated.
The iOS ApiClient boundary is the conversion point: it receives [BlockRule.Frozen]
from ConnectedRules_v2 and maps to [BlockRule] before any internal code sees it.
Migration 068_ReencodeFlowTypeAsString re-encoded all stored block_rules rows from the
old object format to the new string format. This ran in production alongside the 322bc5f8
deploy.
BlockRule.Frozen, FlowType.Legacy, and the frozen endpoints (ConnectedRules_v2,
BlockRules_v2, DefaultBlockRules) are not going away soon — old app versions will be
in the wild for a long time. The right move is additive: the next substantial iOS release
should introduce new endpoints (ConnectedRules_v3, etc.) that traffic entirely in
BlockRule with string-backed FlowType, and deprecate the frozen ones. The iOS app
already has a dual-format FlowType decoder, so it can handle either wire format whenever
the new endpoints ship.
FlowTypeFullOutputTests in the API test suite can be deleted once the new endpoints are
stood up and verified.
Key files: BlockRule+Legacy.swift, FlowType.swift, ConnectedRules_v2.swift,
ApiClient.swift (iOS), migration 068_ReencodeFlowTypeAsString.swift.