Offline-first sync backend for Galka, a to-do app for iOS and macOS. FastAPI, Postgres, Redis, SSE.
194
Sync backend for Galka, an offline-first to-do app for iOS and macOS.
One image, one process: uvicorn app.main:app on port 8000. FastAPI · SQLAlchemy 2
(async) · PostgreSQL (asyncpg) · Redis pub/sub · Server-Sent Events.
| Tag | What it is |
|---|---|
latest | The newest build that passed the test suite. |
sha-<12> | An immutable tag per commit. Pin this one; roll back by pointing at an older one. |
Every published tag is the exact manifest CI ran pytest against — the tests ship
inside the image, so what you pull is what passed.
Offline-first sync with last-write-wins conflict resolution and delta-by-cursor pulls.
Each record carries a client-generated uuid and a client-set updated_at (the conflict
clock). The server keeps a per-user monotonically increasing seq; a client stores the
last seq it applied and asks for everything newer.
| Endpoint | What it does |
|---|---|
POST /auth/register · /auth/login · /auth/logout | One bearer token per device. |
GET /sync?since=N | Delta pull: everything with seq > N, tombstones included. |
POST /sync | Batch push; LWW on updated_at. Returns the newer server rows as conflicts. |
GET /events | SSE stream emitting {"seq": N, "origin": "<device-id>"} after each push, so a device can ignore its own writes. |
GET /trash · POST /trash/empty | Two-stage deletes: trashed first, tombstoned on purge. |
GET /logs | Append-only activity history. |
GET /health | Liveness probe. |
GET /docs · /openapi.json | Interactive API docs and the schema. |
Plus server-rendered public pages at /, /privacy, /terms, /support, a /signup
form and an HTTP Basic /admin. Those are excluded from the OpenAPI schema.
name: galka
services:
db:
image: postgres:17-alpine
environment:
POSTGRES_DB: galka
POSTGRES_USER: galka
POSTGRES_PASSWORD: change-me
volumes: [pgdata:/var/lib/postgresql/data]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U galka -d galka"]
interval: 10s
retries: 10
redis:
image: redis:8-alpine
command: ["redis-server", "--save", "", "--appendonly", "no"]
# Runs to completion before the API starts. There is no Alembic — the schema
# comes from create_all(), and two workers racing to emit the same CREATE TABLE
# would kill the process. This does it once, then exits.
migrate:
image: skymanrm/galkaapp-api:latest
command: ["python", "scripts/init_db.py"]
restart: "no"
environment:
TODOAPI_DATABASE_URL: postgresql+asyncpg://galka:change-me@db:5432/galka
TODOAPI_REDIS_URL: redis://redis:6379/0
depends_on:
db: {condition: service_healthy}
api:
image: skymanrm/galkaapp-api:latest
restart: unless-stopped
ports: ["8000:8000"]
environment:
TODOAPI_DATABASE_URL: postgresql+asyncpg://galka:change-me@db:5432/galka
TODOAPI_REDIS_URL: redis://redis:6379/0
TODOAPI_ADMIN_PASSWORD: change-me-too
depends_on:
migrate: {condition: service_completed_successfully}
volumes:
pgdata:
docker compose up -d
curl localhost:8000/health
Behind a reverse proxy, run the API with --proxy-headers and keep the read and idle
timeouts unlimited — /events is held open for the life of a client.
All settings take the TODOAPI_ prefix.
| Variable | Default | What it is |
|---|---|---|
TODOAPI_DATABASE_URL | postgresql+asyncpg://postgres:postgres@localhost:5432/todoapi | Async SQLAlchemy URL. Must be the asyncpg driver. |
TODOAPI_REDIS_URL | redis://localhost:6379/0 | Pub/sub fan-out for /events. Nothing is persisted there. |
TODOAPI_SSE_PING_SECONDS | 15 | Keep-alive interval on the SSE stream. |
TODOAPI_ADMIN_USERNAME | admin | HTTP Basic user for /admin. |
TODOAPI_ADMIN_PASSWORD | admin | Change this — /admin is reachable wherever the app is. |
TODOAPI_OWNER_NAME | Andrey Fanyagin | Shown on the public pages. |
TODOAPI_CONTACT_EMAIL | [email protected] | Shown on the public pages. |
TODOAPI_PUBLIC_URL | https://api.getgalka.ru | Canonical URL used in the public pages. |
TODOAPI_APP_STORE_URL | the Galka listing | Set to an empty string to hide the App Store button. |
python:3.12-slim, single architecture linux/amd64.galka (uid 10001); nothing in the container writes to disk.8000. Default command:
uvicorn app.main:app --host 0.0.0.0 --port 8000.docker run --rm skymanrm/galkaapp-api pytest -q runs it
against in-memory SQLite and an in-memory event bus, no services required.https://github.com/GalkaApp/galkaapp-api — the repository is public and carries the full sync protocol documentation. Licensed MIT.
Content type
Image
Digest
sha256:2db631a20…
Size
74 MB
Last updated
1 day ago
docker pull skymanrm/galkaapp-api