Odyssey markdown writing, your storage

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.

On this page
  1. Choosing a storage option
  2. Self-hosted Sync API (recommended for self-hosters)
  3. Direct PostgreSQL
  4. Direct MySQL / MariaDB
  5. Amazon DynamoDB
  6. iCloud
  7. On this device only
  8. Switching later & data model

1. Choosing a storage option

OptionBest forYou need
On This DeviceTotal privacy, zero setupNothing
iCloudEffortless sync between your Apple devicesAn iCloud account
Sync APISelf-hosters who want a proper server: bearer-token auth, one open port, works from anywhereA Linux box + PostgreSQL
PostgreSQLPointing the app straight at a database you already runA reachable Postgres server
MySQL / MariaDBSame, for MySQL-family databasesA reachable MySQL/MariaDB server
DynamoDBServerless cloud storage without running anythingAn AWS account
Every option gets the same features: cross-device sync, offline-first editing, and per-sheet version history with automatic coalescing of rapid saves. The direct-database options create their own tables on first connect — you just supply an empty database and credentials.
Reachability: your phone talks to the database directly. A database on your home LAN works at home; to sync from anywhere, expose it via VPN (WireGuard/Tailscale), or use the Sync API behind a normal HTTPS reverse proxy instead — that's the main reason it exists.

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

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;
Bind-address: many default installs listen only on 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

  1. IAM → Users → Create user (e.g. odyssey-app), programmatic access only.
  2. 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"
  }]
}
  1. 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.
Once the table exists you can remove 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