Skip to main content

Local-first SQLite sync for Python apps.

Project description

tide 🌊

Local-first SQLite sync for Python apps.

PyPI version Python versions License: Apache 2.0 CI

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())

No server-side schema, no migrations to manage, no plumbing.

Install

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

Python 3.10+.

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.

Features

  • SQLite-backed local store with WAL mode and a durable outbox of pending writes.
  • Last-writer-wins sync over a single HTTP endpoint, ordered by Lamport stamp — two offline clients converge deterministically.
  • Reactive subscriptionsasync for change in collection.watch() — fire on both local and remote writes.
  • Offline-first: every write hits the local DB immediately; sync runs in the background with automatic retry.
  • Pluggable transport for WebSocket, gRPC, or fake transports for tests.
  • Reference server (tide.server, [server] extra) — a Starlette + SQLite implementation under 200 lines you can deploy or port to Postgres.
  • Typed, tested, async-first: py.typed, ships with pyright-clean source, 36+ tests including crash/retry/replay coverage.

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})

        await tide.sync_once()          # one-shot push/pull
        async with tide.sync_forever(interval=1.0):  # background loop
            await asyncio.sleep(10)

asyncio.run(main())

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

Auth

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

async def auth_headers():
    return {"Authorization": f"Bearer {await get_token()}"}

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

How it works

Every local write goes into two places atomically: the canonical document row and a durable outbox. Each write carries a Lamport stamp(counter, node_id) — that totally orders writes across nodes without relying on wall-clock time. The server accepts a write only if its stamp beats the stamp it already has for that (collection, id). Plain last-writer-wins, with deterministic, network-free tie-breaking.

A sync exchange is a single POST:

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

See PROTOCOL.md for the full wire spec, idempotency guarantees, and failure semantics.

Comparison

tide automerge-py pycrdt¹ sqlite-sync
Turnkey sync engine ❌ (low-level primitives) ✅ (Rust extension)
Pure Python client ❌ (Rust extension)
Conflict model LWW + Lamport CRDT CRDT CRDT (SQLite rows)
Reference server ✅ Starlette
Pluggable transport

¹ Successor to y-py, which is no longer actively maintained.

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.

Non-goals (for v0.x)

  • 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.

Status

Alpha. The Python API and wire protocol may change in backward-incompatible ways until 1.0. The store schema is versioned and migrated forward.

License

Apache 2.0.

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.1.tar.gz (24.2 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.1-py3-none-any.whl (22.5 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: tide-0.2.1.tar.gz
  • Upload date:
  • Size: 24.2 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.1.tar.gz
Algorithm Hash digest
SHA256 b7843f680a06abe3c7e17897cd1ffc826b260afe4e5a9ca71db3a09f6c12fabb
MD5 8a32f572a7a945b07e10499aefe38614
BLAKE2b-256 f2d183eca4d51f4a227f60414109edcfacd11ddfe85553eb5137ca626a288694

See more details on using hashes here.

Provenance

The following attestation bundles were made for tide-0.2.1.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.1-py3-none-any.whl.

File metadata

  • Download URL: tide-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 22.5 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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 9c289bd82c31b9a14af4cc3aa41fc363c5317b8ad2a4277a828cdd8f9e2a7643
MD5 1bcbe7b91b5b5cda140efe9662d1b232
BLAKE2b-256 d217b69db7f25b530265ddc763638b5ae527c3e54191752830187dc9d0b818f8

See more details on using hashes here.

Provenance

The following attestation bundles were made for tide-0.2.1-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