# Introduction

Taskchampion-sync-server is an implementation of the TaskChampion sync protocol server. It supports synchronizing Taskwarrior tasks between multiple systems.

The project provides both pre-built images for common use-cases (see usage) and Rust libraries that can be used to build more sophisticated applications (integration).

It also serves as a reference implementation: where the specification is ambiguous, this implementation's interpretation is favored in resolving the ambiguity. Other implementations of the protocol should interoperate with this implementation.

## Sync Overview

The server identifies each user with a client ID. For example, when syncing Taskwarrior tasks between a desktop computer and a laptop, both systems would use the same client ID to indicate that they share the same user's task data.

Task data is encrypted, and the server does not have access to the encryption secret. The server sees only encrypted data and cannot read or modify tasks in any way.

To perform a sync, a replica first downloads and decrypts any changes that have been sent to the server since its last sync. It then gathers any local changes, encrypts them, and uploads them to the server.

# Usage

This repository is flexible and can be used in a number of ways, to suit your needs.

- If you only need a place to sync your tasks, using cloud storage may be cheaper and easier than running taskchampion-sync-server. See task-sync(5) for details on cloud storage.

- If you have a publicly accessible server, such as a VPS, you can use `docker compose` to run taskchampion-sync-server as pre-built docker images. See Docker Compose.

- If you would like more control, such as to deploy taskchampion-sync-server within an orchestration environment such as Kubernetes, you can deploy the docker images directly. See Docker Images.

- For even more control, or to avoid the overhead of container images, you can build and run the taskchampion-sync-server binary directly. See Binaries.

# Docker Compose

The `docker-compose.yml` file in this repository is sufficient to run taskchampion-sync-server, including setting up TLS certificates using Lets Encrypt, thanks to Caddy. This setup uses the SQLite backend, which is adequate for one or a few clients.

You will need a server with ports 80 and 443 open to the Internet and with a fixed, publicly-resolvable hostname. These ports must be available both to your Taskwarrior clients and to the Lets Encrypt servers.

On that server, download `docker-compose.yml` from the link above (it is pinned to the latest release) into the current directory. Then run

    TASKCHAMPION_SYNC_SERVER_HOSTNAME=taskwarrior.example.com \
    TASKCHAMPION_SYNC_SERVER_CLIENT_ID=your-client-id \
    docker compose up

The `TASKCHAMPION_SYNC_SERVER_CLIENT_ID` limits the server to the given client ID; omit it to allow all client IDs. You may specify multiple client IDs separated by commas.

It can take a few minutes to obtain the certificate; the caddy container will log a message "certificate obtained successfully" when this is complete, or error messages if the process fails. Once this process is complete, configure your `.taskrc`'s to point to the server:

    sync.server.url=https://taskwarrior.example.com
    sync.server.client_id=your-client-id
    sync.encryption_secret=your-encryption-secret

The docker-compose images store data in a docker volume named `taskchampion-sync-server_data`. This volume contains all of the task data, as well as the TLS certificate information. It will persist over restarts, in a typical Docker installation. The docker containers will start automatically when the Docker dameon starts. See the docker-compose documentation for more information.

# Docker Images

Every release of the server generates Docker images. One image is produced for each storage backend:

- `ghcr.io/gothenburgbitfactory/taskchampion-sync-server` (SQLite)
- `ghcr.io/gothenburgbitfactory/taskchampion-sync-server-postgres` (Postgres)

The image tags include `latest` for the latest release, and both minor and patch versions, e.g., `0.5` and `0.5.1`.

## Running the Image

At startup, each image applies some default values and runs the relevant binary directly. Configuration is typically by environment variables, all of which are documented in the `--help` output of the binaries. These include

- `RUST_LOG` - log level, one of `trace`, `debug`, `info`, `warn` and `error`.
- `DATA_DIR` (SQLite only; default `/var/lib/taskchampion-sync-server/data`) - directory for the synced data.
- `CONNECTION` (Postgres only) - Postgres connection information, in the form of a LibPQ-style connection URI.
- `LISTEN` (default `0.0.0.0:8080`) - address and port on which to listen for HTTP requests.
- `CLIENT_ID` - comma-separated list of client IDs that will be allowed, or empty to allow all clients.
- `CREATE_CLIENTS` (default `true`) - if true, automatically create clients on first sync. If this is set to false, it is up to you to initialize clients in the DB.

### Example

    docker run -d \
      --name=taskchampion-sync-server \
      -p 8080:8080 \
      -e RUST_LOG=debug \
      -v /data/taskchampion-sync-server:/var/lib/taskchampion-sync-server/data \
      taskchampion-sync-server

### Image-Specific Setup

The SQLite image is configured with `VOLUME /var/lib/taskchampion-sync-server/data`, persisting the task data in an anonymous Docker volume. It is recommended to put this on a named volume, or persistent storage in an environment like Kubernetes, so that it is not accidentally deleted.

The Postgres image does not automatically create its database schema. See the integration section for more detail. This implementation is tested with Postgres version 17 but should work with any recent version.

