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.
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:
- No API to write. The relay treats state as bytes; the app defines state in the browser.
- Auth + rate limiting + payload caps by default. Not "bolt on your own middleware." On by default.
- Your server can't read your data (optional E2E encryption).
- Offline-first. Not a plugin. Built in.
- Fifteen-second demo. The shopping list works before you've read the README.
Docs
- Getting started
- Core concepts
- JavaScript API
- Python API
- E2E encryption (new)
- IndexedDB persistence (new)
- Recipes
- Deployment
- Troubleshooting
- Security model
- Benchmarks
- Contributing (new)
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0cf78a1aa172a810ec56a77f72842f0ee22f1928f71468d92142a7ae154d33b6
|
|
| MD5 |
e94ba949376220a910b74815ed2e964b
|
|
| BLAKE2b-256 |
89b40cc3aacf94235a811b7ae1fb2c7ae62c094babcc34bddb0cda9c9a5e0921
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5853d51fe0031501f72a0387ad4f18d7e0f1286c95a9e4b4d5ad645f13fab681
|
|
| MD5 |
6798a749169232377142b12d253180df
|
|
| BLAKE2b-256 |
1e1911824674e74417ae2245f613f32f86403942d04a9dfab8880361877ae195
|