# Sync Server Deployment

This guide is for people who want to run a Cosimo sync server. If you only want
to use an existing server from Cosimo, read the `Deck Sync` section of the user
guide instead.

The sync server stores and serves deck snapshots. It does not run scheduling
logic and does not merge independently edited decks. Owner uploads can include
review history; public and unlisted downloads are derived as content-only
bundles so another user's review database is not served accidentally.

The sync HTTP API uses the `/api/v1` route prefix. From the release candidate
onward, current `/api/v1` endpoints should remain available on future sync
servers so RC-and-later Cosimo clients can keep using upgraded servers. Future
releases may add endpoints, fields, or headers, but existing endpoint behaviour
should not be removed or incompatibly changed unless security or comparable
safety requirements make that necessary.

This compatibility promise applies to the machine API used by Cosimo clients.
The server storage directory is private implementation detail, not a public
API; future releases may change how bundles are stored internally. The
human-facing sync pages, including the root status page, privacy notice,
registration, device-authorisation, and account-deletion pages, are also not a
machine-readable compatibility surface. Cosimo may open those pages for the
user, but clients should not scrape their text or HTML structure.

Version matching is still prudent during the RC transition, especially
for decks with newly supported sidecar formats such as imported MP3 audio.
Older beta 11-era servers reject bundles containing MP3 sidecars because they
only accepted the earlier Opus/WAV sidecar formats.
The public project sync server at `https://cosimo.isonomia.net/sync.cgi`
currently runs the RC line. Users who want to use that public server should use
Cosimo `0.1.11-rc.1` or a later RC/stable build. Operators of private servers
can choose their own upgrade cadence, but should avoid mixing beta-era servers
with RC-era clients for decks containing MP3 sidecars or browser-mediated sync
registration/login flows.

## Basic Requirements

A deployed sync server should have:

- an HTTPS URL reachable by Cosimo clients
- a private storage directory that is not served as static web content
- regular server-side backups of that storage directory
- enough disk space for deck bundles, especially decks with audio
- a test pass covering login, upload, download, remote deck listing, and
  owner-only deletion before relying on it with real decks

The storage directory contains the SQLite sync database, bearer-token
verifiers, pending OAuth/login records, privacy-acceptance records, deck
metadata, deck UUID owner reservations, and stored bundle files. Treat it as
private application data.
The sync SQLite backend enables `PRAGMA secure_delete = ON` for its ordinary
tables so deleted or replaced sync metadata is overwritten. This does not cover
web-server logs, external backups, or bundle files; account deletion removes
owned bundle directories separately.

## Release Packages

The normal Windows portable Cosimo package includes the sync executables under
`server`: `cosimo-sync-server.exe` for local or reverse-proxy style use, and
`cosimo-sync-cgi.exe` for CGI-compatible Windows hosting.

For Linux amd64 servers, the project release process can also produce
`cosimo-sync-x.y.z-linux-x86_64.tar.gz`. That archive is server-only and
contains `cosimo-sync-cgi`, `cosimo-sync-server`, this deployment guide, and
the matching source archive.

## CGI Deployment

CGI behind an existing HTTPS web server is the preferred deployment model at
present. It is simpler to host, does not need a long-running process, and has
had the most direct deployment testing.

The intended layout is similar to Fossil's CGI mode. Put a small executable
config script where the web server runs CGI programs, but keep the real
`cosimo-sync-cgi` binary and the storage directory outside the web-readable
tree.

In that intended layout, neither the binary nor the stored deck data is served
as static web content. The web server only exposes the small CGI config script,
and that script is executed as CGI rather than offered as a download.

The CGI binary can generate that script interactively:

```bash
cosimo-sync-cgi config sync.cgi
```

The wizard asks for the private storage root, public or domestic service mode,
privacy/controller details, domestic allowlist if relevant, and optional quotas
such as maximum bundle size and owner storage limits. It writes a complete
executable config script in the format shown below. Existing output files are
not overwritten unless `--force` is passed:

