Repository navigation
Releases: ontola/atomic-server
Release list
v0.41.0-beta.8
-
Discovery (#2198): a drive's pkarr record also lists the public https origins of the servers hosting it (
_atomic_http), published at boot and every hour, so the app can find a drive's server from the secret alone. New anonymousGET /genesis?subject=<did>returns only a drive's genesis certificate, which the app verifies against the drive DID before connecting. Drives minted before genesis certificates existed answer 404 and are not found this way. -
Schemas (#2151): properties get content-addressed subjects,
atomic:prop:{blake3}of their ontology, shortname and datatype (docs/src/schema/property-identity.md). The server verifies the hash on genesis, accepts the same property from two devices, and refuses changes to itsparent,shortnameordatatype. Existing properties keep their subjects and stay editable. -
Class constraints (#2151): a Class's
constraintsmap holds JSON Schema keywords per property (enum,minimum,maximum,exclusiveMinimum,exclusiveMaximum,minLength,maxLength,minItems,maxItems,pattern,class), enforced on commit; peer commits and sync skip it. An emptyallowsOnlyon a Property now restricts nothing. -
Lenses (#2151):
atomic:lens:resources map one property onto another (rename,wrap,head,map,convert); the derived values appear in reads and indexes without changing signed data (docs/src/schema/lenses.md). -
atomic-cli schema push | lock | check: turn a JSON Schema or ontology file into an ontology and a lockfile that pins its property IDs (#2151).atomic_lib::schema::json_schemaimports JSON Schema andensure_ontologycreates the result. -
Forms: field options use JSON Schema keyword names (
minItems,maxItems,minimum,maximum; the old names are still read) and may only tighten the column's class constraints;describe_formreturns the form's JSON Schema (#2151). -
--served-domain-suffix/ATOMIC_SERVED_DOMAIN_SUFFIXtakes a comma-separated list (atomicserver.eu,atomic.place), so a node can answer under two domains while it moves from one to the other. -
MCP endpoint:
POST /mcp(Streamable HTTP) is the one implementation of the
MCP tools, for clients that cannot run a local process, such as claude.ai, and
for the local@tomic/mcp, which now only bridges to it. It is an OAuth 2.1
resource server (RFC 9728 metadata at/.well-known/oauth-protected-resource)
with the node as authorization server (/oauth/register,/oauth/authorize,
/oauth/token, PKCE S256, dynamic client registration,resourcechecked).
The person approves in the app (/app/authorize-mcp): which drives, and read
only or read and edit. Reading is as an issued agent under the normal rights
checks. If editing was allowed,edit_resource,create_resource(documents
take Markdown as_documentText) anddelete_resourceexist, and the node
signs those commits as that issued agent, never as the person. Revoking is
removing it under Connected apps.list_drives,get_resource,search,
query,get_user_classesandget_schemaare the read tools.
--app-url/ATOMIC_APP_URLsays where the approval page lives when the app
is not served by this node. -
The approval page only works with a node the person is already using: a link
naming another server is refused, and the redirect back to the client must be
exactly the one registered, so a crafted link cannot grant access to someone
else's key or run script in the app. -
atomic-cli connect: connect the CLI with a key made on this machine,
approved in the app (/app/connect-agent), instead of pasting your agent
secret. Without a config file, the CLI now points you to it rather than
asking for the secret. -
Forms (beta): guests can fill in a form without an account, and each answer
becomes a row in a Table. New default ontologyforms
(lib/defaults/forms.json:Form,FormPage,FormField,FormHeading,
FormParagraph,FormInfoBox,FormCondition,FormInviteCodeand their
properties). New public endpoints:GET /form/{id}(the form page),
GET /form/{id}/definition,GET /form/{id}/challenge(a self-hosted
proof-of-work captcha, ALTCHA),GET /form/{id}/imageand
POST /form/{id}/submit.{id}is a short publish slug or the form's
identifier. Only published forms inside their open/close window answer
(others get410); invite-only forms need a single-use code. Submissions
are validated against the form's questions and branching conditions
(answers to hidden questions are dropped), rate-limited per client address,
and written by the server's agent; a form can only write into, and show
visitors resources from, its own drive. A class extender adds
form-submission-summaryto a Form for readers who may see it.
server/build.rsembeds thebrowser/form-appbundle.
#875 -
Collection
/query: the rights walk no longer full-decodes ancestors (memoize by subject +get_resource_shallow). Denied members still do not consumepage_size, so a readable row after a private streak is returned. -
Commits whose Loro update depends on ops the server does not have are now
refused ("parked as pending") instead of being accepted without effect; the
browser resends a self-contained update. -
/download/files/{hash}and blob URLs serve the File's stored mimetype. -
A plain
GET /wsthat is not a WebSocket upgrade (a crawler, a pasted URL) now answers 400 instead of 500, so it no longer shows up as an incident in error reporting. -
A commit is also sent to WebSocket subscribers of the resource's parent. A
resource shared out of a drive the recipient can't read (a chatroom) can only
be subscribed to on its own, and a new child is a new subject, so its new
messages reached neither that subscription nor a drive subscription the
recipient could hold. Read rights are checked per subscriber against the
resource when it is sent, and a connection already reached through the
resource or its drive doesn't get it twice. -
End-to-end encrypted conversations.
atomic_lib::conversationderives an
agent's X25519encryptionKey, keeps a conversation's keys in a keyring
wrapped to each member, and seals and opens messages; the WASM build exports
it. New default ontologyconversations(lib/defaults/conversations.json)
with theConversationandSealedMessageclasses. New endpoint
/conversationslists the conversations on the server the requesting agent
is a member of. Seeplanning/encrypted-conversations.md. -
New default ontology
notifications(lib/defaults/notifications.json):
theInboxandNotificationclasses, and aninboxproperty the private
drive points to its Inbox with. -
Removed range-based set reconciliation (RBSR,
lib/src/sync/rbsr.rs). The
server rebuilt a drive's whole inventory for everyRBSR_FPround trip, so
the descent cost more than the one full version-vectorSYNCit replaced.
RBSR_FPis now answered with{"drive","unsupported":true}and no
fingerprints, which makes an older browser fall back to the fullSYNC
immediately.RBSR_ITEMSstays as the drive inventory, and aSYNCwith
subjectsis still honoured. Therbsrcapability is no longer advertised. -
The server raises its own file-descriptor soft limit to its hard limit at
startup. It already budgeted HTTP connections against the soft limit and kept
a reserve, but HTTP is not the only tenant of that pool: the database, Iroh's
QUIC sockets and every open websocket draw on it too, so on a stock soft limit
of 1024 the process can run out while the HTTP budget still looks healthy.
Staging did, for twenty-three minutes, with 2,655error accepting connection: No file descriptors available, Iroh unable to bind its hairpin probe, and a
panic at the tail. A process may raise its own soft limit as far as the hard
limit without privileges, and the connection budget is computed from whatever
is in force afterwards, so a refused raise is logged and not fatal. -
Publishing a plugin release that is already recorded on the server (the
same zip, uploaded by someone else) now grants the new publisher read on
theReleaseand its package File. Before, their Installation could not
resolve the release and activation failed with a 401. -
Version 2 request signatures (ontola/atomic-plugins#54). A request sent
withx-atomic-signature-version: 2is checked against
atomic-request-v2\n{METHOD}\n{full URL}\n{timestamp ms}\n{sha-256 hex of the body}instead of v1's"{url} {timestamp}", so a captured proof can
no longer be replayed with a different method or body within its five
minutes. v2 is opt-in: a request without the header is checked as v1
exactly as before, and a v2 request that fails is refused, never retried as
v1./app-agentand/plugin-view-tokenaccept v2 (they do not require it
yet); every other endpoint refuses a v2 signature with a 401 that says to
sign with v1, as do an unknown version and a v2 header without the
x-atomic-*headers. Cookies and WebSocketAUTHstay v1.atomic_lib
exportsrequest_signature_message_v2,RequestBindingand
client::get_authentication_headers_v2; shared test vectors live in
lib/src/authentication_v2_vectors.json. -
Identifiers are now emitted as
atomic:(atomic:{genesis},
atomic:agent:,atomic:commit:,atomic:blob:,atomic:node:). The
previousdid:ad:spelling is accepted forever and names the same
resource. New genesis certificates still encode the v1 header byte
(0x01) soGenesisCertliterals in downstream crates keep compiling;
GenesisCert::new_v2canonicalizes parent/drive strings toatomic:.
Decode accepts a0x02header. Existing v1 certificates and the
personal-drive singleton stay v1. Pairing is
atomic:node:{id}?v=1&drives=*;/resource?subject=is the HTTP
en...
v0.40.4
Security patch release for the 0.40 line. Both fixes concern the WASM plugin
installer, which is built by default (wasm-plugins is in the default feature
set) and reachable by any agent with ordinary write access to a Drive — no
admin rights required.
- Reject path traversal in plugin zip asset entries. A zip entry named
assets/../../..was joined onto the plugin directory using the raw,
attacker-controlled entry name, allowing arbitrary file write with
attacker-controlled content anywhere the server process could write
(GHSA-g2q9-fjm7-cmm3). Entry names underassets/must now be plain
relative paths of normal components. Reported by @T4ran24. - Reject path traversal in plugin
namespace/nameon uninstall. Both values
were read straight off the Plugin resource and joined into filesystem paths,
so a../payload could delete arbitrary files or recursively delete a
directory (GHSA-vr56-vwrw-w2vg). Both are now validated as a single path
segment on every path that reaches the filesystem, with a direct-child check
as defense in depth.
Version bumps: atomic_lib 0.40.1 -> 0.40.2, atomic-server 0.40.3 -> 0.40.4
(cli untouched). server's atomic_lib dep pinned to 0.40.2.
v0.41.0-beta.7
-
Store hosted files in S3 without silently falling back to node-local storage.
-
Fix stale collection queries bringing deleted sidebar resources back.
-
Improve browser database durability and resource save-state handling.
-
Isolate E2E shard state and simplify local test tooling; full develop CI passes on the release base.
-
Add drive-scoped authenticated browser peer sessions; subscription-independent signaling and optional temporary TURN credentials are provided by Atomic SaaS (#1396).
v0.41.0-beta.6
-
Desktop restore discovers reachable workspace sources and supports an explicit server address when automatic discovery does not find one.
-
Security audit fixes (
planning/security-audit-2026-09.md, #1384).
POST /iroh-syncrequires a signed agent with write on the drive and only
answers POST;SYNC_PUSHentries are checked against the admitted drive;
https://host/agents/{key}subjects bind the path key to the signing key
and are never fetched from other hosts; a genesis can no longer append to
a parent through its ownwritearray, squat another agent's
did:ad:agent:resource, or reserve top-level server paths; the request
origin used for token binding honoursHost/X-Forwarded-*only for the
configured domain, its tenants and loopback; IrohAUTHproofs name the
responder node (drive#nodeId, unbound proofs accepted only from paired
peers);/forget-peerneeds write on a drive the peer was paired for;
path-only auth signatures are refused; plugin zips and bookmark bodies go
through the SSRF guard with size caps; blob responses are hash-checked;
ephemeral relays require a subscriber; ACME errors instead of panicking
and certificates are renewed daily on disk (restart to serve); the
desktop and Android builds bind127.0.0.1;config.tomlis written
0600; wasmtime 47. API change:/plugin-listand the plugin UI files are
read as the calling agent, so clients must sign that request (the
data-browser does) or carry the session cookie. -
Fix: live collaboration stopped for the author of a document after a peer
edited it. The server stampslastCommitunder its own Loro peer after
applying a commit; the fan-out only forwarded the author's own bytes and
never echoed to the author, so an edit built on the stored snapshot could
not apply there. TheUPDATEnow carries everything the apply added and
reaches the author too (CommitResponse::fanout_delta). -
Commits are signed envelopes, not a queryable event log. Ordinary content
commits are not stored as resources after apply (genesis and
rights/parent/destroy stay). Loro binaries are not KV-index keys. The
/commitscollection is not created. UI reads author/dates from the
resource, not by fetchingdid:ad:commit:rows. A creation commit is
retained whether or not the client flaggedisGenesis. Clients no longer chain
previousCommit; apply no longer has a previous-commit validation gate.
History no longer offers a "Show Commit" link at a discarded envelope. -
Signed envelopes live on the resource (
Tree::Envelopes). Every
signed commit's JSON-AD is kept per resource, keyed by createdAt and
signature, in the same transaction as the state it signs. Not a
resource, not indexed.--envelope-retention/ATOMIC_ENVELOPE_RETENTION
islatest(the envelope that produced the current state; default) or
all(every envelope: a signed audit log).GET /history-attribution?subject=
(read-gated) answers who signed which Loro change, verified with the
apply code, plus whether every change is covered. Rust builder commits
andcreate_didnow tag their Loro change like the browser does, so
History maps versions to signers. The destroy envelope on the tombstone
(added forSYNC_DIFF.removeCommits) is now the subject's latest row
in this tree. Envelopes do not yet travel in bulk sync or the vault. -
Missing-drive bootstrap is no longer a free pass (OQ5). A
SYNC_PUSHor live write for a drive this node has never stored goes
throughadmit_unknown_drive:Publicnever creates one (even on
OpenPolicy); Owner mode enrolls only the owner; an authenticated
first-sync on an open node still works. AUTH-before-SYNC_PUSHalready
closed the unauthenticated wire; this closes the library path. -
SUB/UNSUBare engine-owned. Parse andcheck_readlive in
handle_frame_full; the WebSocket handler registers the connection
with the commit monitor only when the engine admits the subscription.
The0x20/0x21wire is unchanged (anonymousSUBon a public
share link still works). -
One subscription actor.
LoroSyncBroadcasteris gone; Loro
ephemera and drive presence fan out fromCommitMonitor(one mailbox,
oneUnsubscribeAllon socket close). Wire and behaviour unchanged. -
Bulk
SYNC_DIFF.removecan carry a signed destroy. When the sender
still holds the destroy commit on the tombstone,removeCommitsmaps
that subject to the JSON-AD envelope and the receiver applies it as a
peerCOMMIT. A bad signature does not fall back to the unsigned
tombstone path. Senders without the envelope still send a subject-only
remove[]entry (admission-gated). The envelope is only handed to a
session that may read the drive; a signed destroy that is already
stored here, or that predates the current resource's genesis, is
refused as a replay. The browser applies the envelope as a local-cache
write and removes the resource either way. Requiring an envelope on
every delete still waits onTree::Envelopes. -
AtomicTransport/SyncSessionfirst slice. The engine loop is
callable over any byte-pipe (lib/src/sync/transport.rs,
SyncSession::serve). Iroh and WebSocket still have their own
lifecycles; the outbox port and FRBopen_sync_sessionare not this
change.
v0.41.0-beta.5
-
Local full-text search in
atomic_lib(lib/src/search/): a KV inverted
index on the existing redb / sled / BTreeMap (OPFS) store. Indexes title,
description, and Loro document body on every commit; queries AND tokens,
rank with BM25, and match 1-edit prefix-fuzzy (avacadofindsavocado).
Exactproperty:"value"filters reuse the PropValSub index (emptyq+
isAis the file picker); there is no query language and no key escaping.
Db/AtomicNode/ WASMClientDb.searchexpose it. Hosted/search
is the same engine — Tantivy is removed.
The browser uses this index (MiniSearch is gone) and merges with/search
when online. Scale benches:
cargo bench -p atomic_lib --bench search_bench --features db-redb.
Seeplanning/local-search.md. -
Live collaboration is one binary frame on both transports. Edits in
progress, cursors and drive presence cross the WebSocket as
EPHEMERAL (0x40), the frame the Iroh link already used, in both
directions; the text framesLORO_SYNC_UPDATE,LORO_EPHEMERAL_UPDATE
andPRESENCE_UPDATE(base64 in JSON) are removed, the four subscribe /
unsubscribe text frames stay. The server ignores the agent a client puts
in the frame and stamps the connection's authenticated identity on every
copy it fans out. Between nodes the payload is now the raw bytes rather
than their base64 text, so cursors from a pre-2026-09-04 peer do not
decode on this build (and vice versa); nothing persistent is affected.
WsClient::send_loro_sync_update/send_loro_ephemeral_update/
send_presence_updatesend the binary frame. Capabilityephemeral. -
Drive sync is one binary frame on both transports. The browser's
hash-first probe and its reduced reconcile now arrive asSYNC (0x30)
with"probe": trueor"subjects": [...]in the JSON tail
(encode_sync_probe,encode_sync_filtered), handled by the shared sync
engine; a stale probe is answered with the newSYNC_RESEND (0x38).
The textSYNC_VVrequest, its textSYNC_RESEND <drive>answer and the
server handler's private copy of the reconcile are removed. Capability
sync-probe.WsClientreportsWsMessage::SyncResend. -
One WebSocket subscription frame.
SUB <subject>registers a
drive-wide subscription when the subject is a drive and a per-resource one
otherwise; the per-resource text frameSUBSCRIBE <subject>(which also
registered a spurious drive fan-out entry that delivered URL subjects'
commits twice) and the filter subscriptionSUBSCRIBE_QUERY(sent by
nothing outside the Rust integration tests) are removed, along with the
commit monitor's third subscription map, theMembershipNotification
fan-out andWsClient::subscribe_query.WsClient::subscribe_resource
sendsSUB. The lib's watched-query index (QueryFilter::watch,
DbEvent::QueryMembershipChanged) is unchanged; the Flutter event stream
still consumes it. -
atomic_lib::runtime::AtomicNodeis cut down to what binds to it. The
WASMClientDbis the only adapter on the node and usesfrom_db,db,
queryandapply_commit; those, plus the agent accessors and
IngestPolicy::{Hub, Peer, LocalCache}, are what remains.open/
NodeConfig/NodeStorage,get,mutate/ResourceEdit,subscribe
andsync_with_peerare removed (each was one line overDb, with no
consumer three days after landing). The seam stays and grows again from
what the next binding actually calls (planning/atomic-lib-runtime.md). -
Sync stack cleanup. No wire changes.
WsMessagelost itsCommitand
Resourcevariants and the client no longer parses the pre-v2COMMIT,
RESOURCE,AUTHENTICATEDandERRORtext frames (nothing has sent
them since the binary protocol);IngestPolicy::Replicawent with them.
CommitIngestOpts::hub/::peerare the two validation presets that were
copied in three places;ws_apply::apply_destroy_checked(a byte-identical
twin ofapply_destroy) is merged into it; the server's three copies of
"encode anUPDATE/DESTROYfor a change" are oneencode_change_frame.
Iroh: the deadsync_drive_with_peerwrapper and the private_forced
indirection are gone, the live-peer registry is aLazyLock<Mutex<HashMap>>
holding the pinned QUIC connection alongside the stream (a peer registered
before the registry was initialised used to be dropped silently), and the
sync-event debounce map is pruned instead of growing for the life of the
process. Test-only helpersengine::drive_items/drive_sync_hashand
OwnerPolicy::{owner_agent, hosted_drive_subjects}are removed. -
Sync protocol follow-ups (see
docs/src/websockets.md, "Changed in
2026-09"):- Challenge-bound
AUTHover WebSocket: the server's first frame is
CHALLENGE (0x42)with a per-connection nonce; a client signs
requestedSubject = "{origin}#{nonce}"and the proof is good on that
socket only, so a captured frame can no longer be replayed elsewhere
inside the five-minute window. Nonce-less proofs are still accepted
(engine::AuthChallenge::Issued);AuthChallenge::Requiredis the
strict mode, not yet wired to an option. Capabilityauth-nonce. - The WebSocket handler reads a client
HELLO(capabilityclient-hello)
and answersCOMMITwith a slimCOMMIT_OKof[request_id] [commit_id]
for a client that listedcommit-ok-slim(the browser and the Rust
WsClientboth do).protocol::decode_commit_okreads both forms. - Subscriptions follow the connection's identity: when an
AUTHchanges
it, the commit monitor re-runscheck_readfor every subject, drive and
filter subscription the connection holds and drops the unreadable ones
(RebindAgent, capabilityrebind-on-auth). Until now aSUBaccepted
as the owner kept delivering after the socket re-authenticated as a
stranger. WsMessage::Erroris a struct (request_id,code,message);
WsClient::post_commitsettles only on its ownrequest_idand returns
the commit id, so several commits can be in flight on one connection.
Unknown text frames surface asWsMessage::Unrecognizedinstead of an
error.WsClientsends aHELLOon connect and answers the server's
challenge automatically (WsClient::auth_subject).- New error code
INVALID_SIGNATURE (9)for aCOMMITwhose signature
does not verify; it went out asUNKNOWNbefore. - Iroh:
LIVE_CONNECTIONSis keyed by peer and pruned when the peer is
removed. It was an append-only list that pinned every QUIC connection
ever dialed for the life of the process. ws_apply::apply_commit_json(the replica applier that skips rights
checks, audit finding F6) is gone: its only route was the pre-v2COMMIT
text frame, which no server sends. The server's rights-checked
handlers::commit::apply_commit_jsonis the only function of that name.- New integration tests:
ws_errors(ERROR format,request_idecho and
codes), the nonce, the identity re-bind, and the slim ack.
- Challenge-bound
-
Sync protocol hardening (WebSocket and Iroh; wire reference rewritten in
docs/src/websockets.md, see its "Changed in 2026-09" section):AUTHproofs expire: a signed authentication older than five minutes
(atomic_lib::authentication::AUTH_MAX_AGE_MS) is refused, on WebSocket,
Iroh and the HTTP auth headers alike. Until now only future-dated
timestamps were rejected, so a captured proof never expired.- Over WebSocket,
AUTH.requestedSubjectmust name the server: either the
origin the socket was opened on or the configured server URL
(AuthBinding::Origins, the same tolerance the HTTP auth headers have); a
proof signed for another server, or for the agent's own subject, no
longer opens a session. A refusedAUTHanswers
with the new error codeAUTH_FAILED (8). The RustWsClientnow signs
the server origin (it signed the agent subject before). The Iroh
initiator only accepts an auth-back signed for its own node id. - The hash-first
SYNC_VVprobe and theRBSR_FP/RBSR_ITEMSframes
are gated bycheck_readper subject, like the fullSYNC_VVexchange.
Previously any socket could enumerate every subject and version vector of
any drive it could name. - On the Iroh live link, destroys travel as the signed destroy
COMMIT
(DbEvent::Destroyednow carriescommit_json); a nakedDESTROYframe
from a peer is ignored instead of applied on the connection's drive-level
write verdict. - Capability advertisement:
AUTH_OKcarries a JSON array of capability
names (atomic_lib::sync::protocol::CAPABILITIES),HELLOcarries the
same list after the device name. Both are optional trailing bytes. UNSUB (0x21)now actually cancels a drive subscription (it previously
edited a set nothing read).KEEPALIVE (0x41)is echoed over WebSocket so
browsers can detect a dead socket.- Shared golden wire vectors (
lib/src/sync/protocol_vectors.json, mirrored
atbrowser/lib/src/protocol_vectors.json) pin the Rust and TypeScript
codecs byte-for-byte. - Integration tests bind the test server to
127.0.0.1and fall back to an
OS-assigned port whenportpickerfinds none (hosts without IPv6).
-
Tauri Android: ship
arm64-v8aonly. The sideloadable universal APK was ~369 MB because it bundled four copies oflibatomic_server_tauri.so(armeabi-v7a / x86 / x86_64 as well). Phones and tablets we install on are arm64; override withcargo tauri android build --target …for an Intel emulator.
v0.41.0-beta.4
- Fix: with
ATOMIC_HOME_DRIVEset,GET /serveranswered 500 for plain JSON and JSON-LD (Accept: application/json): thehomeDriveproperty the endpoint sets was never defined in the default store, so rendering tried to fetch it from atomicdata.dev. JSON-AD (what the data-browser asks for) was unaffected. The property is now part of the defaults.
v0.41.0-beta.3
- Git / CI: one integration branch (
develop) plus stablev*tags. Staging
followsdevelop; production and live docs follow a tagged release.master
is no longer a deploy or docs trigger, andmainis not introduced as a
copy of the latest tag. - Fix: Atomic Canvas no longer returns to the gallery when the screen rotates.
Android was treating the rotation as a back press. - Fix: built-in defaults (
lib/defaults/*.json+ base models) now reach
existing stores. The store records a fingerprint of the embedded defaults;
every open (server and browser/OPFS) re-seeds add-only when the fingerprint
changed, so a Property or Class added in a new release no longer 404s against
a store seeded by an older one. Existing values, including user edits to
default resources, are never overwritten.--repopulate-defaultsremains as
a forced re-run with the same add-only semantics. atomic_lib:atomic_lib::runtime::AtomicNode— a named node surface over
Db(open,get,query,apply_commit(json, IngestPolicy),mutate,
subscribe,sync_with_peer). Thin delegation, no behaviour change; the
WASMClientDbis the first adapter on it.IngestPolicy::{Hub, Peer, Replica, LocalCache}names the four commit-validation profiles.
sync::engine::ingest_commitreturns theCommitResponsethat
ingest_commit_jsonserializes;sync::ws_apply::apply_commit_jsonnow
returns theCommitResponseinstead of(). See
planning/runtime-boundary-decision.md.
v0.41.0-beta.2
This is the local-first release. Atomic Data no longer needs a server to exist.
It is also the first release with new features since v0.40.0 in October 2024 — v0.40.2 and v0.40.3 were security-only patches. That is 1,222 commits and ~334,000 lines. The highlights below cover the whole 0.41 line; per-release detail continues in the v0.41.0-beta.1 and v0.41.0-beta.0 sections of the changelog, and everything front-end is in the browser changelog.
Highlights
Your data works without us. Create a drive, write to it, and read it back with no server in reach. Every write is a signed CRDT commit against a local database, so the app is fully usable offline and syncs when it can. This is not a cache in front of a server — the browser runs the same Rust storage and query engine through WASM, which is why an offline table computes the same totals as an online one.
did:ad — identifiers that don't belong to a hostname. Resources are addressed by a decentralized identifier rather than a URL on someone's domain, and resolved peer-to-peer over the Mainline DHT. No DNS, no certificate authority, no hosting company in the trust path. Combined with self-verifying genesis certificates, a resource proves who created it on its own terms. Your agent is now just a private key: no account on any server, nothing to be locked out of.
Peers, not clients. A sync protocol (#1178) and Iroh/Mainline peer sync let two devices converge directly. Pair a phone by scanning a QR code, mount a drive, and edits flow between them. A server, when you use one, is now described as just another device you sync with — it is a convenience, not the source of truth.
Live everything. Live queries (#1174) mean a collection updates as the data does, rather than when you reload. On top of that sits a full presence layer (#1229): see who else is here as avatars, follow someone through the app, watch "typing…" appear in chats and comments, and see live cursors while co-editing a document. Presence rides a drive-scoped ephemeral relay, so it never touches your stored data.
A new document editor. A ground-up rich text editor backed by a Loro CRDT, so two people can edit the same document at once without a lock or a merge conflict — with slash commands, resource mentions, tables, math and a collaborative canvas alongside it. And peer-to-peer video and audio calls in the meeting panel, where media never passes through a server at all.
Tables became apps. A Kanban board (#1198) with drag-and-drop and custom card ordering, plus calendar and timer views — all the same grid underneath, so cells stay editable and columns sortable whichever way you look at them. Then: dashboards that compose numbers, charts and embedded tables over your data; computed columns (a live duration, days-since, quantity × price); totals computed by the store across every matching row rather than the page on screen; row actions and one-tap create buttons that turn a table into something you press rather than something you fill in; and thirteen ready-made templates — issue tracker, CRM, expenses, grocery list, plant care, workout log and more. None of it ships a custom renderer: a mini-app is configuration, so the built-in assistant can build and reshape one for you.
Private by default. The browser's local database is now one encrypted store per agent (XChaCha20-Poly1305 at rest), so switching accounts or signing out genuinely closes the door. Account backup is passkey-first — one prompt, no secret to write down — and a device lock can seal the keypair after inactivity or when the browser closes.
And it got much faster. A query and indexing rework cut a 1000-member collection query by ~88% at the Rust level and ~70% over HTTP.
Also in this line: content localization with a LocalizedText datatype and locale-routed site templates, a desktop app that mounts your drives as a real virtual drive in Finder or your file manager, emoji and cover images on any resource, favorites, stateless invites, and the removal of SERVER_URL so a server can answer on several domains at once.
Beta, and honest about it: the storage format changed, so your database migrates on first run — take a backup first. See STATUS.md for what is expected to stay stable.
Security
All four fixes documented under v0.40.3 are present here — they reached this line through the develop merge, not through that tag. One deliberate divergence:
- The commit-level
check_server_managed_propertiesguard that accompanied the/downloadarbitrary-file-read fix is not part of this line. It rejected any commit writinginternalId, which breaks local-first uploads — here the client legitimately assigns that content hash at genesis./downloadresolvesinternalIdas an opaque content-addressed blob key rather than a filesystem path, so the path-traversal primitive the guard defended against does not exist. Thedownload.rshardening itself is retained.
Added
DashboardandBlockclasses in the default store, withdashboard-blocks/dashboard-layoutandblock-kind/block-source/block-view/block-query/block-aggregate/block-chart-spec. OneBlockclass carrying a kind string, the wayViewdoes, so a new block kind needs no ontology change. This is the schema behind the browser's new dashboards; the numbers themselves come from the aggregation engine already inQuery.- A
view-row-actionsproperty in the default store: the buttons a view puts on each row, as a JSON array of{ id, label, kind, property, value }.kindis a closed vocabulary of patches (setNow/setValue/toggle/increment) rather than code, so a person can edit one in a dialog and an assistant can write one, and each press stays an ordinary commit. - A
view-quick-addproperty in the default store: the button a view offers for creating a row, as a JSON object{ label, field?, placeholder?, presets? }. Each preset is{ kind, property, value? }using the same closed patch vocabulary asview-row-actions, applied to the row being created — so a button can stamp the current time and create in one press. dashboard-layoutholds{ subject, w, h }— sizes only. It previously also carriedx/y, which no renderer read.- A
block-quick-addproperty and a'create'block kind, so a Dashboard can carry a create button. It holds the same JSON shape asview-quick-add, which is what lets the table and the dashboard describe one button identically. - Aggregate queries: a
Querycan carry anaggregation, and the store answers it by walking its own index instead of returning rows for the caller to add up. Sum, count, average, min and max over every row a query matches — filters included, paging excluded — plus an optional breakdown giving one subtotal per distinct value of a column, with day and month buckets resolved in the caller's timezone. Results arrive on the Collection's newcollection/aggregatesproperty. Because the browser's local database runs this same code through wasm, a table shows identical totals with no server in reach. Note thatcountcounts the rows the asking agent may actually read, which can be lower thantotalMembers— that one counts raw index hits, before rights are applied. - Argon2id key derivation in
atomic-libasvault::keys(argon2id_derive_key), exposed to the browser asargon2idDeriveKey. This backs the passkey-wrapped ("envelope v2") backup of the agent secret: AES-GCM runs natively in WebCrypto, and Argon2id is the one primitive the Web platform's crypto API is missing. Defaults to ~64 MiB / 3 iterations / 1 lane. EncryptedBackendwraps any redbStorageBackendand encrypts data at rest with XChaCha20-Poly1305 (4 KiB blocks, a fresh random nonce per write, block-index AAD, key-check header), so resources, Loro snapshots, blobs and derived indexes are all ciphertext. Backs the browser's per-agent OPFS databases.- A
coverImageproperty in the default store — an image File shown as a decorative banner at the top of a resource's page — and acoverImageFocusfloat (0-1) holding that banner's vertical focal point, since a wide crop of a photo rarely has its subject in the middle.
Changed
- Vector search is now opt-in (pass
--enable-vector-index/ATOMIC_ENABLE_VECTOR_INDEX) instead of on by default. Loading embedding models and indexing every write on a plain create/edit had a real, measured performance cost that most deployments don't need. - BREAKING (build):
vector-searchis no longer a default cargo feature — build with--features vector-searchfor semantic search. It was already opt-in at runtime and already absent from every released binary, butcargo install atomic-serverstill compiled the whole fastembed/lancedb/arrow/ort stack for a feature that stays switched off unless asked for: a very large build, requiringprotoc, and failing outright on musl whereorthas no prebuilt ONNX Runtime binary. Default now matches both the runtime default and what actually ships. Passing--enable-vector-indexto a build without the feature warns instead of silently doing nothing. - Query/index performance rework (
atomic-lib): collection queries now read materialized rows instead of decoding a Loro CRDT snapshot per member, permission checks are memoized per request, and the query-members index uses compact query ids with typed, order-preserving sort keys. Verified paired before/after: a 1000-member collection query dropped ~88% at the Rust level (52ms → 6–7ms) and ~70% over HTTP in a single round trip (~104ms → ~30ms). Seeplanning/index-performance.md. - Live-query index ...
v0.40.3
Security patch release. All four fixes below are included.
- Guard outbound fetches (
/bookmark,/import) against SSRF: reject loopback,
RFC1918/CGNAT, and link-local (incl. cloud metadata) targets on every
connection and redirect hop, plus a scheme check. Escape hatch:
ATOMIC_ALLOW_PRIVATE_FETCH=1. Reported by Ray Sabee / Whitehat Security
(@raysabee). - Close two bugs undermining the single-use guarantee of the bootstrap
/setupinvite: an inverted expiry check that rejected valid invites and
let expired ones through, and a TOCTOU race onusagesLeftthat let
concurrent requests both redeem what's meant to be a single-use invite.
Reported by @luuhung1217. - Block arbitrary file read via
internalId: a signed Commit could set this
server-managed property directly, and/downloadtrusted it verbatim as a
filesystem path (traversal / absolute path).internalIdis now denied in
externally-submitted commits, and/downloadindependently sanitizes and
confines the resolved path to the uploads directory. GHSA-8vc4-8hjq-988p,
reported by @luuhung1217. - Prevent stored XSS via uploaded files:
/downloadnow forces
Content-Disposition: attachmentandX-Content-Type-Options: nosniff, so
an uploaded HTML/SVG file can no longer render inline in the app's own
origin. GHSA-x277-3wcg-g9r2, reported by @luuhung1217
v0.40.2
- Security (SSRF): the
/bookmarkand/importendpoints fetched a
caller-supplied URL with no restriction on the target, letting an
unauthenticated caller make the server request internal-only hosts
(loopback, private ranges, and the link-local cloud-metadata endpoint) and,
via bookmark, read the response back. Outbound fetches are now blocked from
reaching non-public addresses, validated at DNS-resolution time so redirects
and DNS-rebinding are covered too. SetATOMIC_ALLOW_PRIVATE_FETCH=1to opt
out (e.g. for intentional LAN fetches). Reported by Ray Sabee / Whitehat
Security (whitehatsecurity.nl).