Observed since 20 Oct, 20:56 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
Zero-Transit sync engine — your frontend variables are the database
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.
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.
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.
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: 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.
pip install aether-zta # Python relay + CLI
npm install @nishantbhatte/aether-core # Browser client + React hooks
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:
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.
web/aether.js picks the best persistence backend available
automatically:
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.
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.
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.
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:
uv run pytest
# 73 passed
The suite exercises:
CI runs on every PR: .github/workflows/ci.yml.
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.
MIT. See LICENSE.
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 counts and popularity will appear here as the collector receives them.
| Version | Published | Direct dependencies | Open version |
|---|---|---|---|
| 0.5.0 | 4 Jul 2026 | 5 |
This describes our collected data; it is not a security assessment.
Shared repository data · GitHub
Public sponsor counts have not been collected. These signals belong to the repository owner.
github · 1 observations shown. Missing days are unknown.
| Date / source period | Value | Observed at |
|---|---|---|
| 2026-10-21 | 0 | 2026-10-21T13:03:26.2277741Z |
Observed since 20 Oct, 20:56 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package:5f913f6186f49e22d70c962a9b32711787db9ff9bb07da3e79d3456c2b3410fd