```bash
cosimo-sync-cgi config sync.cgi --force
```

Review the generated file before deployment. It is intended to be executed as
CGI, not served as static text. The storage root named by the script must stay
outside the web-readable tree.

A minimal config script looks like this:

```text
#!/home/user/bin/cosimo-sync-cgi
storage-root: /home/user/private/cosimo-sync-storage
service-mode: public
controller-name: Example Sync Operator
controller-contact: privacy@example.invalid
rights-contact: privacy@example.invalid
privacy-notice-version: 2026-06-03
data-categories: Fediverse account identity, bearer-session metadata, deck bundles, review-history bundles, and operational logs
processing-purposes: Cosimo deck synchronisation and account security
lawful-basis: Explicit consent
recipients: Server operator and hosting provider
international-transfers: No international transfers beyond the configured hosting location
retention-policy: Account data is retained until deletion; logs are retained for up to one week unless a security hold applies
complaint-contact: the operator's supervisory authority or privacy contact
```

Mark the script executable and configure the web server so it runs as CGI. The
URL of that script is the sync server URL users enter in `Tools -> Options`.
For example, if the script is reachable as:

```text
https://example.invalid/cgi-bin/cosimo-sync.cgi
```

then Cosimo should use that exact URL. Cosimo appends `/api/v1/...` paths below
the same CGI endpoint.

Opening the CGI URL in a browser should show a small Cosimo Sync Server status
page. This is only a sanity check; Cosimo itself uses the API paths.

Owner routes need this request header to reach the CGI process:

```text
Authorization: Bearer ...
```

In CGI environments this is normally exposed as `HTTP_AUTHORIZATION`. If the
web server strips it, login may succeed but owner listing, upload, download, or
deletion will fail with an authentication error. On Apache-style setups this
may require `CGIPassAuth On` or the host's documented equivalent.

The CGI executable resolves storage in this order:

1. `--storage-root PATH`
2. `storage-root:` in the config script
3. `COSIMO_SYNC_STORAGE_ROOT`
4. a `cosimo-sync-storage` directory beside the CGI executable

Prefer an explicit private storage root outside the web-readable tree. In the
recommended layout above this keeps all sync data out of the document root. The
fallback directory beside the executable is convenient for testing, but may be
unsafe if you copy the real CGI binary into a web-readable CGI directory and
also let the web server serve sibling files as static content.

Concurrent CGI requests are allowed. SQLite serializes sync storage writes, so
two simultaneous uploads or deletes do not update metadata at the same time,
even if they target different decks. Upload bundle bytes are staged before the
SQLite write transaction starts, then the server re-checks ownership, expected
revision, and quota inside a short `BEGIN IMMEDIATE` transaction before
atomically promoting the staged bundle files and updating metadata. This keeps
large audio-deck uploads from holding the SQLite write lock for the whole
transfer.

A second writer waits up to 15 seconds for the current writer before SQLite
reports the database as busy. With the staged-upload path this should normally
be a short wait, but it can still happen during bursts of uploads, deletes, or
maintenance. When the sync server sees SQLite busy/locked contention, it
reports `503 Service Unavailable` with a `Retry-After` hint rather than a
generic server failure.

Configure the front-end web server to enforce request-size and slow-client
limits as well. In CGI mode, Cosimo deliberately avoids rejecting ordinary
configured sync quota limits before consuming stdin, because doing so can make
some clients report a transport-level send failure instead of receiving a
useful HTTP response. The CGI executable still has a hard global body-size
safety cap, and the normal upload route rejects over-limit declared bundle
sizes and quota failures after the request body is available. The web server
controls connection-level timeouts, buffering, and how long a slow client can
occupy a CGI worker before Cosimo starts.

