Skip to main content

Aether-Core

Delete the backend. Your React state is the database. Two lines of code make it multiplayer, offline-first, and encrypted end-to-end so even your own server can't read it.

tests CI security e2e-encrypted benchmarks license npm

The 15-second demo

Open web/demos/shopping-list.html in two tabs. Type "eggs" in one, watch it appear in the other. Go to airplane mode, check items off, come back online — everything syncs. No backend was written. There is no API. There is no ORM.

That's Aether-Core.

What it is

Most collaborative apps look like this:

Browser ⇄ REST API ⇄ ORM ⇄ Database
         + auth, validation, business logic, migrations,
           DTOs, serializers, controllers, background jobs...

Aether-Core looks like this:

Browser ⇄ dumb relay (opaque bytes) ⇄ append-only log

The entire backend is a relay. It cannot read your data, does not implement features, does not validate anything, and does not have endpoints. It has one job: forward CRDT operations between clients and append them to a log. That's it.

Your app's state lives in the browser as ordinary JavaScript variables. Adding a feature means adding a variable. Never a migration. Never a route. Never a controller.

Why this is different

Nobody else is doing this. Yjs, Automerge, and Loro are excellent CRDT libraries, but they leave the server problem to you — you still write API endpoints, still design a persistence layer, still bolt on auth. Aether-Core deletes that entire stack.

The wedge:

Yjs / Automerge / Loro Aether-Core
CRDT sync
Backend code required You write it None
Auth built-in ❌ bring your own ✅ HMAC by default
Rate limiting built-in ✅ token bucket
Payload caps built-in ✅ configurable
E2E value encryption in the recommended path Ecosystem addon (e.g. Serenity) ✅ ships in web/aether-crypto.js
Anti-entropy after partition State-vector sync (O(delta)) Merkle-trie (O(k log N))
LAN peer auto-discovery ✅ mDNS opt-in

Everything on the right column is default-on, opt-out. Everything on the left column is your problem, or exists only as an ecosystem addon rather than the built-in path.

Before / after

// BEFORE: single-user React
function ShoppingList() {
  const [items, setItems] = useState([]);
  const add = (text) => setItems([...items, { text, done: false }]);
  return <List items={items} onAdd={add} />;
}
// AFTER: same component, multiplayer + offline + persisted
import { useAether } from '@nishantbhatte/aether-core/react';

function ShoppingList() {
  const [items, setItems] = useAether('items', []);
  const add = (text) => setItems([...items, { text, done: false }]);
  return <List items={items} onAdd={add} />;
}

Two changes: the import, and useState -> useAether. That's it. Multiple tabs sync instantly. Offline edits queue and merge on reconnect. State is durable across restarts.

Install

pip install aether-zta                      # Python relay + CLI
npm install @nishantbhatte/aether-core      # Browser client + React hooks

Quickstart

The recommended path is uv — no global installs:

git clone https://github.com/IronFighter23/aether-core
cd aether-core
uv sync
uv run aether-demo

Then open any of the demos:

  • Shopping list — start here. Zero explanation needed. Open in two tabs, watch it work.
  • Live cursors — see other visitors' cursors move in real time.
  • Live poll — everyone votes, tallies update live, no server aggregation.
  • Kanban board — richer state, drag & drop.
  • Firewall topology — the original security-engineer demo. Kept because it's technically interesting, but not the primary front door.

End-to-end encryption (new in v0.5)

Because the relay treats every value as opaque bytes, you can encrypt those bytes on the client and the relay never sees plaintext — including someone who runs the relay for you:

