v0.2.0
[0.2.0] — 2026-06-13
Spec-maturity milestone: faster joins (MSC3706/MSC3902), delegated auth
(MSC3861), extended profiles (MSC4133), notifications history, dehydrated
devices (MSC3814), and a long tail of CS-API / federation correctness work.
Added
-
Dehydrated devices (MSC3814).
PUT/GET/DELETE /_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_deviceplus
POST .../{device_id}/eventslet a client park an encrypted, offline
device on the server and rehydrate it later to decrypt to-device room keys
that arrived while it was away. The dehydrated device is registered like
any other device, so/keys/query,/keys/claim, to-device routing, and
cross-serverm.device_list_updateall work for it; the events endpoint
drains its to-device queue with a read-ahead cursor. One per user — a new
upload replaces and purges the previous one;device_dataand one-time-key
counts are bounded, and a dehydrated id may not alias an existing device. -
Notification history.
GET /_matrix/client/v3/notificationsreturns a
paginated list of past notifications (withfrom/limit/only=highlight).
Rows are persisted whenever a push rule matches for a local recipient — even
when no push gateway is registered — and thereadflag is computed at query
time from the user'sm.readreceipt /m.fully_readmarker. -
Extended profile fields (MSC4133).
GET/PUT/DELETE /_matrix/client/v3/profile/{userId}/{keyName}for arbitrary profile fields
(timezone, pronouns, …) alongside the standarddisplayname/avatar_url;
the full-profile response folds them in. Owner-only writes, size-bounded. -
GET /_matrix/client/v1/auth_metadata(MSC2965). Relays the configured
IdP's OAuth authorization-server metadata, falling back to an issuer-only
document if the IdP is unreachable; 404 when delegated auth is off. -
Faster remote joins (MSC3706 + MSC3902 partial state). Joining a
large federated room no longer blocks on downloading its full member
list. vela requests a join withomit_members=true, becomes joinable
immediately with a partial-state hint, and a background filler
reconciles the rest of the room state and device lists out of band.
/syncsurfacespartial_state: trueso clients can soft-fail
membership-dependent features until the room resolves; endpoints that
need complete state (/members,/joined_members,/state,
/state_ids, inboundmake/send_joinandmake/send_knock) block or
reject while a room is still partial rather than serving incomplete
answers. The filler mirrors member transitions and the remote
device-key cache on re-verify, replays bufferedm.device_list_update
EDUs once full state lands, and re-evaluates events optimistically
accepted or soft-failed during the partial window. -
Delegated authentication via OIDC / OAuth2 (MSC3861, phase 2). vela
can defer login to an external identity provider instead of managing
passwords itself. Set[auth.oidc].introspection_endpointand vela
validates Bearer tokens by introspecting them against the IdP (RFC 7662,
client_secret_basic/client_secret_post), caches results up to two
minutes bounded by the token's own expiry, and provisions accounts on
first touch from thesub/username/device_idclaims. When
enabled, the legacy auth surface is refused:/loginadvertises no
password flow,/registerrejects non-AS callers, and
/account/password//account/deactivatereturnM_UNRECOGNIZED. -
Stateful sliding sync (MSC4186). The
/syncsuccessor persists
per-connection state in a newsliding_sync_connscolumn family.
Reconnects no longer re-deliver state the client already has; the
server emits room data only for rooms that crossed a list window,
visibility, or state version since the last poll (DELTA ops), and
explicit room subscriptions stay live across polls until the client
drops them (sticky subscriptions). -
Matrix Application Service support. Bridges (mautrix-telegram,
mautrix-discord, mautrix-signal, etc.) and bots can now register
with vela. An operator pastes the AS's registration YAML into the
admin room via!as register <yaml>; vela validates the
namespaces, hashes the tokens (cleartext is shown to the operator
once, never stored), persists to a newappservicesCF, and
starts a per-AS outbound delivery worker. Every event matching the
AS's namespaces is enqueued into the newappservice_outboxCF;
the worker drains it, posts toPUT /_matrix/app/v1/transactions/ {txnId}withAuthorization: Bearer <hs_token>, falls back to
the legacy/transactions/{txnId}URL on 404/405, and retries
with exponential backoff (2s → 5min cap, 24h dead threshold).
InboundBearer <as_token>+?user_id=masquerades the request
as a user in the AS's namespace — virtual users are provisioned
on demand. Admin commands:!as register/list/unregister/enable/ disable. Interest filter is wired into both the local send path
andfederation_receive, so federated events trigger AS delivery
too. Deferred to follow-ups:m.login.application_service
register/login types,M_EXCLUSIVEenforcement on non-AS callers,
ping protocol, query endpoints,?ts=timestamp massaging,
ephemeral passthrough (m.presence/m.typing/m.receipt under
receive_ephemeral), device management UIA bypass, third-party
protocols. -
Delayed events (MSC4140). Clients can schedule an event to be sent
after a delay (self-destructing status, scheduled posts). A
[server] max_delay_msknob caps how far out a delay may be set. -
Event relationships over federation (MSC2836).
/event_relationshipswalks and backfills threaded and related events
across servers, so clients see complete relation chains for rooms whose
history spans multiple homeservers. -
Owned state events (MSC3757). State events whose
state_keyis a
user's own MXID can be set by that user regardless of power level,
enabling per-user state without granting elevated permissions. -
Server-side invite filtering (MSC4155). An invitee's
org.matrix.msc4155.invite_permission_configaccount data drives
which invites vela accepts on their behalf, so unwanted invites are
rejected before they ever reach the client. -
Restricted-room joins over federation. vela can complete the
send_joinhappy path for restricted rooms, joining via a third
server that vouches for the join authorisation. -
Notary server-key proxy.
/_matrix/key/v2/queryanswers questions
about other servers' keys: vela fetches the target's
/key/v2/server, countersigns it, and returns the result, acting as a
key-notary for peers. -
Room summary (MSC3266).
GET /_matrix/client/v1/rooms/{roomIdOrAlias}/summary
(and theim.nheko.summaryunstable path) lets clients preview a room
before joining. Resolves aliases, gates visibility via the existing
peek rules (members/invitees always; otherwise public/knock/
world-readable/allow-list), returns the caller'smembership, and
serves unauthenticated requests for world-readable rooms only. Rooms we
don't host are summarised over federation (the hierarchy root from a
via/ known candidate server), so previewing a remote room works for
authenticated callers. -
Intentional mentions (MSC3952).
m.mentionsis now honoured for
push:.m.rule.is_user_mentionnotifies a user listed in
content.m.mentions.user_ids, and.m.rule.is_room_mentionhandles
@room— but only when the sender's power level meets the room's
notifications.roomthreshold, so low-power users can't @room-spam.
Highlight counts in/syncreflect the same rules. -
Batch device delete.
POST /_matrix/client/v3/delete_devices
with the same UIA discipline as single-device delete; ids the caller
doesn't own are skipped instead of failing the whole batch. -
Content reporting.
POST /_matrix/client/v3/rooms/{roomId}/report/{eventId}
plus the v1.13/rooms/{roomId}/reportand v1.14
/users/{userId}/reportsiblings. v1.18 semantics: optional
reason, noscore. Always returns 200{}(privacy mode —
doesn't leak whether the target exists or the reporter is in the
room). Reports persist into a newevent_reportsCF. -
?server=on/publicRooms. GET/POST/_matrix/client/v3/publicRooms
now accept theserverquery param. When set to a remote homeserver
name, vela forwards the request via
POST /_matrix/federation/v1/publicRoomsand returns the peer's
response. Clients can browse other homeservers' directories without
having to talk to them directly. -
/.well-known/matrix/support. Serves admin/security contacts and
a support page from a new[support]config section (MSC1929);
returns 404 when unconfigured. -
m.get_login_tokencapability advertised as disabled so clients
hide the cross-device-login affordance we don't implement. -
vela-admin rooms-topanddiagnose.rooms-top --limit Nlists
rooms by most-recent activity with a relative-age column (clock-skewed
future bumps render asfuture);diagnoseis a one-screen operator
health probe covering current stream position, rooms still mid
partial-state resync, destinations with pending outbound queues, and
24h / 7d room activity. Both read existing schema, no new state. -
Admin bot commands
!reports [N](show the last N abuse reports,
default 20),!reactivate <mxid>(undo!deactivate's flag), and
!reset-password <mxid> [password](atomic: fresh argon2 hash, clear
the deactivated flag, revoke every access token; generates a random
password when none is given). -
Complement in CI. The Matrix spec-compliance suite now runs on
every PR via.github/workflows/complement.yml. Image build is
cached through buildkit's GHA backend; the existing
tools/testing/complement/{run.sh,skiplist.txt}runner is reused
unmodified so local and CI behaviour stay aligned. -
[presence]config block:idle_after/offline_after/
sweep_interval. Seetools/deploy/vela.toml.example.
Changed
- Federation throughput. Inbound
/sendtransactions process
concurrently — one task per room, with each room's PDUs still
serialised under a per-room lock spanning the full state-at-event,
auth-chain, and persist sequence. The outbound sender is concurrent per
(destination, room)instead of one serial task per destination, so a
busy room no longer stalls delivery to others. /members?at=historical snapshots.GET /rooms/{roomId}/members
honors theat=sync-token parameter, returning membership as it stood
at that point./sync'sprev_batchnow points past the events the
client just received soat=resolves to useful state, deleting a
canonical alias rewritesm.room.canonical_aliasto drop the dead
reference, andsend_joinresponses carry the fullauth_chain.- Race-free NID allocation. Replaced the global event-id counter with
a HiLo allocator handing out per-namespace counter blocks, eliminating
a check-then-write race where two concurrent writers for the same fresh
identifier could each consume a slot and leave one writer's state
unreachable (seen as spurious 403 "not a member" on federated leaves).
The/syncdevice-list watermark was aligned to the same boundary to
stop phantom duplicatedevice_lists.changedentries. - Threads and relations are served from indices maintained on write
(relation_counts,thread_index,thread_participants) instead of
prefix scans, so/threadsand relation-count lookups are point reads. - Dependency bumps. rocksdb to 0.24 and object_store to 0.13; the
OpenTelemetry stack to 0.32 with tracing-opentelemetry 0.33. vela-apireorganised into per-domain folders (auth,room,
sync,federation, …) instead of one flat module tree.
Security
/messagescould leak pre-join history.GET /rooms/{r}/messages
applied only a coarse departed-member cap, with no per-event
history-visibility check (the gate/eventand/contextalready use). In
a room withhistory_visibility: joinedorinvited, a member could page
backward and read events from before they joined/were invited. Both the
timeline and DAG-backfill paths now gate each event by the user's
membership at that event;shared/world_readableare unaffected.
Fixed
/syncnow honours the state, ephemeral, presence, and account_data
sub-filters. Only the timeline filter was applied before (and the state
filter only on left rooms); the rest were accepted but silently ignored.
Theirtypes/not_types/senders/limitconstraints now apply to
each section, including the room state filter on joined rooms.- Device deletion is now federated. Removing a device (logout,
/devices/{id}delete, or a dehydrated-device replace) didn't tell remote
servers, so they kept the dead device in their/keys/queryview. It now
emits anm.device_list_updatewithdeleted: trueto servers sharing a
room with the user, paired with the local key reclaim below. - Device deletion now reclaims its keys. Removing a device (logout,
/devices/{id}delete, or a dehydrated-device replace) only dropped the
device record, leaving itsdevice_keysandone_time_keysbehind to
accumulate forever.delete_devicenow cascades to both, scoped to the
one device (a device id that is a prefix of another is unaffected). - Outlier events now promote to live on re-delivery. An event first
seen as an outlier — fetched via an/eventprobe for auth/prev context
and never timelined — was dropped by the already-seen check when it later
arrived through/send, so it stayed invisible to/syncand never fired
its membership/device-list side effects. It's now promoted to a live
timeline event: the receive path re-auths it and re-persists it as live
reusing its nid (references stay intact). Backed by a new
event_timeline_posforward index, which also retires the O(timeline)
backward scan inevent_stream_pos. - Push-rule keys with escaped dots weren't resolved. The condition
key parser split on every., so a key likecontent.m\.mentions.room
(whosem.mentionssegment contains a literal dot) never matched.
It now honours\./\\escaping, which the MSC3952 mention rules
depend on. - Federated messages didn't trigger push notifications. The push
dispatch path only ran on locally-sent events; when a remote user
sent a message to a federated room, every local member's mobile
client stayed silent. Inbound federation now calls the same
dispatch_for_eventafter persistence, so remote-sender pushes go
through identical rule evaluation and gateway POST as local ones. m.room.server_aclwas only enforced on inbound /send. Banned
origins could still hit/make_join,/send_join,/make_knock,
/send_knock, and/v2/invite— i.e. join, knock, and invite
themselves into rooms whose ACL was supposed to keep them out.
All five handlers now run the same ACL check before doing room
work. Leave handlers are intentionally exempt per spec: a banned
origin must still be able to leave a room it's already in.- Own presence not visible in /sync.
collect_presence_events
filtered the requesting user out of the emitted peer set, so
clients that draw their own profile indicator from /sync (Element
X among them) fell back to "offline" for the requester. Now
always included. - Stored presence never decayed. A user who set themselves
online and closed their browser stayed "online" in every other
client indefinitely. Effective presence is now computed at read
time fromlast_active_mswithidle_after→ unavailable and
offline_after→ offline transitions; a background sweeper
persists those transitions and broadcasts the federation EDU so
remote servers see the new state. Thresholds configurable under
[presence](defaults: 5min / 30min / 60s sweep). - Push-notification retries are bounded. A failing pusher backs off
with a per-pusher exponential schedule and a hard ceiling instead of
stalling the push queue behind it. - Federation and sync robustness. A long tail of correctness fixes to
the inbound federation and/syncpaths: lazy-fetch of unknown
prev-events and auth-chain parents during the state-at-event check
(transient gaps no longer cascading into permanent rejections), correct
v12 auth context (no synthetic invite-stripped create injected), EDU
coalescing andm.room.server_aclapplied to room-scoped EDUs,
split-tracked stream watermark sonext_batchand delta scans agree,
redaction markers for not-yet-seen targets parked and applied on
arrival, and a localpart fallback for the display-name push rule so
.m.rule.contains_display_namefires for users without a set profile
name.