Unlike the standalone reverse-proxy server, the current CGI entry point does
not yet enforce a portable no-progress timeout while reading the request body
from stdin. Deployments that can configure the front-end server should use
request-body timeout or minimum-rate controls. Deployments without that control
should treat slow-client protection as a known limitation of the CGI path.

## Standalone Server Behind A Reverse Proxy

The standalone `cosimo-sync-server` is intended to run behind a reverse proxy
for any real deployment. Do not expose it directly to the public internet at
this stage. The proxy should provide TLS, mature HTTP parsing, request-size
limits, slow-client protection, logging, and any site-level rate limiting. The
Cosimo process should be treated as a private backend service.

This model is supported, but it is more operationally involved and currently
less exercised than CGI. It is most suitable when you control a VPS, dedicated
server, home server, or VPN host and can run a supervised process.

Run the server bound to a private address, for example:

```bash
cosimo-sync-server --bind 127.0.0.1:44791 --storage-root /home/user/private/cosimo-sync-storage
```

Do not omit `--storage-root` for a real deployment. Without a storage root, the
standalone server uses volatile in-memory storage intended only for development
or temporary tests. Uploaded decks, login sessions, and OAuth state are lost
when the process exits.

Run only one standalone server process for a given storage root. The server
uses an OS-backed `cosimo-sync-server.lock` file in that storage root and exits
if another standalone server already holds it. This lock is deliberately not
used by CGI deployments, where concurrent per-request processes are expected
and SQLite transactions serialize storage changes. On Unix, the lock file is
opened relative to a no-follow storage-root directory descriptor, and the
storage root must be a plain directory.

Put an HTTPS reverse proxy in front of it. The reverse proxy should provide the
public URL and TLS; the sync server itself has no built-in TLS and uses a small
HTTP implementation intended for this controlled backend role only.

The standalone server handles one request per TCP connection and expects
`Content-Length` request bodies. It applies a request read timeout, a response
write timeout, and a bounded concurrent-request gate. When too many requests
are already active, it returns `503 Service Unavailable` with a `Retry-After`
hint. A normal reverse proxy should buffer and pass requests in a compatible
form, and should have its own stricter or equivalent request-size and timeout
rules.

For OAuth rate limiting, the standalone server normally uses the TCP peer
address. Behind a same-host reverse proxy this is usually `127.0.0.1`, which
means all users share the same client-address limit. If the proxy supplies a
real client-IP header, start the server with:

```bash
cosimo-sync-server --bind 127.0.0.1:44791 --storage-root /home/user/private/cosimo-sync-storage --trusted-client-ip-header X-Forwarded-For
```

Only use this when the reverse proxy strips any incoming copy of that header
and then supplies its own trusted value. Cosimo only honours the configured
trusted header from loopback peers, so accidentally exposing the backend port
does not let remote clients spoof their rate-limit identity through this option.

## Limits

Both CGI and reverse-proxy deployments support the same limit environment
variables:

```text
COSIMO_SYNC_MAX_BUNDLE_BYTES
COSIMO_SYNC_MAX_BUNDLE_EXPANDED_BYTES
COSIMO_SYNC_MAX_BUNDLE_ENTRIES
COSIMO_SYNC_MAX_DECK_FILE_BYTES
COSIMO_SYNC_MAX_DECKS_PER_OWNER
COSIMO_SYNC_MAX_STORAGE_BYTES_PER_OWNER
COSIMO_SYNC_MAX_STORAGE_BYTES_TOTAL
COSIMO_SYNC_MAX_OAUTH_STARTS_PER_CLIENT_PER_HOUR
COSIMO_SYNC_MAX_OAUTH_STARTS_PER_INSTANCE_PER_HOUR
COSIMO_SYNC_MAX_PENDING_OAUTH_LOGINS_PER_INSTANCE
```

The standalone reverse-proxy server also supports:

```text
COSIMO_SYNC_MAX_CONCURRENT_REQUESTS
```

If this limit is reached, new connections receive `503 Service Unavailable`
with `Retry-After`. The default is deliberately modest; use the reverse proxy
for broader traffic shaping rather than increasing this value blindly.

