Skip to main content

Sync SDK

Send changes made offline to a central database, retry safely after connection failures, and pull server changes back to the device.

Beta: Core durability and database guarantees are tested. Public APIs may change before version 1.0.

Your application chooses the records, fields, database, authentication, and access rules. Sync SDK supplies the durable device outbox, synchronization loop, gateway, conflict checks, and database adapters.

Try it locally

Run the complete pipeline against temporary SQLite databases:

uv sync --extra sqlite
uv run --extra sqlite python examples/pluggable_tasks.py

The example registers a task table, queues one offline change, synchronizes it through an in-process gateway, and prints the device and server records. It removes its temporary files when finished. See the example source.

Install

Install the transport, server integration, and database adapter your application uses:

# Recommended production gateway
pip install 'offline-sync-sdk[http,fastapi,postgres]'

# Other supported gateway databases
pip install 'offline-sync-sdk[http,fastapi,mysql]'
pip install 'offline-sync-sdk[http,fastapi,mongodb]'

# Local development or a small single-instance gateway
pip install 'offline-sync-sdk[http,fastapi,sqlite]'

# Optional OpenTelemetry support
pip install 'offline-sync-sdk[http,fastapi,postgres,observability]'

Python 3.12 or newer is required. The device outbox uses Python's built-in SQLite support. You provide the gateway database connection and credentials.

Integration overview

Part What you add
Server Register records, build a gateway, and mount its routes.
Device Open the store, create the client, and enqueue changes.
Worker Run continuously or schedule one synchronization cycle.

How it works

flowchart TB
    subgraph device["Device"]
        direction LR
        app["Application"]
        client["SyncClient<br/>enqueue · sync_once · run"]
        local[("SQLiteStore<br/>outbox · synced records<br/>cursor · retry state")]
        local_note["enqueue: outbox only<br/>pull: records + cursor atomically"]

        app -->|"enqueue mutation"| client
        client -->|"durable enqueue<br/>acknowledge · retry state"| local
        local -->|"FIFO pending batch<br/>cursor"| client
        local -->|"read server-derived state"| app
        local --- local_note
    end

    subgraph transport["Transport"]
        direction LR
        http["HTTPTransport<br/>push · pull<br/>retry transient failures"]
        network["Network<br/>offline · timeout · lost response"]
    end

    subgraph host["Host application"]
        direction LR
        access["Authentication<br/>authorization · tenant scope"]
        routes["FastAPI sync routes"]
        gateway["SyncGateway<br/>validation · conflicts · idempotency"]

        access --> routes --> gateway
    end

    subgraph server["Server database adapter: PostgreSQL · MySQL · SQLite · MongoDB"]
        direction LR
        transaction["One adapter transaction<br/>whole batch commits or rolls back"]
        records[("Business records<br/>+ sync versions")]
        results[("Processed mutation results<br/>for idempotent replay")]
        feed[("Ordered change feed<br/>+ sequence clock")]
        capture["Optional SQL change capture<br/>direct writes · triggers · cascades"]
    end

    client <-->|"push batches<br/>pull pages"| http
    http <--> network <--> access
    client -.->|"DirectTransport<br/>(same process)"| gateway

    gateway --> transaction
    transaction -->|"accepted writes"| records
    transaction -->|"applied · conflict · rejected results"| results
    transaction -->|"accepted changes"| feed
    feed -->|"changes after cursor"| gateway
    records -.->|"poll selected fields"| capture
    capture -.->|"append detected changes"| feed

    retry["Retryable failures keep mutations pending<br/>stable IDs make replay safe"]
    http --- retry
  1. The application queues a mutation in the device outbox.
  2. The client sends pending mutations in bounded FIFO batches.
  3. The gateway validates a batch and applies it in one database transaction.
  4. Each mutation result is saved for idempotent replay. Accepted changes also enter the ordered server feed.
  5. The device acknowledges outbox results and pulls feed pages after its cursor.
  6. Each page updates local records and its cursor in one SQLite transaction.
  7. After a failure or restart, synchronization resumes from the saved outbox, retry state, and cursor.

Add Sync SDK to an application

The following path uses PostgreSQL or another SQLAlchemy-supported backend. See the MongoDB guide for document collection mapping.

1. Register records and fields

