Storage & Sync Setup
Odyssey is a markdown-first writing app that never dictates where your work lives. On first launch it asks where to keep your library; this guide covers every option and how to connect each one. Everything always saves to the device first — the storage choice is about where it syncs.
1. Choosing a storage option
| Option | Best for | You need |
|---|---|---|
| On This Device | Total privacy, zero setup | Nothing |
| iCloud | Effortless sync between your Apple devices | An iCloud account |
| Sync API | Self-hosters who want a proper server: bearer-token auth, one open port, works from anywhere | A Linux box + PostgreSQL |
| PostgreSQL | Pointing the app straight at a database you already run | A reachable Postgres server |
| MySQL / MariaDB | Same, for MySQL-family databases | A reachable MySQL/MariaDB server |
| DynamoDB | Serverless cloud storage without running anything | An AWS account |
2. Self-hosted Sync API
The odyssey-api server is a small FastAPI + PostgreSQL service (~300 lines, MIT). It gives you token auth and a single HTTPS endpoint, so your database never faces the network.
Requirements
- Linux host (a container or VM is plenty — it idles at ~50 MB RAM)
- Python 3.11+
- PostgreSQL 13+
- A domain name + reverse proxy for TLS (nginx/Caddy/Traefik)
Install
# 1. Database
sudo -u postgres createuser odyssey -P # pick a strong password
sudo -u postgres createdb odyssey -O odyssey
# 2. App
git clone https://github.com/cwfrazier1/odyssey-api /opt/odyssey-api
cd /opt/odyssey-api
python3 -m venv venv && venv/bin/pip install -r requirements.txt
psql postgresql://odyssey:PASSWORD@127.0.0.1/odyssey -f deploy/schema.sql
# 3. Config
cp .env.example .env
# ODYSSEY_DATABASE_URL=postgresql://odyssey:PASSWORD@127.0.0.1:5432/odyssey
# ODYSSEY_API_TOKEN=$(openssl rand -hex 24)
# ODYSSEY_COALESCE_WINDOW=120
# 4. Run (systemd unit included)
cp deploy/odyssey-api.service /etc/systemd/system/
systemctl enable --now odyssey-api # uvicorn on 127.0.0.1:8000
Reverse proxy (nginx example)
server {
server_name odyssey.example.com;
location / { proxy_pass http://127.0.0.1:8000; }
# then: certbot --nginx -d odyssey.example.com
}
Connect the app
Choose Odyssey Sync API during onboarding (or in
Settings → Storage & Sync) and enter your URL
(https://odyssey.example.com) and the token from .env.
Test & Connect verifies auth before saving. Verify server health any time
at https://odyssey.example.com/health.
3. Direct PostgreSQL
Odyssey speaks the Postgres wire protocol natively — no server component. On first
connect it creates its four tables (groups, sheets,
sheet_versions, goals).
-- on your Postgres server
create user odyssey password 'a-strong-password';
create database odyssey owner odyssey;
That's it — ownership covers table creation. If you'd rather use an existing
database, the user needs CREATE on a schema plus read/write on the tables
it makes.
In the app enter host, port (5432), database, username, password, and pick a TLS mode. TLS notes: "On" does full certificate verification; "On (allow self-signed)" encrypts but skips verification — the usual choice for homelab Postgres with a self-signed cert; "Off" is fine over a VPN or trusted LAN.
4. Direct MySQL / MariaDB
Identical flow to Postgres — Odyssey connects natively and creates its tables on first connect. Works with MySQL 5.7+/8.x and any modern MariaDB.
-- on your MySQL/MariaDB server
CREATE DATABASE odyssey CHARACTER SET utf8mb4;
CREATE USER 'odyssey'@'%' IDENTIFIED BY 'a-strong-password';
GRANT ALL PRIVILEGES ON odyssey.* TO 'odyssey'@'%';
FLUSH PRIVILEGES;
127.0.0.1. To reach the server from your devices, set
bind-address = 0.0.0.0 (or your LAN IP) in the server config and restart —
and firewall port 3306 to your LAN/VPN only.Timestamps are stored in UTC (datetime(6)), so server timezone settings
don't matter.
5. Amazon DynamoDB
Serverless option — no machine to run. Odyssey signs requests itself (SigV4) and creates the table on first connect with on-demand billing; a personal writing library costs pennies a month.
Create a scoped IAM user
- IAM → Users → Create user (e.g.
odyssey-app), programmatic access only. - Attach this inline policy (replace region/account, and table name if you customize it):
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"dynamodb:CreateTable", "dynamodb:DescribeTable",
"dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem",
"dynamodb:Query", "dynamodb:Scan"
],
"Resource": "arn:aws:dynamodb:us-east-1:YOUR_ACCOUNT_ID:table/odyssey"
}]
}
- Create an access key for the user and enter region, table name, access key ID and secret key in the app. The secret is stored in the device Keychain.
CreateTable from
the policy if you like. All items live under a simple pk / sk key layout;
deletes are soft (a deleted_at attribute), so point-in-time recovery plus
version rows give you a full safety net.6. iCloud
Zero configuration: pick iCloud and Odyssey syncs through the private CloudKit database of whatever Apple ID the device is signed into. Data is visible only to that account — not to us, there is no Odyssey server involved. All devices must use the same iCloud account.
7. On this device only
Nothing leaves the device. You still get full version history (kept locally) and can switch to any synced option later — your library is pushed up wholesale when you do. Use iOS backups if you go this route; the library lives in the app's Documents.
8. Switching later & data model
Settings → Storage & Sync re-opens this chooser at any time. When you switch, Odyssey pushes your entire local library (groups, sheets, goals) to the new backend, then reconciles — so moving from Local → Postgres, or API → iCloud, is a one-tap migration. Version history does not migrate between backends; it starts fresh on the new one (the old backend keeps its copy).
How syncing works
- The app owns all UUIDs, so every write is an idempotent upsert — offline creates keep their identity.
- Reconciliation is per-sheet last-writer-wins by timestamp, polled every few seconds while the app is open.
- Every meaningful save records a version; rapid saves from the same device within a 2-minute window coalesce into one, so history stays readable.
- Deletes are soft everywhere (except iCloud, where record absence is tracked client-side) and deleted sheets land in the in-app Trash, restorable indefinitely.