CGI config scripts can also set the same limits with lower-case hyphenated
keys, such as:

```text
max-bundle-bytes: 52428800
max-bundle-expanded-bytes: 52428800
max-decks-per-owner: 20
max-storage-bytes-per-owner: 104857600
max-storage-bytes-total: 1073741824
controller-name: Example Sync Operator
controller-contact: privacy@example.invalid
rights-contact: privacy@example.invalid
privacy-notice-version: 2026-06-03
data-categories: Fediverse account identity, bearer-session metadata, deck bundles, review-history bundles, and operational logs
processing-purposes: Cosimo deck synchronisation and account security
lawful-basis: Explicit consent
recipients: Server operator and hosting provider
international-transfers: No international transfers beyond the configured hosting location
retention-policy: Account data is retained until deletion; logs are retained for up to one week unless a security hold applies
complaint-contact: the operator's supervisory authority or privacy contact
```

Uploads are checked before they become authoritative. The server verifies
declared hashes, rejects unsupported bundle paths, rejects review databases in
content-only bundles, limits deck file size, and enforces configured storage
and deck-count quotas. Declared request and bundle lengths are rejected early
when they exceed configured limits, but deployments should still set matching
or stricter proxy/web-server body-size limits so obviously oversized uploads
are stopped before they reach Cosimo.

Current operational limits to understand:

- SQLite remains a single-writer database. Uploads and deletes are staged to
  keep write transactions short, but metadata writes still serialize.
- A stale upload from another device is rejected by expected-revision conflict.
  The client must download or otherwise refresh before retrying.
- On startup/open, the SQLite backend reconciles known Cosimo bundle leftovers:
  stale `.upload-*` staged files and unreferenced server-generated revision
  bundle files are removed. Unknown files are preserved, and symlinked bundle
  paths are rejected rather than followed.
- Sync does not yet have a manifest/preflight protocol, so a client may still
  transfer a large bundle before learning that another device already uploaded
  a newer revision.

## OAuth Behaviour

Cosimo uses Mastodon-compatible OAuth as the account proof. Public-service mode
is the default and refuses new account registration until the operator has
configured controller and privacy notice information. Domestic mode is available
for non-public personal deployments; if it is exposed beyond that, the operator
is responsible for the legal consequences. Domestic mode can restrict
registration to a comma-separated allowlist of specific Fediverse account
identities.

`Deck -> Sync -> Register New Account` asks the server to create a pending
browser registration and opens the sync server's registration page. That page
shows the configured privacy notice, asks for the Fediverse instance or full
account, and requires an unchecked acceptance checkbox before redirecting to
the Fediverse authorisation URL. If the user enters a full account, the
callback rejects a successful OAuth return for a different account on the same
instance. After authorisation, the Fediverse instance redirects the browser
back to the sync server callback. The server asks only for the `read:accounts`
scope, verifies the account, revokes the temporary Fediverse token, checks the
privacy/domestic registration policy, records the notice version/hash,
timestamp, verified owner identity, and lawful basis, and then lets Cosimo poll
the pending login token to receive a Cosimo bearer token.

`Deck -> Sync -> Log In` is for authorising another Cosimo installation for an
account that already exists on the sync server. It uses the same server-hosted
browser OAuth callback, but it does not create a new account and does not show
the full registration/privacy-acceptance flow again.

The server registers OAuth applications for deployment-specific callback URLs
as needed during browser login. Cosimo bearer sessions are stored as hashed
verifiers rather than plaintext bearer tokens.

The browser-login OAuth redirect URI is deployment-specific, for example:

```text
https://example.invalid/sync.cgi/auth/fedi/callback
```

Account deletion uses the same browser OAuth mechanism with a separate callback
URL:

```text
https://example.invalid/sync.cgi/auth/fedi/delete/callback
```

