5.1 KiB
Shared storage and Mastodon OAuth
Personal Workspace runs as a single personal server behind Tailscale Serve. The backend binds to loopback. The configured Tailscale identity is an outer access check; a separate owner login is required in every deployment mode. 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.