🌐 US-Proxy
class="logged-out env-production page-responsive" style="word-wrap: break-word;" >
Skip to content

Releases: ontola/atomic-server

v0.41.0-beta.8

v0.41.0-beta.8 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 15 Oct 08:22
25a1487
  • 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 anonymous GET /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 its parent, shortname or datatype. Existing properties keep their subjects and stay editable.

  • Class constraints (#2151): a Class's constraints map 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 empty allowsOnly on 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_schema imports JSON Schema and ensure_ontology creates 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_form returns the form's JSON Schema (#2151).

  • --served-domain-suffix / ATOMIC_SERVED_DOMAIN_SUFFIX takes 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, resource checked).
    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) and delete_resource exist, 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_classes and get_schema are the read tools.
    --app-url / ATOMIC_APP_URL says 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 ontology forms
    (lib/defaults/forms.json: Form, FormPage, FormField, FormHeading,
    FormParagraph, FormInfoBox, FormCondition, FormInviteCode and 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}/image and
    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 get 410); 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-summary to a Form for readers who may see it.
    server/build.rs embeds the browser/form-app bundle.
    #875

  • Collection /query: the rights walk no longer full-decodes ancestors (memoize by subject + get_resource_shallow). Denied members still do not consume page_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 /ws that 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::conversation derives an
    agent's X25519 encryptionKey, keeps a conversation's keys in a keyring
    wrapped to each member, and seals and opens messages; the WASM build exports
    it. New default ontology conversations (lib/defaults/conversations.json)
    with the Conversation and SealedMessage classes. New endpoint
    /conversations lists the conversations on the server the requesting agent
    is a member of. See planning/encrypted-conversations.md.

  • New default ontology notifications (lib/defaults/notifications.json):
    the Inbox and Notification classes, and an inbox property 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 every RBSR_FP round trip, so
    the descent cost more than the one full version-vector SYNC it replaced.
    RBSR_FP is now answered with {"drive","unsupported":true} and no
    fingerprints, which makes an older browser fall back to the full SYNC
    immediately. RBSR_ITEMS stays as the drive inventory, and a SYNC with
    subjects is still honoured. The rbsr capability 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,655 error 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
    the Release and 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
    with x-atomic-signature-version: 2 is 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-agent and /plugin-view-token accept 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 WebSocket AUTH stay v1. atomic_lib
    exports request_signature_message_v2, RequestBinding and
    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
    previous did:ad: spelling is accepted forever and names the same
    resource. New genesis certificates still encode the v1 header byte
    (0x01) so GenesisCert literals in downstream crates keep compiling;
    GenesisCert::new_v2 canonicalizes parent/drive strings to atomic:.
    Decode accepts a 0x02 header. Existing v1 certificates and the
    personal-drive singleton stay v1. Pairing is
    atomic:node:{id}?v=1&drives=*; /resource?subject= is the HTTP
    en...

Read more

v0.40.4

Choose a tag to compare

@github-actions github-actions released this 17 Sep 17:42

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 under assets/ must now be plain
    relative paths of normal components. Reported by @T4ran24.
  • Reject path traversal in plugin namespace/name on 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

v0.41.0-beta.7 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 12 Sep 18:03
e110aa4
  • 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