Use a new SQLAlchemy table or one your application already owns:

from sqlalchemy import Column, Integer, MetaData, String, Table
from sync_sdk.adapters.sqlalchemy_config import SQLAlchemyConfig

metadata = MetaData()
tasks = Table(
    "my_tasks",
    metadata,
    Column("id", Integer, primary_key=True),
    Column("title", String(200), nullable=False),
    Column("private_note", String(200), server_default="private"),
)

config = SQLAlchemyConfig()
task_binding = config.register_table(
    "tasks",
    tasks,
    fields={"title": "title"},
)

tasks is the name sent over sync, while my_tasks is the database table. Only title is synchronized; private_note stays on the server. If fields is omitted, every non-key column is included.

2. Initialize storage and build the gateway

from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine

engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/app")
sessions = async_sessionmaker(engine, expire_on_commit=False)

await config.create_tables(engine)
gateway = config.build(sessions)

create_tables() creates registered tables that are missing and the SDK tracking tables. It does not alter existing tables. Use application migrations for schema changes. If the application already creates its business tables, initialize only the SDK metadata:

from sync_sdk.adapters.sqlalchemy import initialize_sync_metadata

await initialize_sync_metadata(engine)

Run initialization once before starting sync workers.

3. Mount the gateway routes

from fastapi import FastAPI
from sync_sdk.integrations.fastapi import create_sync_router

app = FastAPI()
app.include_router(create_sync_router(lambda: gateway, prefix="/v1/sync"))

The host application must authenticate these routes and enforce authorization and tenant isolation. A gateway exposes the full change feed of its configured store.

4. Queue and synchronize device changes

import httpx
from sync_sdk import SQLiteStore, SyncClient
from sync_sdk.adapters.http import HTTPTransport

with SQLiteStore("device.db", client_id="device-1") as local:
    async with httpx.AsyncClient(
        base_url="https://api.example.com",
        timeout=10,
    ) as http:
        sync = SyncClient(local, transport=HTTPTransport(http))
        sync.enqueue(
            entity="tasks",
            entity_id="1",
            operation="create",
            payload={"title": "Offline edit"},
            base_version=0,
        )
        report = await sync.sync_once()
        print(report, local.results())

Call enqueue() once for every application change. Do not enqueue the same logical change again after a send failure; the pending mutation keeps its stable ID and is retried safely.

sync_once() performs a bounded amount of push and pull work. Use the continuous worker during the application lifetime:

await sync.run(poll_interval=1)

Reopen the same SQLite file with the same client ID after a restart. Use one sync worker per local file, on the thread that opened it. DirectTransport(gateway) can replace HTTP when the client and gateway run in the same process.

Consistency and failure behavior

Behavior Guarantee
Local enqueue The mutation commits to SQLite before enqueue() returns.
Lost response The mutation remains pending and replays with the same ID.
Duplicate ID Stored results prevent duplicate application.
Gateway batch Data and sync metadata commit or roll back together.
Feed order Sequences are strictly increasing and commit-ordered.
Device checkpoint Pulled records, deletes, and the cursor commit together.

base_version is the last server version known by the application. A different server version returns conflict; omitting base_version skips the default version check. Results are applied, conflict, or rejected. The latter two are acknowledged and require the application to create a new mutation after resolving the problem.

Connection failures, HTTP 408/429, and 5xx responses schedule a retry with exponential backoff and jitter. Retry-After can extend the wait. Other errors are raised to the application.

The local record view contains changes pulled from the server. enqueue() does not apply an optimistic update to that view; applications that need immediate local UI changes should maintain their own local state.

Capture writes made directly to SQL

The optional SQL change-capture worker detects committed inserts, updates, and deletes made outside the gateway, including selected trigger effects and cascading deletes:

import asyncio

capture = config.build_change_capture(sessions)
await capture.capture_once()  # establish a baseline; existing rows are not emitted
capture_task = asyncio.create_task(capture.run(interval=1.0))

The worker polls the selected fields of every registered table. Multiple writes to one row between scans appear as one final-state change. Large or high-write tables may need a native transaction-log CDC adapter instead. MongoDB direct writes are not captured by this SQL worker.

Choose a gateway database

