- Shipped
- September 8, 2026 at 12:23 AM UTC
- Author
- Kamo
- Commit
- 98cf9f7
There was no VOIP_MESSAGES stream in NATS at all. Every `voip.*` publish answered 503 No Responders Available and both media pods logged, once, at boot: [VoipRelay] Failed to subscribe to voip.> subject: [SUB-90007] No matching streams for subject. So the entire realtime layer was dead — live inbound texts, the unread badge, softphone call events, voicemail, and every mobile push that rides them — and each half failed in the one way nobody greps for. The publish failure is caught and logged at warn inside publishNatsEvent; the subscribe failure was a single ERROR line on a pod that then ran for weeks looking healthy. The cause was a configuration hole. Every service that owns subjects declares its stream in application.yml and the shared NatsConfig creates it while the JetStream bean is built — EMAIL_NOTIFICATIONS/email.>, DAEMON_SYNC/daemon.>. This service declared nothing, silently inherited the shared default CHAT_MESSAGES/chat.>, and reported "NATS stream 'CHAT_MESSAGES' already exists" at boot. The hole looked exactly like a healthy startup. VoipNatsStreamProvisioner was meant to cover this and never ran once. It was a plain @Component annotated **************** — a combination Spring only evaluates reliably in auto-configuration — so the Connection bean was not yet visible during component scanning, the condition was false, and the bean was never created. It logged nothing at all, not even its own "NATS not available, skipping" branch. Deleted, in favour of the mechanism that works. VoipNatsStreamConfigTest pins the property that actually failed: every subject this service publishes on is covered by the stream it configures. Nothing smaller would have caught this — there was no wrong code, only a stream that did not exist. Separately, the frame named the wrong party. `fromPhoneNumber` carried **************** which is the org's OWN line; the far side lives in `externalPhoneNumber`. All three consumers read it the natural way, so an inbound text raised a window titled with the member's own number, ran its contact lookup against that number and found nobody, and pushed to the phone as "a text from <your own number>". Now oriented by direction, with an explicit `externalPhoneNumber` that is always the far side — the field to key on, because unlike from/to it does not flip when the member replies. The payload also carries attachmentUrl and status, so a live MMS is not an empty bubble until a refetch. A publish that fails now logs at ERROR, and a pod with no NATS says so, rather than recording texts and telling nobody.