Files
twitter-lite/docs/storage-and-oauth.md
T

5.1 KiB

Shared storage and Mastodon OAuth

Twitter Lite runs as a single personal server behind Tailscale Serve. The backend binds to loopback and accepts only the configured Tailscale login. Browser requests that change state must have the configured Origin. OAuth callbacks also pass the owner check; the browser must be able to reach the tailnet HTTPS address after Mastodon authorization.

Runtime configuration

Variable Value
TWITTER_LITE_ORIGIN Exact Serve HTTPS origin, without a trailing slash
TWITTER_LITE_ALLOWED_LOGIN Owner's Tailscale login
TWITTER_LITE_DB_PATH Absolute path to the SQLite database on local disk
TWITTER_LITE_MASTODON_ORIGINS Comma-separated approved HTTPS instance origins
TWITTER_LITE_CREDENTIAL_KEY_FILE Runtime file containing 32 random bytes encoded as base64

The credential key is required for Mastodon, but not for Twitter-only use. Generate it once, keep it outside Git and the Nix store, and retain it when updating the application. For example, with an existing private directory:

umask 077
nix develop -c node --input-type=module -e 'import {randomBytes} from "node:crypto"; import {writeFileSync} from "node:fs"; writeFileSync("/absolute/private/credential-key", randomBytes(32).toString("base64") + "\n", {flag: "wx", mode: 0o600})'

The command refuses to replace an existing file. Losing the key makes saved SNS credentials unreadable. A separate protected backup of the key is needed alongside database backups.

Database and migrations

Drizzle ORM 0.45.3, Drizzle Kit 0.31.11 and better-sqlite3 13.0.3 are pinned. The driver ships native prebuilds; dependency install scripts remain disabled. Both the actual Nix Node runtime and the built Nix package have been exercised with SQLite operations and the packaged backup command.

drizzle/ contains generated SQL and metadata. pnpm db:generate generates SQL and bundles it into TypeScript for the server. Commit both artifacts with schema changes. The server applies pending migrations under an immediate transaction before serving an authorized application request. It enables WAL, foreign keys and a five-second busy timeout. Deployment does not depend on a particular working directory or a separate migration command.

Deck definitions, ordered columns, connection metadata, OAuth applications and short-lived OAuth attempts live in SQLite. Tokens, client secrets and PKCE verifiers are authenticated encrypted envelopes in separate fields. Their associated data binds each secret to its record and purpose. Public connection responses never select those fields.

Backups and restore

Use the live SQLite backup API instead of copying only the main file while WAL is active. The destination must be an absolute path that does not already exist; backups are mode 0600 and pass a SQLite integrity check.

TWITTER_LITE_DB_PATH=/absolute/workspace.sqlite \
  nix develop -c pnpm db:backup /absolute/backups/workspace-2026-09-24.sqlite

The Nix package exposes the same operation as twitter-lite-backup:

TWITTER_LITE_DB_PATH=/var/lib/twitter-lite/workspace.sqlite \
  twitter-lite-backup /absolute/backups/workspace-2026-09-24.sqlite

Run it as an identity that can read the database and write the backup directory. On NixOS the application uses DynamicUser, StateDirectory=twitter-lite and mode 0700. The runtime key is passed with systemd LoadCredential.

For restore, stop the service first. Preserve the current state directory as a separate recovery copy, then restore the verified backup as workspace.sqlite in a clean state directory with the service's ownership and permissions. Do not leave old -wal or -shm sidecars beside a restored main database. Restore the matching credential key separately, then start the service and verify decks and account access. Do not attempt to restore a newer schema into an older application version.

OAuth and instance support

Initial support targets Mastodon 4.3+ with PKCE S256. The first configured instance, https://fedi.yutakobayashi.com, reported 4.5.3 and S256 on 2026-09-24. Only configured HTTPS origins with public DNS addresses are accepted; server requests pin the resolved address and refuse redirects.

Each instance/callback/scope combination has an OAuth application. Authorization uses read:accounts read:statuses read:lists read:search, PKCE, a ten-minute one-use state and an HttpOnly browser-binding cookie. Tokens are exchanged and account identity is verified on the server. The callback URL never contains an access token. Same-instance accounts remain separate; reconnecting preserves the connection ID only for the same account.

An expired token marks its connection unavailable until reconnected. Disconnect first revokes the token using the original OAuth application, then removes the local credential while retaining the connection reference used by saved decks.

Search results depend on the instance's backend and indexing. A successful empty response does not prove full-text search is enabled: Mastodon 4.5.3 also returns empty status results when its search backend is disabled.