<script src="aether.js"></script>
<script src="aether-crypto.js"></script>
<script>
  const room = await AetherCrypto.roomFromPassphrase(
    'shared-passphrase-users-typed',
    'app-embedded-salt',
  );
  const aether = new Aether('ws://localhost:8211', { transform: room.transform,   // AES-256-GCM in the browser });
</script>

Full docs: docs/e2e-encryption.md.

IndexedDB persistence (default in v0.5)

web/aether.js picks the best persistence backend available automatically:

  • IndexedDB (default when available) — per-key async writes, quota measured in GB.
  • localStorage (fallback) — blob format, backward-compatible with pre-v0.5 caches, used when IDB is unavailable (private mode in some browsers).
  • In-memory only — when you pass persist: false.

No code change needed to get IDB — just upgrade. For advanced setups (custom DB name, multi-room scoping, quota reporting), use the standalone adapter:

import AetherIndexedDB from '@nishantbhatte/aether-core/indexeddb';

const storage = new AetherIndexedDB({
  dbName: 'my-app',
  room:   'shopping',
});
const aether = new Aether('ws://...', { storage: storage });

Full docs: docs/indexeddb-persistence.md.

Anti-entropy reconciliation (new in v0.5)

When two peers reconnect after a partition, epidemic gossip alone can't recover the ops that flowed while they were split. Aether-Core now ships a Merkle-trie anti-entropy exchange:

# Automatic on peer reconnect, or manually:
await node.reconcile_with('peer_id')

Bandwidth is O(k log N) in the number of divergent keys, not O(N) total keys. A 1000-key mesh with 3 divergent keys after a partition uses fewer than 50 probe messages to reconcile, not 1000.

LAN peer discovery (new in v0.5)

Two Aether relays on the same wifi find each other automatically:

node = MeshNode('my-node', host='0.0.0.0', port=8211,
                discovery=True)          # advertise + browse via mDNS
await node.start()
# Any other relay with discovery=True on the LAN auto-federates.

Install the optional dependency: pip install aether-zta[discovery] (or pip install zeroconf). For internet-wide federation across NATs, put the relay on a publicly-reachable host or reverse-tunnel it (Cloudflare Tunnel, ngrok, tailscale). Aether is client-server, not peer-to-peer, so WebRTC/STUN doesn't apply here.

Positioning

Aether-Core is not a "CRDT sync engine." Yjs is a CRDT sync engine. Automerge is a CRDT sync engine. There are dozens of CRDT sync engines and they are all mature.

Aether-Core is a backend-deletion tool. The CRDT is a means, not the pitch. What we sell:

  1. No API to write. The relay treats state as bytes; the app defines state in the browser.
  2. Auth + rate limiting + payload caps by default. Not "bolt on your own middleware." On by default.
  3. Your server can't read your data (optional E2E encryption).
  4. Offline-first. Not a plugin. Built in.
  5. Fifteen-second demo. The shopping list works before you've read the README.

Docs

Tests

uv run pytest
# 73 passed

The suite exercises:

  • CRDT algebra (LWW convergence, HLC ordering, tombstones)
  • Wire protocol conformance (17 tests)
  • Bounded-cache invariants (12 tests)
  • Auth + rate-limit + payload-cap enforcement (16 tests)
  • Anti-entropy reconciliation (14 tests, new in v0.5)
  • LAN mDNS discovery (14 tests, new in v0.5)

CI runs on every PR: .github/workflows/ci.yml.

Contributing

Please see CONTRIBUTING.md. New contributors are warmly welcomed — the codebase is small (~4k lines of Python), the tests are fast, and there's a clear roadmap of good-first-issues.

License

MIT. See LICENSE.


Appendix: wire protocol

Kept here as the canonical reference (mirrored in gateway.py). Any change here must land in both places or the conformance test fails.

Browser -> gateway:
    {"type": "set",      "key": "<str>", "value": <json>}
    {"type": "delete",   "key": "<str>"}
    {"type": "presence", "x": <int>, "y": <int>}        # ephemeral cursor

Gateway -> browser:
    {"type": "hello",          "id": "<uuid>", "color": "<hsl>"}    # on connect
    {"type": "snapshot",       "data": {"<key>": <json>, ...}}      # on connect
    {"type": "set",            "key": "<str>", "value": <json>}
    {"type": "delete",         "key": "<str>"}
    {"type": "presence",       "id": "<uuid>", "color": "<hsl>",
                               "x": <int>, "y": <int>}
    {"type": "presence-leave", "id": "<uuid>"}

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

aether_zta-0.5.0.tar.gz (176.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

aether_zta-0.5.0-py3-none-any.whl (63.9 kB view details)

Uploaded Python 3

File details

Details for the file aether_zta-0.5.0.tar.gz.

File metadata

  • Download URL: aether_zta-0.5.0.tar.gz
  • Upload date:
  • Size: 176.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.8

File hashes

Hashes for aether_zta-0.5.0.tar.gz
Algorithm Hash digest
SHA256 0cf78a1aa172a810ec56a77f72842f0ee22f1928f71468d92142a7ae154d33b6
MD5 e94ba949376220a910b74815ed2e964b
BLAKE2b-256 89b40cc3aacf94235a811b7ae1fb2c7ae62c094babcc34bddb0cda9c9a5e0921

See more details on using hashes here.

File details

Details for the file aether_zta-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: aether_zta-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 63.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.8

File hashes

Hashes for aether_zta-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5853d51fe0031501f72a0387ad4f18d7e0f1286c95a9e4b4d5ad645f13fab681
MD5 6798a749169232377142b12d253180df
BLAKE2b-256 1e1911824674e74417ae2245f613f32f86403942d04a9dfab8880361877ae195

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.5.0 This release

2 files

0.4.0

1 file

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page