v0.41.0-beta.6 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 09 Sep 04:15
270e80b
  • 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-sync requires a signed agent with write on the drive and only
    answers POST; SYNC_PUSH entries 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 own write array, squat another agent's
    did:ad:agent: resource, or reserve top-level server paths; the request
    origin used for token binding honours Host/X-Forwarded-* only for the
    configured domain, its tenants and loopback; Iroh AUTH proofs name the
    responder node (drive#nodeId, unbound proofs accepted only from paired
    peers); /forget-peer needs 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 bind 127.0.0.1; config.toml is written
    0600; wasmtime 47. API change: /plugin-list and 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 stamps lastCommit under 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. The UPDATE now 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
    /commits collection is not created. UI reads author/dates from the
    resource, not by fetching did:ad:commit: rows. A creation commit is
    retained whether or not the client flagged isGenesis. 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
    is latest (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
    and create_did now tag their Loro change like the browser does, so
    History maps versions to signers. The destroy envelope on the tombstone
    (added for SYNC_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_PUSH or live write for a drive this node has never stored goes
    through admit_unknown_drive: Public never creates one (even on
    OpenPolicy); Owner mode enrolls only the owner; an authenticated
    first-sync on an open node still works. AUTH-before-SYNC_PUSH already
    closed the unauthenticated wire; this closes the library path.

  • SUB / UNSUB are engine-owned. Parse and check_read live in
    handle_frame_full; the WebSocket handler registers the connection
    with the commit monitor only when the engine admits the subscription.
    The 0x20 / 0x21 wire is unchanged (anonymous SUB on a public
    share link still works).

  • One subscription actor. LoroSyncBroadcaster is gone; Loro
    ephemera and drive presence fan out from CommitMonitor (one mailbox,
    one UnsubscribeAll on socket close). Wire and behaviour unchanged.

  • Bulk SYNC_DIFF.remove can carry a signed destroy. When the sender
    still holds the destroy commit on the tombstone, removeCommits maps
    that subject to the JSON-AD envelope and the receiver applies it as a
    peer COMMIT. 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 on Tree::Envelopes.

  • AtomicTransport / SyncSession first 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 FRB open_sync_session are not this
    change.

v0.41.0-beta.5

v0.41.0-beta.5 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 04 Sep 17:07
fa8fa59
  • 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 (avacado finds avocado).
    Exact property:"value" filters reuse the PropValSub index (empty q +
    isA is the file picker); there is no query language and no key escaping.
    Db / AtomicNode / WASM ClientDb.search expose 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.
    See planning/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 frames LORO_SYNC_UPDATE, LORO_EPHEMERAL_UPDATE
    and PRESENCE_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_update send the binary frame. Capability ephemeral.

  • Drive sync is one binary frame on both transports. The browser's
    hash-first probe and its reduced reconcile now arrive as SYNC (0x30)
    with "probe": true or "subjects": [...] in the JSON tail
    (encode_sync_probe, encode_sync_filtered), handled by the shared sync
    engine; a stale probe is answered with the new SYNC_RESEND (0x38).
    The text SYNC_VV request, its text SYNC_RESEND <drive> answer and the
    server handler's private copy of the reconcile are removed. Capability
    sync-probe. WsClient reports WsMessage::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 frame SUBSCRIBE <subject> (which also
    registered a spurious drive fan-out entry that delivered URL subjects'
    commits twice) and the filter subscription SUBSCRIBE_QUERY (sent by
    nothing outside the Rust integration tests) are removed, along with the
    commit monitor's third subscription map, the MembershipNotification
    fan-out and WsClient::subscribe_query. WsClient::subscribe_resource
    sends SUB. The lib's watched-query index (QueryFilter::watch,
    DbEvent::QueryMembershipChanged) is unchanged; the Flutter event stream
    still consumes it.

  • atomic_lib::runtime::AtomicNode is cut down to what binds to it. The
    WASM ClientDb is the only adapter on the node and uses from_db, db,
    query and apply_commit; those, plus the agent accessors and
    IngestPolicy::{Hub, Peer, LocalCache}, are what remains. open /
    NodeConfig / NodeStorage, get, mutate / ResourceEdit, subscribe
    and sync_with_peer are removed (each was one line over Db, 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. WsMessage lost its Commit and
    Resource variants and the client no longer parses the pre-v2 COMMIT ,
    RESOURCE , AUTHENTICATED and ERROR text frames (nothing has sent
    them since the binary protocol); IngestPolicy::Replica went with them.
    CommitIngestOpts::hub / ::peer are the two validation presets that were
    copied in three places; ws_apply::apply_destroy_checked (a byte-identical
    twin of apply_destroy) is merged into it; the server's three copies of
    "encode an UPDATE/DESTROY for a change" are one encode_change_frame.
    Iroh: the dead sync_drive_with_peer wrapper and the private _forced
    indirection are gone, the live-peer registry is a LazyLock<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 helpers engine::drive_items / drive_sync_hash and
    OwnerPolicy::{owner_agent, hosted_drive_subjects} are removed.

  • Sync protocol follow-ups (see docs/src/websockets.md, "Changed in
    2026-09"):

    • Challenge-bound AUTH over 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::Required is the
      strict mode, not yet wired to an option. Capability auth-nonce.
    • The WebSocket handler reads a client HELLO (capability client-hello)
      and answers COMMIT with a slim COMMIT_OK of [request_id] [commit_id]
      for a client that listed commit-ok-slim (the browser and the Rust
      WsClient both do). protocol::decode_commit_ok reads both forms.
    • Subscriptions follow the connection's identity: when an AUTH changes
      it, the commit monitor re-runs check_read for every subject, drive and
      filter subscription the connection holds and drops the unreadable ones
      (RebindAgent, capability rebind-on-auth). Until now a SUB accepted
      as the owner kept delivering after the socket re-authenticated as a
      stranger.
    • WsMessage::Error is a struct (request_id, code, message);
      WsClient::post_commit settles only on its own request_id and returns
      the commit id, so several commits can be in flight on one connection.
      Unknown text frames surface as WsMessage::Unrecognized instead of an
      error. WsClient sends a HELLO on connect and answers the server's
      challenge automatically (WsClient::auth_subject).
    • New error code INVALID_SIGNATURE (9) for a COMMIT whose signature
      does not verify; it went out as UNKNOWN before.
    • Iroh: LIVE_CONNECTIONS is 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-v2 COMMIT
      text frame, which no server sends. The server's rights-checked
      handlers::commit::apply_commit_json is the only function of that name.
    • New integration tests: ws_errors (ERROR format, request_id echo and
      codes), the nonce, the identity re-bind, and the slim ack.
  • Sync protocol hardening (WebSocket and Iroh; wire reference rewritten in
    docs/src/websockets.md, see its "Changed in 2026-09" section):

    • AUTH proofs 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.requestedSubject must 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 refused AUTH answers
      with the new error code AUTH_FAILED (8). The Rust WsClient now 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_VV probe and the RBSR_FP / RBSR_ITEMS frames
      are gated by check_read per subject, like the full SYNC_VV exchange.
      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::Destroyed now carries commit_json); a naked DESTROY frame
      from a peer is ignored instead of applied on the connection's drive-level
      write verdict.
    • Capability advertisement: AUTH_OK carries a JSON array of capability
      names (atomic_lib::sync::protocol::CAPABILITIES), HELLO carries 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
      at browser/lib/src/protocol_vectors.json) pin the Rust and TypeScript
      codecs byte-for-byte.
    • Integration tests bind the test server to 127.0.0.1 and fall back to an
      OS-assigned port when portpicker finds none (hosts without IPv6).
  • Tauri Android: ship arm64-v8a only. The sideloadable universal APK was ~369 MB because it bundled four copies of libatomic_server_tauri.so (armeabi-v7a / x86 / x86_64 as well). Phones and tablets we install on are arm64; override with cargo tauri android build --target … for an Intel emulator.

v0.41.0-beta.4

v0.41.0-beta.4 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 03 Sep 18:54
72f1c53
  • Fix: with ATOMIC_HOME_DRIVE set, GET /server answered 500 for plain JSON and JSON-LD (Accept: application/json): the homeDrive property 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

v0.41.0-beta.3 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 01 Sep 21:39
f97d12c
  • Git / CI: one integration branch (develop) plus stable v* tags. Staging
    follows develop; production and live docs follow a tagged release. master
    is no longer a deploy or docs trigger, and main is 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-defaults remains 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
    WASM ClientDb is the first adapter on it. IngestPolicy::{Hub, Peer, Replica, LocalCache} names the four commit-validation profiles.
    sync::engine::ingest_commit returns the CommitResponse that
    ingest_commit_json serializes; sync::ws_apply::apply_commit_json now
    returns the CommitResponse instead of (). See
    planning/runtime-boundary-decision.md.

v0.41.0-beta.2

v0.41.0-beta.2 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 02 Aug 09:45

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_properties guard that accompanied the /download arbitrary-file-read fix is not part of this line. It rejected any commit writing internalId, which breaks local-first uploads — here the client legitimately assigns that content hash at genesis. /download resolves internalId as an opaque content-addressed blob key rather than a filesystem path, so the path-traversal primitive the guard defended against does not exist. The download.rs hardening itself is retained.

Added

  • Dashboard and Block classes in the default store, with dashboard-blocks / dashboard-layout and block-kind / block-source / block-view / block-query / block-aggregate / block-chart-spec. One Block class carrying a kind string, the way View does, 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 in Query.
  • A view-row-actions property in the default store: the buttons a view puts on each row, as a JSON array of { id, label, kind, property, value }. kind is 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-add property 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 as view-row-actions, applied to the row being created — so a button can stamp the current time and create in one press.
  • dashboard-layout holds { subject, w, h } — sizes only. It previously also carried x/y, which no renderer read.
  • A block-quick-add property and a 'create' block kind, so a Dashboard can carry a create button. It holds the same JSON shape as view-quick-add, which is what lets the table and the dashboard describe one button identically.
  • Aggregate queries: a Query can carry an aggregation, 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 new collection/aggregates property. Because the browser's local database runs this same code through wasm, a table shows identical totals with no server in reach. Note that count counts the rows the asking agent may actually read, which can be lower than totalMembers — that one counts raw index hits, before rights are applied.
  • Argon2id key derivation in atomic-lib as vault::keys (argon2id_derive_key), exposed to the browser as argon2idDeriveKey. 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.
  • EncryptedBackend wraps any redb StorageBackend and 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 coverImage property in the default store — an image File shown as a decorative banner at the top of a resource's page — and a coverImageFocus float (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-search is no longer a default cargo feature — build with --features vector-search for semantic search. It was already opt-in at runtime and already absent from every released binary, but cargo install atomic-server still compiled the whole fastembed/lancedb/arrow/ort stack for a feature that stays switched off unless asked for: a very large build, requiring protoc, and failing outright on musl where ort has no prebuilt ONNX Runtime binary. Default now matches both the runtime default and what actually ships. Passing --enable-vector-index to 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). See planning/index-performance.md.
  • Live-query index ...
Read more

v0.40.3

Choose a tag to compare

@github-actions github-actions released this 06 Jul 10:00

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
    /setup invite: an inverted expiry check that rejected valid invites and
    let expired ones through, and a TOCTOU race on usagesLeft that 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 /download trusted it verbatim as a
    filesystem path (traversal / absolute path). internalId is now denied in
    externally-submitted commits, and /download independently sanitizes and
    confines the resolved path to the uploads directory. GHSA-8vc4-8hjq-988p,
    reported by @luuhung1217.
  • Prevent stored XSS via uploaded files: /download now forces
    Content-Disposition: attachment and X-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

Choose a tag to compare

@github-actions github-actions released this 03 Jul 17:22
  • Security (SSRF): the /bookmark and /import endpoints 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. Set ATOMIC_ALLOW_PRIVATE_FETCH=1 to opt
    out (e.g. for intentional LAN fetches). Reported by Ray Sabee / Whitehat
    Security (whitehatsecurity.nl).