Matrix chat integration — feasibility notes
Matrix chat integration — feasibility notes
Status: exploratory, not decided. This is not an ADR — no decision has been made to build any of this. It records what’s technically possible and what it costs, as input to that decision.
Goal
Let attendees jump from a profile/session in SchellingBoard to a chat with that person, without requiring an existing Signal/Telegram/WhatsApp contact. Scope agreed so far:
- One chat server per event, not shared across events, not federated with the public Matrix network.
- Accounts auto-provisioned/synced from event participants.
- 1:1 chats and group chats (subset or all attendees).
- Rooms with elevated permissions (e.g. an orga-only room).
- A no-install web client is required; native/desktop clients are a bonus, not a replacement for it.
- Chat is reached via a link to a separate URL (e.g.
chat.sb.example.com) — it does not need to be embedded in the SchellingBoard UI itself. - Voice/video: opt-in only, useful mainly for organizers, not part of the default feature.
- Server-initiated notifications (e.g. “session moved to a different slot”) sent into Matrix, targeted at attendees who RSVP’d.
Architecture
Server: Synapse (Matrix’s reference homeserver,
AGPL-3.0, open source), one container per event, alongside the existing app container. Isolation
from the public Matrix network: remove/firewall the federation listener (port 8448) and set
federation_domain_whitelist: []. No single config flag does this; it’s a combination, and Synapse
may still be able to originate some outbound federation traffic unless the listener is actually
blocked at the network level. (synapse#6401)
Provisioning: SchellingBoard’s server calls Synapse’s admin API (/_synapse/admin/v1/register,
HMAC-signed with a shared secret held only by the app container) to create/sync a Matrix account
when a guest is created. This is a server-to-server call; the shared secret is never exposed to a
browser. (Register API)
1:1 chat: a matrix.to link (https://matrix.to/#/@alice:sb.example.com)
on the attendee’s profile opens whatever Matrix client the visitor is signed into and starts/opens a
DM. No embedding needed to satisfy “jump from a profile to a chat.”
Group / orga rooms: created via the Client-Server API; membership managed by inviting/removing the mapped Matrix IDs of the guests in the group. Elevated permissions (who can invite, redact, change room settings) are native Matrix room power levels — no extra tooling required.
Server-initiated notifications: a bot/service account (created the same way as regular accounts)
posts into either a per-attendee DM or a per-session room, using the RSVP list SchellingBoard
already has server-side to decide who’s invited/ targeted. This slots in next to the existing
sessionHeadsUp email trigger in packages/contracts/src/guest.ts — same trigger point, an
additional send instead of/alongside the email.
Hosting under sb.example.com/messaging?
Not for the homeserver API. Synapse’s client/server API paths are hardcoded to start at
/_matrix and /_synapse/client from the root of whatever (sub)domain it’s served on; the official
reverse-proxy docs explicitly warn against inserting a path prefix in proxy_pass, since it breaks
URL canonicalisation (and, when federation is on, signature verification).
(reverse_proxy.html) A
subdirectory setup is DIY, undocumented, and risks subtly breaking media URLs and .well-known
discovery. The supported shape is a dedicated (sub)domain, e.g. chat.sb.example.com.
Clean-looking Matrix IDs are still possible via
well-known delegation:
sb.example.com can serve a static /.well-known/matrix/client (and /server, if federation were
ever wanted) JSON file pointing base_url at https://chat.sb.example.com, giving IDs like
@alice:sb.example.com while the actual homeserver runs on its own subdomain. This is a static file
the Next.js app (or the reverse proxy in front of it) can serve trivially, and doesn’t conflict with
anything else on the main domain.
The web client UI (Element Web or similar) is a static SPA and can be built for a subpath
(<base href> + matching web-server root), but the simplest and best-tested option is to serve it
from the same subdomain as the homeserver (chat.sb.example.com) rather than fight subpath
rewriting for no real benefit, given embedding isn’t required.
Embedding a chat UI directly in SchellingBoard (not required, but asked about)
Possible, but a real subproject, not a dependency you add:
matrix-js-sdkis headless (protocol/sync/E2EE only) — a custom message list, composer, and room list would need to be built on top of it.matrix-react-sdkis not an embeddable component library — by its own docs it only works as part of Element Web’s own “skin” and isn’t usable standalone.
Given chat can live at its own URL, this is not worth building unless attendees specifically ask for it after using the link-out version.
Can this replace Signal/Telegram?
Mostly, with real gaps:
| Area | Status |
|---|---|
| 1:1/group text, files, reactions, E2EE | Works out of the box |
| Search | Weaker — encrypted rooms aren’t server-searchable, client-side only |
| Voice/video | Opt-in (per current scope): Element Call is now production-grade (MatrixRTC + LiveKit SFU), but needs its own LiveKit + JWT-auth-sidecar + TURN/UDP relay — a second stateful service, not just Synapse |
| Push notifications | See below — the hard part |
| Mobile client | Element/Element X work, but are heavier UX than Signal/Telegram, and users must point them at a non-default homeserver |
Push notifications collide with an existing, deliberate project decision
ADR 0006 chose Web Push over a native app specifically to avoid
any relay a self-hoster doesn’t control: no Apple Developer account, no App Store, “nothing for
a self-hoster to configure.” Matrix mobile push normally goes through a push gateway
(Sygnal/UnifiedPush) wired to FCM/APNs, which is either a third-party relay (often Element’s own
sygnal.im) or another service every self-hoster has to stand up and configure themselves. Either
way it reintroduces the exact problem ADR 0006 solved. Without it, messages still arrive next time
the client is opened — just no OS-level alert.
This is the main reason “fully replace Signal/Telegram” is a materially bigger project than “stand up a chat container”: it needs either accepting the relay dependency ADR 0006 rejected, or shipping text-only-when-open as the default and treating real push as a distinct, later increment.
Alternative: Zulip
Could Zulip (Apache-2.0, topic-based chat) fill the same role? Claims below were cross-checked against zulip.com docs/pricing and the Matrix spec; items marked (from memory) were not re-verified.
Threading
- Matrix: yes. Threads are
m.threadrelations on an event (spec since v1.4; current spec has server-side aggregation and a “list threads in room” endpoint). Element Web and Element X both render them. But threading is opt-in per message on top of a linear room timeline: a room has no required topic, so it is the “thread bolted onto linear chat” model, not Zulip’s mandatory-topic model. - Signal / Telegram: Signal has only quote-replies. Telegram has reply chains and forum-style topics in groups, but no real threads.
- Zulip: every channel message must belong to a topic; topics are cheap to create, rename, and move messages between. Direct messages have no topics (from memory).
Comparison
| Area | Matrix (Synapse + Element) | Zulip |
|---|---|---|
| Conversation structure | Linear rooms, optional threads | Channels with mandatory topics |
| 1:1 and group DMs | Yes, E2EE | Yes, not E2EE (from memory) |
| Profile → DM link | matrix.to link opens any client |
Link to the org’s web app; user must be logged in there |
| Account provisioning | Admin register API (HMAC secret) | REST API can create users; SSO via email/social, LDAP, or custom python-social-auth |
| Permissions | Room power levels | Per-channel permissions, roles, user groups (user groups: paid on cloud only) |
| Orga-only room | Private room + power levels | Private channel |
| Server-sent messages | Bot account posting to rooms/DMs | Bots, incoming webhooks, REST API; fits “session moved” notices well |
| Search | Weak in E2EE rooms | Server-side full-text (no E2EE), cloud free plan limited to latest 10k messages |
| Web client | Element Web (static SPA) | Built-in, first-class |
| Mobile apps | Element / Element X | Official iOS/Android apps |
| Voice/video | Element Call (LiveKit + TURN) | Integrations with external providers (BigBlueButton, Jitsi etc.), nothing to self-host |
| Federation | Core feature; must be disabled | None, nothing to disable |
| Multi-event | One homeserver per event | One org (realm) per event, several realms per server possible (from memory) |
| Deployment weight | Synapse + Postgres, plus client | Heavier stack (Postgres, Redis, RabbitMQ, memcached, nginx), official Docker image |
| Subpath hosting | Not supported | Not supported (from memory); needs own (sub)domain |
Push notifications: same problem, different shape
Self-hosted Zulip cannot talk to APNs/FCM directly either; it forwards E2EE-encrypted push through Zulip’s own relay. The relay is free for up to 10 users, or for eligible communities (open source, academic, nonprofit, grassroots, families) on request; otherwise paid (~$3.50/user/month, discounts for non-workplace use). There is no documented way to run your own relay. This is the same conflict with ADR 0006 as Matrix, but with a single well-defined relay and an application process rather than a third-party Sygnal. Browser notifications work without the relay (from memory).
Assessment
- Zulip fits better for topic-structured group conversation (e.g. one topic per session, with discussion that stays findable) and bot-driven announcements.
- Zulip fits worse for 1:1 “message that person” use: DMs are plain, there is no E2EE, and the
matrix.to-style “opens whatever client you have” link has no equivalent. Also a heavier self-hosting footprint and no federation-style identity. - SSO caveat: auto-login from a SchellingBoard identity needs custom authentication on self-hosted Zulip (python-social-auth patching), or API-created accounts with generated credentials. Generic OIDC/SAML is a paid cloud feature. Matrix with admin-API registration is more direct here.
- Neither avoids the retention question, the second-service ops burden, or the push relay.
If the core need is “DM an attendee,” Matrix is closer. If it is “a place where sessions get discussed without chat noise,” Zulip is closer, and Matrix threads only partly cover it.
Tradeoffs worth weighing before deciding
- Deployment model mismatch:
docker-compose.ymlis deliberately one app container + SQLite — the whole self-hosting pitch. Synapse (and, if ever added, LiveKit/TURN) makes every self-hoster run and maintain a second (or third) stateful service, not just this project’s own deployments. - Data lifecycle: chat history and media accumulate in Synapse’s own store, with no existing end-of-event policy (wipe? export? keep running?). This becomes a retention/GDPR question the moment real conversations happen in it.
- Onboarding friction: attendees already have Signal/Telegram installed; a new per-event account (and possibly a new app) is friction those don’t have. The win — auto-linked contacts, orga rooms — has to outweigh that for adoption.
- Identity is only as strong as SchellingBoard’s own: most guests are unauthenticated by default
(an unsigned cookie lets anyone “become” any unprotected guest — see
utils/acting-guest.ts). A Matrix login minted from “current guest” inherits that weakness. Fine for casual chat matching the app’s existing trust level; gating the orga room specifically behindisVerifiedAsGuest(the signed-cookie path) avoids letting that weakness reach the one room where it matters. - “Must not sync with other servers” doesn’t fully hold once mobile push is in scope: even with federation off, third-party push gateways/APNs/FCM still see device tokens and notification metadata (though not room content, if E2EE’d).
- Per-event ops burden compounds: one more container to patch and monitor, multiplied by however many events run.
Open Questions
- Is this an opt-in feature per event, or something every deployment gets by default? (Given the deployment-model mismatch above, opt-in seems necessary.)
- Is text-only (no push, no calls) an acceptable v1, with push treated as a separate later decision?
- What’s the retention/deletion policy for chat data after an event ends?
- Is a wildcard-DNS/one-subdomain-per-event setup (“chat.sb.example.com”) acceptable operationally, or does each event need its own domain?
- Is the main goal 1:1 contact (favours Matrix) or per-session discussion (favours Zulip)?
- Does the orga-room use case justify the isolation/permissions work on its own, independent of whether general attendee chat ships?