Users can reach the deletion form from the sync server root page. Deletion
requires the full Fediverse account and deletes server-side Cosimo sync data
only after OAuth returns that same verified account. It does not require a
Cosimo bearer token, so users can delete their server-side sync account even if
they have lost or invalidated local Cosimo tokens.
Account deletion also retires the account's deck UUID reservations by clearing
the owner identity from them. This prevents later takeover of deleted deck
identities without retaining the deleted Fediverse account identity.

For CGI, Cosimo constructs this from the request host, request scheme, and CGI
script name. If a reverse proxy is involved, configure it so the public host and
scheme reaching the sync server match the URL users enter in Cosimo.

Open the sync server root page in a browser and follow the privacy notice link,
or request `/api/v1/privacy` below the sync server URL for JSON, to inspect the
configured notice and registration status. Public deployments should configure
at least:

- `service-mode: public`
- `controller-name`
- `controller-contact`, using email, a web form, or postal contact rather than
  Fediverse-only contact
- optional `controller-fedi` as an additional contact route
- `rights-contact` or `complaint-contact`
- `privacy-notice-version`
- `data-categories`
- `processing-purposes`
- `lawful-basis`
- `recipients`
- `international-transfers`
- `retention-policy`

For a private personal deployment, use:

```text
service-mode: domestic
domestic-registration-allowlist: acct:you@example.social
```

If the allowlist is omitted in domestic mode, any account that can complete the
Fediverse OAuth proof can register. Do not use that configuration for a server
offered to general users.

## Moving a Sync Server

To move a sync server to another host or domain, stop traffic and copy the
whole storage directory consistently. The SQLite database and bundle files must
match.

After copying:

1. Configure the new CGI script or reverse proxy to use the copied storage
   directory.
2. Open the new server URL in a browser and check that the status page appears.
3. Configure a Cosimo client to use the new sync server URL.
4. Log in again and test sync status, upload, download, remote deck listing,
   and deletion with a test deck.

Moving a sync server changes the browser-login callback URL. Test login after a
move. Browser login may need the server to register a fresh OAuth application
for the new callback URL.

Changing the sync URL in Cosimo clears the local sync login state. Existing
server-side sessions may still be present in the copied database, but clients
should log in again after the move.

## Production Deployment And Upgrade Checklist

Use this checklist before trusting a new deployment with real decks, and again
when upgrading a server that already stores user data.

### Storage And Backup Checks

1. Confirm the storage root is outside any web-readable document tree.
2. Confirm the storage root is writable by the CGI or server process and not
   writable by unrelated local users.
3. Confirm the storage root contains the SQLite database and bundle
   directories, and that both are covered by the same backup process.
4. Before an upgrade, take a fresh backup of the whole storage root while no
   sync request is running, or use a filesystem/database backup method that
   gives a consistent snapshot.
5. Preserve file ownership and permissions when copying or restoring the
   storage root.
6. Confirm server backups are retained according to the operator's privacy and
   retention policy.

### Binary And Configuration Checks

1. Deploy matching release binaries for the intended platform.
2. For CGI, keep the real `cosimo-sync-cgi` binary outside the web-readable
   tree when possible, and expose only the executable config script.
3. For CGI, review the config script after generation or upgrade and confirm
   `storage-root:` points to the intended private storage root.
4. For the standalone server, start it with an explicit `--storage-root` and
   bind it to loopback or another private backend interface.
5. Confirm configured quota values are intentional: bundle size, expanded
   bundle size, deck-file size, decks per owner, per-owner storage, and total
   storage.
6. Confirm the deployed public URL is the URL users enter in Cosimo. Browser
   OAuth callback URLs are derived from that public URL.

### Front-End Web Server Checks

1. Confirm the public endpoint uses HTTPS.
2. Confirm `Authorization: Bearer ...` reaches the CGI process as
   `HTTP_AUTHORIZATION`, or reaches the reverse-proxy backend unchanged.