Note that the Docker images do not implement TLS. The expectation is that another component, such as a Kubernetes ingress, will terminate the TLS connection and proxy HTTP traffic to the taskchampion-sync-server container.

# Binaries

Taskchampion-sync-server is a single binary that serves HTTP requests on a TCP port. The server does not implement TLS; for public deployments, the recommendation is to use a reverse proxy such as Nginx, haproxy, or Apache httpd.

One binary is provided for each storage backend:

- `taskchampion-sync-server` (SQLite)
- `taskchampion-sync-server-postgres` (Postgres)

### Building the Binary

This is a standard Rust project, and can be built with `cargo build --release`.

By default, only the SQLite binary is built. To also build the Postgres binary, use

    cargo build --release --features postgres

To disable building the SQLite binary and build only the Postgres binary, use

    cargo build --release --no-default-features --features postgres

### Running the Binary

The server is configured with command-line options or environment variables. See the `--help` output for full details.

For the SQLite binary, the `--data-dir` option or `DATA_DIR` environment variable specifies where the server should store its data.

For the Postgres binary, the `--connection` option or `CONNECTION` environment variable specifies the connection information, in the form of a LibPQ-style connection URI. Note that unlike LibPQ, the Rust client only supports `sslmode` values `disable`, `prefer`, and `require`, and will always validate CA hostnames and certificates when using TLS.

The remaining options are common to all binaries.

The `--listen` option specifies the interface and port the server listens on. It must contain an IP-Address or a DNS name and a port number. This option is mandatory, but can be repeated to specify multiple interfaces or ports. This value can be specified in environment variable `LISTEN`, as a comma-separated list of values.

By default, the server will allow all clients and create them in the database on first contact. There are two ways to limit the clients the server will interact with:

- To limit the accepted client IDs, specify them in the environment variable `CLIENT_ID`, as a comma-separated list of UUIDs. Client IDs can be specified with `--allow-client-id`, but this should not be used on shared systems, as command line arguments are visible to all users on the system. This convenient option is suitable for personal and small-scale deployments.

- To disable the automatic creation of clients, use the `--no-create-clients` flag or the `CREATE_CLIENTS=false` environment variable. You are now responsible for creating clients in the database manually, so this option is more suitable for large scale deployments. See Integration for more information on such deployments.

The server only logs errors by default. To add additional logging output, set environment variable `RUST_LOG` to `info` to get a log message for every request, or to `debug` to get more verbose debugging output.

# Integration

Taskchampion-sync-server can be integrated into larger applications, such as web-based hosting services.

- Most deployments can simply use the pre-built Docker images to implement the sync protocol, handling other aspects of the application in separate containers. See Pre-built Images.

- More complex deployments may wish to modify or extend the operation of the server. These can use the Rust crates to build precisely the desired functionality. See Rust Crates.

- If desired, an integration may completely re-implement the sync protocol. See Sync Protocol Implementation.

# Pre-built Images

The pre-built Postgres Docker image described in Docker Images may be adequate for a production deployment. The image is stateless and can be easily scaled horizontally to increase capacity.

## Database Schema

The schema defined in `postgres/schema.sql` must be applied to the database before the container will function.

The schema is stable, and any changes to the schema will be made in a major version with migration instructions provided.

An integration may:

- Add additional tables to the database
- Add additional columns to the `clients` table. If those columns do not have default values, ensure the server is configured with `CREATE_CLIENTS=false` as described below.
- Insert rows into the `clients` table, using default values for all columns except `client_id` and any application-specific columns.
- Delete rows from the `clients` table. Note that this table is configured to automatically delete all data associated with a client when the client's row is deleted.

## Managing Clients

By default, taskchampion-sync-server creates a new, empty client when it receives a connection from an unrecognized client ID. Setting `CREATE_CLIENTS=false` disables this functionality, and is recommended in production deployments to avoid abuse.

In this configuration, it is the responsibility of the integration to create new client rows when desired, using a statement like `INSERT into clients (client_id) values ($1)` with the new client ID as a parameter. Similarly, clients may be deleted, along with all stored task data, using a statement like `DELETE from clients where client_id = $1`.

# Rust Crates

This project publishes several Rust crates on `crates.io`:

- `taskchampion-sync-server-core` implements the core of the protocol
- `taskchampion-sync-server-storage-sqlite` implements an SQLite backend for the core
- `taskchampion-sync-server-storage-postgres` implements a Postgres backend for the core

If you are building an integration with, for example, a custom storage system, it may be helpful to use the `core` crate and provide a custom implementation of its `Storage` trait.

We suggest that any generally useful extensions, such as additional storage backends, be published as open-source packages.

# Sync Protocol Implementation

The sync protocol is an open specification, and can be re-implemented from that specification as desired. This specification is not battle-tested, so refer to taskchampion-sync-server's implementation to resolve any ambiguities, and please create pull requests to resolve the ambiguity in the specification.

We suggest that new implementations be published as open-source packages where possible.