Skip to content

Instantly share code, notes, and snippets.

@jaredh159
Created March 12, 2026 19:05
Show Gist options
  • Select an option

  • Save jaredh159/af45d98eee7f8a33b6b5af945ec23cfc to your computer and use it in GitHub Desktop.

Select an option

Save jaredh159/af45d98eee7f8a33b6b5af945ec23cfc to your computer and use it in GitHub Desktop.
FlowType wire format migration: why the frozen tiers exist

FlowType Wire Format Migration

Commits: 2bcc763f (macro migration) · 322bc5f8 (wire format fix)

What Happened

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.

The Problem It Revealed

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": {}}.

The Three-Tier Architecture

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.

DB Migration

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.

Path Forward

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment