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>"}
Metadata
Release files for aether-zta 0.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| aether_zta-0.5.0.tar.gz | 176.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| aether_zta-0.5.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 239.8 kB
Release files / aether_zta-0.5.0.tar.gz
| Download URL | aether_zta-0.5.0.tar.gz |
|---|---|
| Size | 176.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0cf78a1aa172a810ec56a77f72842f0ee22f1928f71468d92142a7ae154d33b6
|
|
BLAKE2b-256 checksum How to use checksums |
89b40cc3aacf94235a811b7ae1fb2c7ae62c094babcc34bddb0cda9c9a5e0921
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.8
|
Release files / aether_zta-0.5.0-py3-none-any.whl
| Download URL | aether_zta-0.5.0-py3-none-any.whl |
|---|---|
| Size | 63.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5853d51fe0031501f72a0387ad4f18d7e0f1286c95a9e4b4d5ad645f13fab681
|
|
BLAKE2b-256 checksum How to use checksums |
1e1911824674e74417ae2245f613f32f86403942d04a9dfab8880361877ae195
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.8
|