Backend Recommended use Requirement
PostgreSQL Production default Row locking and transactions
MySQL Existing MySQL deployments Transactional engine
MongoDB Document applications Replica set or sharded cluster
SQLite Development and small gateways One local file

Every adapter implements the same gateway contract. SQL and MongoDB adapters serialize sequence allocation so a cursor cannot skip a late-committing change. This favors correctness over maximum concurrent write throughput.

Performance evidence

Backend benchmark: PostgreSQL, MySQL, and MongoDB applied all mutations without failures from concurrency 1 through 8; the chart compares concurrency 1 and 8 throughput and concurrency 8 p95 latency

PostgreSQL 16, MySQL 8.4, and MongoDB 8.0 applied all 12,000 measured mutations across concurrency levels 1, 2, 4, and 8 with zero failures. Rising latency shows batches queuing behind each adapter's shared sequence clock.

Run a SQLite smoke benchmark:

uv run --extra sqlite python benchmarks/sync_throughput.py \
  --mutations 1000 --warmup 100 --batch-size 100 --concurrency 1 \
  --fail-on-errors --output benchmark-sqlite.json

The benchmark guide defines the metrics, environment, correctness audit, and commands for every backend.

Observability

Install the observability extra and configure OpenTelemetry once at startup:

from sync_sdk.adapters.opentelemetry import setup_signoz

monitoring = setup_signoz("my-sync-service")
sync = SyncClient(local, transport=transport, telemetry=monitoring.telemetry)
gateway = config.build(sessions, telemetry=monitoring.telemetry)

Set OTEL_EXPORTER_OTLP_ENDPOINT for the collector and call monitoring.shutdown() during application shutdown. Monitoring failures do not stop synchronization, and payloads or record identifiers are not used as metric labels. See the observability guide for metrics, traces, SigNoz setup, and alert suggestions.

Test and develop

Run the normal suite:

uv sync --locked --group dev --all-extras
uv run pytest -q

GitHub Actions runs the suite on Python 3.12, 3.13, and 3.14, builds and installs the package, and verifies live PostgreSQL, MySQL, and MongoDB adapters. The testing report records exact versions, commands, expected skips, measured results, and release checks.

The app/ directory contains a sample FastAPI gateway:

uv run --extra gateway alembic upgrade head
uv run --extra gateway uvicorn app.main:app --reload

Current scope

  • The host application owns authentication, authorization, tenant isolation, database credentials, schema migrations, backups, and retention.
  • Existing server rows are not emitted automatically. The first SQL capture scan records them as its baseline.
  • SQL change capture scans registered tables; native transaction-log CDC is not included.
  • MongoDB is the ready-to-use NoSQL adapter. Its gateway writes require multi-document transaction support.
  • The SDK provides at-least-once delivery attempts with idempotent server processing; permanent validation or authorization errors require application handling.

For composite keys, linked tables, custom schemas, custom adapters, and metadata tables, see the setup and adapter guide.

Contributing and security

See CONTRIBUTING.md for development setup and DCO sign-off. Report vulnerabilities privately through the process in SECURITY.md.

License

Sync SDK is available under the MIT License.

Metadata

Release files for offline-sync-sdk 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for offline-sync-sdk 0.2.0
File Size Uploaded
offline_sync_sdk-0.2.0.tar.gz 71.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for offline-sync-sdk 0.2.0
File Interpreter ABI Platform
offline_sync_sdk-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 107.2 kB

Release files / offline_sync_sdk-0.2.0.tar.gz

Download URL offline_sync_sdk-0.2.0.tar.gz
Size 71.7 kB
Tags Source
SHA-256 checksum
How to use checksums
25df71ff3e3c8d36d9664794a9105fb0edacd77f41ac2e20044738a58be34dc2
BLAKE2b-256 checksum
How to use checksums
41be079a569318c0864264a19e3ff6266b893bf20919fac00f81923f3d0a13be
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.

Transparency log

Release files / offline_sync_sdk-0.2.0-py3-none-any.whl

Download URL offline_sync_sdk-0.2.0-py3-none-any.whl
Size 35.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a0532e8b3049d0138c73caf766fdf2d735c5d9b12fd3de7aa27da65fdc9c9c48
BLAKE2b-256 checksum
How to use checksums
de2ce8fc72f76c511aedea7d964100392607e0af919baba3868bae2f58f2d3f7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page