Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# ── ANKI SYNC SERVER ─────────────────────────────────────────────────────
RUST_LOG=debug

# Required: Anki sync credentials (username:password)
SYNC_USER1=username:password
Expand Down
22 changes: 19 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,26 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max

build-sync-image:
name: Build sync Docker image
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Build image (no push)
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile.sync
push: false
tags: sync-platform-cloud:ci
cache-from: type=gha
cache-to: type=gha,mode=max

docker-compose-smoke:
name: Docker Compose smoke test
runs-on: ubuntu-latest
needs: build-sync-image
env:
SIDECAR_TOKEN: ci-dummy-token
BETTER_AUTH_SECRET: ci-dummy-secret-32-chars-minimum!!
Expand All @@ -62,7 +79,6 @@ jobs:
TOKEN_ENCRYPTION_KEY: "0000000000000000000000000000000000000000000000000000000000000000"
steps:
- uses: actions/checkout@v4
- name: Pull sync server image
run: docker compose pull anki-sync-server
- name: Build API image and create containers
- uses: docker/setup-buildx-action@v3
- name: Build all images and create containers
run: docker compose up --no-start --build
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "1.0.0"
".": "2.0.0"
}
1 change: 1 addition & 0 deletions .sync-server-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v25.09-r8
57 changes: 42 additions & 15 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,18 +179,18 @@ All of that lives in the user's Google Drive.

### 4.3 Tech Stack

| Layer | Technology | ADR |
|---------------------|--------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Sync server | Rust (fork of ankitects sync server) | [ADR-0003](docs/decisions/0003-fork-rust-ankitects-sync-server.md) |
| REST API + Auth | TypeScript / Hono on Bun | [ADR-0008](docs/decisions/0008-use-hono-on-bun-for-rest-api-and-mcp-server.md) |
| MCP Server | TypeScript / Hono on Bun | [ADR-0007](docs/decisions/0007-mcp-server-wraps-rest-api-not-direct-db.md) · [ADR-0008](docs/decisions/0008-use-hono-on-bun-for-rest-api-and-mcp-server.md) |
| Persistent DB | SQLite (via Drizzle ORM) | [ADR-0009](docs/decisions/0009-use-sqlite-for-persistent-storage.md) |
| Cache / Sessions | Redis | — |
| Storage backends | Google Drive API / Dropbox API / S3 SDK | [ADR-0002](docs/decisions/0002-use-user-owned-cloud-storage-for-deck-data.md) · [ADR-0006](docs/decisions/0006-use-google-drive-as-the-primary-storage-backend.md) |
| Containerization | Docker + Docker Compose | — |
| CI/CD | GitHub Actions | — |
| Docs: API reference | Scalar (from OpenAPI spec) | — |
| Docs: Narrative | Docusaurus | — |
| Layer | Technology | ADR |
|---------------------|-----------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Sync server | Rust (fork of ankitects sync server) | [ADR-0003](docs/decisions/0003-fork-rust-ankitects-sync-server.md) |
| REST API + Auth | TypeScript / Hono on Bun | [ADR-0008](docs/decisions/0008-use-hono-on-bun-for-rest-api-and-mcp-server.md) |
| MCP Server | TypeScript / Hono on Bun | [ADR-0007](docs/decisions/0007-mcp-server-wraps-rest-api-not-direct-db.md) · [ADR-0008](docs/decisions/0008-use-hono-on-bun-for-rest-api-and-mcp-server.md) |
| Persistent DB | SQLite (via Drizzle ORM) | [ADR-0009](docs/decisions/0009-use-sqlite-for-persistent-storage.md) |
| Cache / Sessions | Redis | — |
| Storage backends | Google Drive API / Dropbox API / S3 SDK | [ADR-0002](docs/decisions/0002-use-user-owned-cloud-storage-for-deck-data.md) · [ADR-0006](docs/decisions/0006-use-google-drive-as-the-primary-storage-backend.md) |
| Containerization | Docker + Docker Compose | — |
| CI/CD | GitHub Actions | — |
| Docs: API reference | Scalar (from OpenAPI spec) | — |
| Docs: Narrative | Docusaurus | — |

### 4.4 OpenAPI as Single Source of Truth

Expand All @@ -213,9 +213,35 @@ polished auto-generated SDK clients.

### 4.5 Repository Structure

