Skip to main content

Local-first SQLite sync for Python apps.

Project description

tide

Local-first SQLite sync for Python apps.

tide gives a Python app a local SQLite store, a durable outbox of pending writes, and bidirectional sync against an HTTP server — so your data is available offline and converges automatically when the network comes back.

import asyncio
from tide import Tide

async def main():
    async with Tide(
        db_path="app.db",
        endpoint="https://example.com/sync",
    ) as tide:
        notes = tide.collection("notes")

        await notes.put("n1", {"title": "Hello", "body": "World"})
        print(await notes.get("n1"))

        async with tide.sync_forever(interval=2.0):
            async for change in notes.watch():
                print(change.source, change.id, change.doc)

asyncio.run(main())

That's it. No server-side schema, no migrations to manage, no plumbing.


Why tide?

JavaScript has a thick local-first stack — Yjs, Automerge, Replicache, ElectricSQL, PowerSync, RxDB, Triplit. Python's local-first story is mostly low-level CRDT bindings with no integrated engine on top.

If you build with Streamlit, Marimo, Gradio, Textual, FastHTML, NiceGUI, BeeWare/Briefcase, or any AI agent that needs durable local state that reconciles with a cloud store, tide is the missing piece.

What you get in v0.1

  • SQLite-backed local store with a durable outbox of pending writes.
  • Last-writer-wins sync over a single HTTP endpoint, ordered by a Lamport clock so two offline clients can converge deterministically.
  • Reactive subscriptions (async for change in collection.watch()) that fire on both local and remote writes.
  • Offline-first: every write hits the local DB immediately; sync runs in the background.
  • Pluggable transport if you want WebSocket / gRPC / a fake for tests.
  • Reference server (tide.server, optional [server] extra) — a Starlette + SQLite implementation in under 200 lines you can deploy or port to Postgres.

Install

pip install tide
# with the optional reference server:
pip install "tide[server]"

Python 3.10+.

Quickstart

Client

import asyncio
from tide import Tide

async def main():
    async with Tide(db_path="app.db", endpoint="http://localhost:8000/sync") as tide:
        todos = tide.collection("todos")
        await todos.put("t1", {"text": "buy bread", "done": False})

        # One-shot push/pull:
        await tide.sync_once()

        # Or run a background sync loop:
        async with tide.sync_forever(interval=1.0):
            await asyncio.sleep(10)

asyncio.run(main())

Run the reference server

# server.py
import uvicorn
from tide.server import create_app

app = create_app("server.db")

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)
pip install "tide[server]" uvicorn
python server.py

Authentication

headers accepts a plain mapping or a callable (sync or async) — useful for short-lived tokens:

async def auth_headers():
    token = await get_token_from_somewhere()
    return {"Authorization": f"Bearer {token}"}

Tide(db_path="app.db", endpoint="...", headers=auth_headers)

Watching changes

async with Tide(...) as tide:
    notes = tide.collection("notes")
    async for change in notes.watch():
        # change.source is "local" or "remote"
        # change.kind is "put" or "delete"
        # change.doc is the new document (or None for delete)
        print(change)

watch() is cancellation-safe — break out of the loop and the subscription unregisters automatically.

Protocol

tide speaks a tiny JSON-over-HTTP protocol — one endpoint, one deterministic conflict rule. See PROTOCOL.md for the full spec, including idempotency guarantees and failure semantics.

How it works

Every local write goes into two places atomically: the canonical document row and a durable outbox. The outbox is what the sync loop pushes to the server.

Each write carries a Lamport stamp — a (counter, node_id) pair. The counter monotonically advances both on local writes and when observing remote writes, so any two writes have a total order all nodes agree on. The server accepts a write only if its stamp beats the stamp it already has for that (collection, id). This is plain last-writer-wins, but with deterministic, network-free tie-breaking — no clock drift surprises.

A sync exchange is a single POST:

POST /sync
{ "node_id": "...", "cursor": "...", "ops": [...] }
→ { "cursor": "...", "changes": [...] }

cursor is the server's monotonic sequence number; the client persists the largest seq it has seen and replays from there next round.

Non-goals (for v0.1)

  • CRDT merges. v0 is row-level LWW. A separate tide-crdt package will add Automerge / Yjs-backed merge semantics for documents that need it.
  • Schemas and validation. Documents are plain JSON-serializable dicts. Layer Pydantic or msgspec on top.
  • Multi-master writes without a server. v0 needs a sync endpoint; P2P is a future addition.
  • Encryption at rest and end-to-end encryption. Out of scope; bring your own.
  • Browser/WASM clients. Pure Python today.

These are explicitly deferred so v0 stays small and obvious to reason about.

Comparison

tide automerge-py pycrdt / y-py sqlite-sync
Turnkey sync engine ⚠️ low-level ✅ (Rust ext)
Pure Python client ⚠️ Rust ext
Conflict model LWW + Lamport CRDT CRDT CRDT (SQLite rows)
Reference server ✅ Starlette
Pluggable transport ⚠️ n/a

Use automerge-py or pycrdt directly when you need true collaborative editing (concurrent text/JSON merges). Use tide when you want offline-first row sync without writing the plumbing yourself.

Stability

0.1.x is alpha. The Python API and wire protocol may change in backward-incompatible ways until 1.0. The store schema is versioned and will be migrated forward.

License

MIT

Project details


Download files

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

Source Distribution

tide-0.2.0.tar.gz (21.3 kB view details)

Uploaded Source

Built Distribution

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

tide-0.2.0-py3-none-any.whl (20.0 kB view details)

Uploaded Python 3

File details

Details for the file tide-0.2.0.tar.gz.

File metadata

  • Download URL: tide-0.2.0.tar.gz
  • Upload date:
  • Size: 21.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for tide-0.2.0.tar.gz
Algorithm Hash digest
SHA256 832e99394247b59240bc810c1d8d77ebd8829c96ccba83ce3d1a8c2de76e554e
MD5 256e3cc0a4c5a103129dac2b666f6838
BLAKE2b-256 6a394030550455e3f375b961a37f0c2f06b1fa9811d1a8e861de1c3c19b1b7ad

See more details on using hashes here.

Provenance

The following attestation bundles were made for tide-0.2.0.tar.gz:

Publisher: publish.yml on gauthierpiarrette/tide

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file tide-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: tide-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 20.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for tide-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 76fd2f83d831f74c738318c20c5c949f4ecdda8ace80aa58818978ea9037ddeb
MD5 b2c9d2880877536f03ad611f6f28acee
BLAKE2b-256 d24da4602cb420abf4c575499df9e28bac07fa846b5dcd4784307f8aaf2aadb9

See more details on using hashes here.

Provenance

The following attestation bundles were made for tide-0.2.0-py3-none-any.whl:

Publisher: publish.yml on gauthierpiarrette/tide

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

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