feat: migrate to V4 binary topic format#94
Conversation
ApprovabilityVerdict: Needs human review This PR implements a significant migration from string-based to binary topic format, involving a database schema migration that transforms existing data, changes to core subscription interfaces, new API fields, and a new external dependency. These are substantial runtime behavior changes with schema impact. Additionally, there is an unresolved review comment identifying a potential bug in error handling. You can customize Macroscope's approvability policy. Learn more. |
0faf45b to
8043a6b
Compare
d1cc14b to
529e2f5
Compare
d3d3967 to
2e9f455
Compare
8c776b6 to
70db0e2
Compare
70db0e2 to
c7d7e68
Compare
bf652cf to
0de1f5f
Compare
Replace V3 string-based topic identifiers with V4 binary topics across the entire notification pipeline. Adds parallel bytes fields to the protobuf API for backwards compatibility, converts the database topic column from TEXT to BYTEA via migration, and updates all internal services to work exclusively with *topic.Topic from xmtpd. - Add ParseV3Topic, TopicToString, GetMessageTypeFromTopic conversions - Add topic_bytes/topics_bytes fields to proto definitions - Migration 00003: convert TEXT topics to BYTEA with hex normalization - Migration 00004: drop legacy topic_legacy column - Update Subscription/SubscriptionInput types to use *topic.Topic - Add topic normalization and validation at API boundary - Update listener to convert string topics to binary before processing - Add dual topic fields (string + base64 bytes) to APNS/FCM delivery - Remove dead code: GetMessageType, HmacUpdates, isV3Topic Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Renumber migrations: 00003→00004 (binary_topics), 00004→00005 (drop_topic_legacy) - Adapt subscriptions service to new batch query + RunInTx pattern - Update SQLC batch queries for bytea[] topic parameters - Fix installations test to use *topic.Topic - Remove deleted schema.sql (SQLC now reads from migrations) - Regenerate SQLC queries Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tion Combine migrations 00004 and 00005 into a single 00004_binary_topics migration since the two-step split adds no value for a stop-and-restart deployment model. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Migration regex: use ([0-9a-f]{2})+ to reject odd-length hex strings
- Add nil guard to GetSubscriptions for *topic.Topic parameter
- Strengthen Unsubscribe_BytesTopics test: assert topic kind and bytes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move test/helpers.go to pkg/testutils/db.go, add TestLogger and MustParseTopic helpers, convert topics_test.go to table-driven tests, standardize t.Cleanup in api_test.go, replace logging.CreateLogger with zaptest in all Branch 1 test files. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
c7d7e68 to
3dac7c0
Compare
There was a problem hiding this comment.
🟢 Low
At line 192, return err returns the last err value from the loop, but this is wrong in two ways: (1) if a deliver call fails and logs an error, execution continues and the stale err is returned at the end, causing startMessageWorkers to log a duplicate error for a failure that was already handled; (2) if the final iteration skips delivery via shouldDeliver (hitting continue), err still holds the error from a previous iteration's failed deliver, so processEnvelope returns a stale error for a message that was otherwise processed successfully.
- return err
+ return nil🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file pkg/xmtp/listener.go around line 192:
At line 192, `return err` returns the last `err` value from the loop, but this is wrong in two ways: (1) if a `deliver` call fails and logs an error, execution continues and the stale `err` is returned at the end, causing `startMessageWorkers` to log a duplicate error for a failure that was already handled; (2) if the final iteration skips delivery via `shouldDeliver` (hitting `continue`), `err` still holds the error from a *previous* iteration's failed `deliver`, so `processEnvelope` returns a stale error for a message that was otherwise processed successfully.
Evidence trail:
pkg/xmtp/listener.go lines 180-192 (loop with deliver and return err), pkg/xmtp/listener.go lines 127-139 (startMessageWorkers logs returned error). Reviewed at REVIEWED_COMMIT.