The sync server lives in a separate repository:
The sync server traits and backends live in a separate repository:
**[github.com/danielpmichalski/anki-cloud-sync](https://github.com/danielpmichalski/anki-cloud-sync)**
— consumed here as a Docker image (`ghcr.io/danielpmichalski/anki-cloud-sync:<version>`).
— consumed here as Cargo git dependencies, built into a local Docker image via `Dockerfile.sync`.

### 4.6 sync-platform-cloud (Rust sync binary)

`sync-platform-cloud/` is a Rust binary crate at the repo root. Built by `Dockerfile.sync`, it
replaces the pre-built `ghcr.io/danielpmichalski/anki-cloud-sync` image with a locally compiled
binary that wires the Anki sync protocol into the cloud database.

**Implements:**

- `CloudAuthProvider` — bcrypt-verifies against `user_sync_config.sync_password_hash`; upserts
hkey into `user_sync_state.sync_key`
- `CloudBackendResolver` — reads `user_storage_connection`, AES-256-GCM decrypts the refresh
token, exchanges it for a Google access token, returns a `GoogleDriveBackend`

**Required env vars** (beyond `SYNC_BASE` / `SYNC_INTERNAL_*`):

- `DATABASE_URL` — shared SQLite path, e.g. `file:/data/anki-cloud.db`
- `TOKEN_ENCRYPTION_KEY` — 32-byte AES-256 key (64 hex chars or 44 base64 chars)
- `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` — for OAuth2 refresh token exchange

**Pinned anki-cloud-sync tag:** see `.sync-server-version`. To upgrade:

1. Update `.sync-server-version`
2. Update the `tag =` value in `sync-platform-cloud/Cargo.toml` (all 4 git deps)
3. Run `docker build -f Dockerfile.sync .` locally and verify it succeeds
4. Update this file

```
/
Expand Down Expand Up @@ -410,7 +436,8 @@ Storage backend credentials are per-user (their own Google Drive etc.).
1. **We never store deck data.** User data lives in user-controlled storage. Always.
2. **We never store passwords for OAuth-authenticated users.** OAuth tokens only. Always scoped, always revocable. Exception: Anki sync uses a dedicated per-user sync password (bcrypt hash only, plaintext never
persisted) because the Anki sync protocol does not support OAuth.
3. **Dual-license strategy.** `anki-cloud-sync` (Rust sync server fork) is AGPLv3 — required by upstream Ankitects license. `anki-cloud` (REST API, MCP server, web UI) is **Elastic License 2.0 (ELv2)**: source-available, self-hosting permitted, offering it as a managed service requires a commercial license.
3. **Dual-license strategy.** `anki-cloud-sync` (Rust sync server fork) is AGPLv3 — required by upstream Ankitects license. `anki-cloud` (REST API, MCP server, web UI) is **Elastic License 2.0 (ELv2)**: source-available,
self-hosting permitted, offering it as a managed service requires a commercial license.
4. **Self-hostable.** Everything runs with `docker compose up`. No hidden dependencies.
5. **OpenAPI first.** The spec is the contract. SDKs and docs generate from it.
6. **Conventional commits.** Enables automated changelog and semantic versioning.
Expand Down
26 changes: 26 additions & 0 deletions Dockerfile.sync
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
FROM rust:1.95-slim-bookworm AS builder

RUN apt-get update && apt-get install -y --no-install-recommends \
protobuf-compiler \
pkg-config \
git \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /build
COPY sync-platform-cloud/ .

# cargo fetches anki-cloud-sync git deps at build time
RUN cargo build --release

FROM debian:bookworm-slim

RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*

COPY --from=builder /build/target/release/anki-sync-server /usr/local/bin/anki-sync-server

EXPOSE 8080 8081

CMD ["anki-sync-server"]
106 changes: 59 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,61 +12,51 @@ user data.

## Running the stack

The sync server ([anki-cloud-sync](https://github.com/danielpmichalski/anki-cloud-sync)) is a
separate Rust service consumed here as a Docker image or local build. Two run modes are supported:

Two independent axes: **mode** (standalone vs cloud) and **image source** (published vs local build).

### Standalone mode

No database or cloud storage required. Users defined via `SYNC_USER1` in `.env`.
Good for local development and testing the REST API without Google Drive setup.

```bash
cp .env.example .env # set SIDECAR_TOKEN, BETTER_AUTH_SECRET, SYNC_USER1 at minimum

# published image (fast)
docker compose -f docker-compose.yml -f docker-compose.standalone.yml up

# local build of anki-cloud-sync (when hacking on the sync server)
docker compose --build -f docker-compose.yml -f docker-compose.standalone.yml -f docker-compose.dev.yml up
```

### Cloud mode

Full production-like stack. Users authenticate via Google or GitHub OAuth (via Better Auth); deck data
stored in their Google Drive. Requires OAuth credentials in `.env`.
The sync server is compiled locally from `sync-platform-cloud/` via `Dockerfile.sync`.
It authenticates Anki clients against the shared SQLite database and resolves each user's
Google Drive backend. Google OAuth credentials are required.

Before running, register these callback URIs in your OAuth apps:

**Google** ([Cloud Console](https://console.cloud.google.com) → APIs & Services → Credentials):

```
{BETTER_AUTH_URL}/v1/auth/callback/google # sign-in callback
{BETTER_AUTH_URL}/v1/auth/callback/google # sign-in callback
{FRONTEND_URL}/v1/me/storage/connect/google/callback # Google Drive callback
```

**GitHub** (optional — [Settings → Developer settings → OAuth Apps](https://github.com/settings/developers)):

```
{BETTER_AUTH_URL}/v1/auth/callback/github # sign-in callback
{BETTER_AUTH_URL}/v1/auth/callback/github # sign-in callback
```

Set `TRUSTED_ORIGINS` in `.env` to your frontend URL(s) (comma-separated) so Better Auth accepts
requests from the web UI. Defaults to `FRONTEND_URL` if unset.

```bash
cp .env.example .env # fill in all credentials
docker compose up --build
```

> **First build:** `docker compose up --build` compiles the Rust sync server from source
> (~10–20 min on first run). Subsequent starts are instant once the image is cached.

# published image
docker compose -f docker-compose.yml -f docker-compose.cloud.yml up
If you're actively developing `sync-platform-cloud/` or `anki-cloud-sync`, rebuild only the
sync image:

# local build of anki-cloud-sync
docker compose --build -f docker-compose.yml -f docker-compose.cloud.yml -f docker-compose.dev.yml up
```bash
docker compose build anki-sync-server
docker compose up
```

`docker-compose.dev.yml` switches `anki-sync-server` from the published image to a local build
of `../anki-cloud-sync`. First build takes ~2–3 min; subsequent starts are instant.
To develop against a local checkout of `anki-cloud-sync` (instead of fetching git deps):

```bash
docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build
```

`docker-compose.dev.yml` overrides `anki-sync-server` to build directly from `../anki-cloud-sync`.

---

Expand Down Expand Up @@ -107,7 +97,10 @@ LLM: "<calls create_notes_bulk> → cards appear in Anki instantly"
"mcpServers": {
"anki-cloud": {
"command": "bun",
"args": ["run", "/path/to/anki-cloud/mcp/src/index.ts"],
"args": [
"run",
"/path/to/anki-cloud/mcp/src/index.ts"
],
"env": {
"API_URL": "http://localhost:3000",
"API_KEY": "ak_your_key_here"
Expand All @@ -131,7 +124,8 @@ In Anki: **Preferences → Syncing → Self-hosted sync server**, set the URL to
http://localhost:8080
```

In standalone mode, use the `SYNC_USER1` credentials (email:password) when Anki prompts for login.
In the web UI, go to **Account → Sync Password** to generate your Anki sync credentials
(one-time setup). Use your account email and the generated password when Anki prompts for login.

---

Expand Down Expand Up @@ -183,30 +177,48 @@ cd e2e && bun test
## Project structure

```
api/ REST API — TypeScript / Hono on Bun (shared workspace with db/ and mcp/)
db/ Drizzle ORM + SQLite schema (@anki-cloud/db)
mcp/ MCP server — wraps REST API, 8 tools for LLM integration
web/ Account management UI (Vite + React) (standalone — cd web && bun install)
e2e/ End-to-end tests (standalone — cd e2e && bun install)
docs/ Architecture decisions (ADRs) + narrative docs
scripts/ Dev tooling (setup, SDK generation)
api/ REST API — TypeScript / Hono on Bun (shared workspace with db/ and mcp/)
db/ Drizzle ORM + SQLite schema (@anki-cloud/db)
mcp/ MCP server — wraps REST API, 8 tools for LLM integration
web/ Account management UI (Vite + React) (standalone — cd web && bun install)
e2e/ End-to-end tests (standalone — cd e2e && bun install)
sync-platform-cloud/ Rust crate — CloudAuthProvider + CloudBackendResolver (sync server binary)
docs/ Architecture decisions (ADRs) + narrative docs
scripts/ Dev tooling (setup, SDK generation)

docker-compose.yml Base stack (api + anki-sync-server)
docker-compose.standalone.yml Standalone mode override (no DB/Google Drive)
docker-compose.cloud.yml Cloud mode override (SQLite + Google Drive OAuth)
docker-compose.dev.yml Local build of anki-cloud-sync (any mode)
docker-compose.yml Full stack (builds sync server from Dockerfile.sync)
docker-compose.dev.yml Override: build sync server from ../anki-cloud-sync local checkout
Dockerfile.sync Multi-stage build for the Rust sync server binary
.sync-server-version Pinned anki-cloud-sync git tag used by Dockerfile.sync
```

The sync server lives in a separate repository:
The sync server traits and backends live in a separate repository:
[github.com/danielpmichalski/anki-cloud-sync](https://github.com/danielpmichalski/anki-cloud-sync) —
see its README for all configuration options and environment variables.
`sync-platform-cloud/` implements the `AuthProvider` and `BackendResolver` traits against
the shared SQLite database.

Full self-hosting walkthrough (Google OAuth setup, Google Drive, Anki Desktop, Claude Desktop): [docs/SELF_HOSTING.md](docs/SELF_HOSTING.md)

Full architecture and design decisions: [CLAUDE.md](CLAUDE.md)

---

## Releasing

Releases are triggered by pushing a version tag. The workflow builds the API Docker image,
pushes it to GHCR, and creates a GitHub Release with auto-generated notes.

```bash
git checkout main
git tag v2.0.0
git push origin v2.0.0
```

The Rust sync server (`Dockerfile.sync`) is not published — self-hosters build it locally
via `docker compose up --build`.

---

## License

Elastic License 2.0 (ELv2) — source-available, self-hosting permitted. See [LICENSE](LICENSE).
Loading
Loading