3. Confirm the front-end server enforces request body size limits compatible
   with the Cosimo quota settings.
4. Confirm the front-end server enforces slow-client request-body protection.
   This is especially important for CGI because the current CGI entry point
   does not yet enforce a portable no-progress timeout while reading stdin.
5. Confirm reverse-proxy deployments strip untrusted incoming client-IP
   headers before setting any header named with
   `--trusted-client-ip-header`.
6. Confirm logs do not record bearer tokens, OAuth secrets, deck contents, or
   review-history contents.

### Privacy And Registration Checks

1. Open the server root page in a browser and confirm it identifies the Cosimo
   Sync Server and links to the health endpoint, privacy notice, and account
   deletion page with meaningful link text.
2. Request `/api/v1/health` below the sync server URL and confirm it returns a
   successful response.
3. Request `/api/v1/privacy` below the sync server URL and confirm the
   configured privacy notice and registration state are correct.
4. For a public server, confirm registration is refused unless controller,
   rights or complaint contact, notice version, data categories, processing
   purposes, lawful basis, recipients, international transfers, and retention
   policy are configured.
5. For a domestic or personal server, confirm the operator understands that it
   should not be offered to general public users, and configure a registration
   allowlist if only named accounts should be able to register.
6. Confirm optional privacy fields that are not configured are omitted from
   browser pages rather than shown as placeholder text.

### Functional Smoke Test

1. Configure a disposable Cosimo installation with the deployed sync server
   URL.
2. Register a new test account through `Deck -> Sync -> Register New Account`.
3. Confirm the server registration page explains the configured notice and
   requires explicit acceptance before continuing.
4. If the registration page asks for a full Fediverse account, confirm OAuth
   rejects a callback for a different account on the same instance.
5. Upload a small test deck with study history.
6. Check `Deck -> Sync -> Sync Status` and confirm the remote owner, revision,
   visibility, storage quota, and last upload device are plausible.
7. Open `Deck -> Sync -> Remote Decks` and inspect the uploaded deck details.
8. Download the deck to a separate local path or test installation.
9. Confirm public or unlisted shared downloads are content-only and do not
   include the owner's review database.
10. Authorise a second Cosimo installation with `Deck -> Sync -> Log In` and
    confirm this does not repeat the full registration/privacy-acceptance flow.
11. Log out locally from one installation and confirm another installation
    using its own token still works.
12. Invalidate the current token or all tokens, then confirm affected
    installations lose owner access.
13. Delete the remote test deck while logged in as its owner.
14. Confirm another account cannot delete a deck it does not own.
15. Delete the test sync account through the server account-deletion flow and
    confirm the browser OAuth proof must match the account being deleted.
16. After account deletion, confirm the Cosimo client reports the missing
    remote account or deck coherently and does not offer impossible download
    actions.

### Upgrade Checks

1. Read the release notes for sync schema, auth-flow, sidecar-media, or API
   compatibility changes.
2. Take a fresh storage-root backup before deploying new binaries.
3. Prevent new sync requests during the binary replacement when possible. For
   CGI, this can be a short maintenance window or a temporary web-server rule
   that rejects the sync endpoint.
4. Replace the binary or packaged server files.
5. Review the CGI config script or service command line for newly required
   configuration.
6. Open the root page, health endpoint, and privacy endpoint before allowing
   normal use again.
7. Run the functional smoke test with a disposable deck.
8. Watch the web-server error log and Cosimo sync logs during the first real
   upload and download after upgrade.

### Standalone Server Restart Check

1. If you use `cosimo-sync-server`, restart it and confirm uploaded decks are
   still listed.
2. If uploaded decks disappear after restart, the server was using volatile
   in-memory storage rather than durable `--storage-root` storage.
3. Confirm a second standalone server process cannot start against the same
   storage root.

Keep the storage directory private and backed up. The server is designed to
avoid serving review databases through public or unlisted downloads, but owner
study-history bundles still contain personal learning data.
