Actually create the Telnyx account setup, carry MMS, and let a carrier-only org hold threads

FeatureVOIPService
Shipped
September 6, 2026 at 10:03 PM UTC
Author
Kamo
Commit
8eaebfb

Three gaps, and the first was the feature not working at all. TELNYX PROVISIONING WAS DEFINED AND CALLED BY NOTHING TelnyxProvider.provision(), ensureMemberCredential() and **************** all existed and had no callers. So an administrator pasted an API key, saved, was told the phone server was created, and nothing whatsoever was created in their Telnyx account — after which the provider reported voiceCalls=false and sms=false with a remedy telling them to press Save, which would have done nothing for ever. TelnyxSetupService now runs on create and on every update, idempotently, the way JustCall's webhook registration already does. It writes the connection and messaging-profile ids back into configJson, which is why it is a service and not a line in the provider: a provider is built FROM that config and has no repository. Assigning a number now mints that member's SIP credential and attaches the number to the messaging profile — the most-missed manual step, and the one with no symptom, because an unattached number sends perfectly and never receives. MMS Telnyx takes media_urls, never bytes. JustCall had already solved exactly this with a Redis-backed public link; it was simply named for JustCall. It now extends a neutral OutboundMediaHandoff, so one bean serves both and no JustCall call site or test changed. Both Telnyx rails carry media, and every refusal is stated — no MMS on the carrier, no handoff configured, or a file the publisher rejects — so a member never watches a text arrive with the picture silently missing. A CARRIER-ONLY ORG CAN NOW HOLD A CONVERSATION This is the limit the previous commit documented rather than fixed. VOIP_CONVERSATIONS.INSTANCE_ID is nullable (kamo-shared-library ae95b01e), so an org with numbers and a carrier and no phone server can thread a reply instead of having it dropped. Two things that had to move with it: - The dedupe. SQL treats NULLs as DISTINCT, so the existing unique index stops covering exactly the rows this introduces. A partial unique index on (ORG_ID, MEMBER_ID, EXTERNAL_PHONE_NUMBER) WHERE INSTANCE_ID IS NULL covers it. - Two NPEs waiting for the first null. The NATS event was built with Map.of, which throws on a null VALUE — caught by the handler below and logged at warn, so the member's SMS window would simply never update. SmsController called .toString() on it outright. DDL for both is in docs/sql/ and applied by hand; InitializerService is not deployed in this cluster.

All changes

Like what you see shipping?

Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.

Start Free ForeverView Pricing