Summary
Migrate the notification server from V3 string-based topic identifiers (e.g.,
/xmtp/mls/1/g-{hex}/proto) to V4 binary topics as defined byxmtpd/pkg/topic. This enables support for V4 clients while maintaining full backwards compatibility for existing V3 consumers.What changed
New: Topic conversion module (
pkg/topics/)ParseV3Topic()— converts V3 string topics to*topic.TopicTopicToString()— converts*topic.Topicback to V3 string representationGetMessageTypeFromTopic()— extracts message type from binary topic kindGetMessageType(),messageTypeByPrefix,HmacUpdatesProto: New binary topic fields
Subscription.topic_bytes(field 4) — binary topic, takes precedence over stringtopicSubscribeRequest.topics_bytes(field 3) — merged with stringtopicsUnsubscribeRequest.topics_bytes(field 3) — merged with stringtopicsDatabase: TEXT → BYTEA migration
topictotopic_legacy, addstopic BYTEA, converts conforming V3 topics inline usinglower()for hex normalization, deletes non-conforming rows, deduplicates collapsed case variants, recreates indexestopic_legacycolumnBYTEAparametersCore interfaces (
pkg/interfaces/)Subscription.Topicchanged fromstringto*topic.TopicSubscription.TopicStringfor backwards-compatible JSON serializationMarshalJSONemits both"topic"(string) and"topicBytesB64"(base64)Subscriptionsinterface methods now accept[]*topic.TopicAPI layer (
pkg/api/)normalizeTopics()merges string + binary topic inputs with deduplicationnormalizeSubscriptionInputs()handlesSubscribeWithMetadatawith bytes-takes-precedence logicInvalidArgumentwith descriptive error messagesListener (
pkg/xmtp/)processEnvelope()converts V3 string topics to binary before subscription lookupstrings.HasPrefixcheck before expensive parsinggetContext()now usesGetMessageTypeFromTopic()for message classificationDelivery services (
pkg/delivery/)"topic"(reconstructed V3 string) and"topicBytesB64"(base64-encoded binary)Subscription.MarshalJSON()Important design considerations
Intentional compatibility break for non-conforming topics
Non-conforming topics (e.g., non-hex identifiers like
test_installation) are deleted from the database during migration and rejected by the API going forward. This is acceptable because non-conforming topics cannot be delivered on the V4 network. Existing tests were updated to use valid hex identifiers.Hex case normalization
The migration applies
lower()inline during conversion without modifyingtopic_legacyat rest. This means two rows that differ only by hex casing (e.g.,g-ABCDvsg-abcd) collapse to the same binary topic. A deduplication step (keep newest by ID) runs before recreating the unique index.String topic fidelity in delivery payloads
The
"topic"string in delivery payloads is now reconstructed from the binary topic viaTopicToString(), rather than forwarded verbatim fromEnvelope.ContentTopic. This means the string will always be lowercase hex. Consumers should treat topic strings as case-insensitive and prefer"topicBytesB64"when available.Envelope passthrough
The raw
Envelope(containing stringContentTopic) is still passed through inSendRequest.Messagefor payload delivery and idempotency key generation. Only subscription lookups and message classification use the binary topic.TopicStringis stored, not computedSubscription.TopicStringis populated once when reading from DB viaTopicToString()and stored on the struct, rather than computed on each access. This avoids repeated hex encoding in delivery hot paths where multiple services read the same subscription.Test plan
go test -p 1 ./...)golangci-lint run— no issuesgo build ./...and./dev/build— binary compiles🤖 Generated with Claude Code
Note
Migrate subscription topics from text to binary V4 format
topic_bytes/topics_bytesbinary fields to theSubscription,SubscribeRequest, andUnsubscribeRequestproto messages, allowing clients to pass V4 binary topics alongside or instead of string topics.subscriptions.topicfromTEXTtoBYTEA, parsing V3 topic strings into kind-prefixed bytes, deleting non-convertible rows, and deduplicating on(installation_id, topic).Subscriptionsinterface and all implementations soSubscribe,Unsubscribe, andGetSubscriptionsaccept*topic.Topicobjects instead of strings; SQL queries are updated to usebytea[]parameters throughout.topics.ParseV3Topicandtopics.TopicToStringhelpers for converting between string and structured topic representations; removes the oldGetMessageType(envelope)function in favour ofGetMessageTypeFromTopic(*topic.Topic).topicfield fromSubscription.Topicrather thanenvelope.ContentTopic.Macroscope summarized c7d7e68.