Cloud sync
Local files are the default. tf does not upload anything until you run tf login and then tf sync push. Skip both commands and the checkout never contacts tweetfactory.sh.
The copy lives on the tweetfactory.sh account you sign into with X. The account page at /account/ shows the synced persona and drafts, and it can export or delete that copy.
Link a checkout
tf logintf login starts a device-code flow. It prints a short code (ABCD-EFGH) and a URL such as https://tweetfactory.sh/account/?code=ABCD-EFGH. Open that URL in a browser where you are signed in with X, approve the code, and return to the terminal. The CLI writes .cloud.json (mode 0600) and stops. No tweets, persona, or drafts are sent by login.
The code expires in about 10 minutes. The saved link lasts about 90 days. Run tf login again when it expires.
Origin, first match wins: --origin, then TF_CLOUD_ORIGIN, then the origin already stored in .cloud.json, then https://tweetfactory.sh.
Push, pull, status, delete
tf sync push
tf sync pull
tf sync pull --replace
tf sync status
tf sync status --json
tf sync delete --yespush uploads the current local snapshot:
tweets.dbrows: authors, tweets (likes, bookmarks, your posts, metrics), article text, Notion tagspersona/PERSONA.mddrafts/*.md
A push replaces the previous remote snapshot. An empty checkout still commits, which clears the remote copy. The previous snapshot stays until the commit succeeds. A push that exceeds the quota is rejected and leaves the last successful snapshot in place.
pull writes remote rows into the local database and writes persona and draft files. A local file newer than the remote copy is kept. pull --replace clears local sync tables first and deletes local persona or draft files the remote snapshot does not contain.
status prints bytes stored, row counts, and the quota. With no .cloud.json it prints that the checkout is not linked. delete --yes removes the remote copy and the device token. It does not delete local files and it does not delete the X sign-in on tweetfactory.sh.
Verify. After
tf login,test -f .cloud.jsonandstat -c '%a' .cloud.jsonis600.tf sync statusprints a quota line only after a push.git check-ignore -v .cloud.jsonmatches an ignore rule.
What is encrypted
Encryption is server-side AES-GCM with SYNC_ENCRYPTION_KEY. It is not end-to-end: the operator who holds that key can decrypt. Losing the key makes stored ciphertext unreadable. The key is separate from SESSION_SECRET.
Encrypted before write: tweet text, author descriptions, article extracts, Notion notes and page text, persona bodies, and draft bodies. Device tokens are stored as SHA-256 hashes. The access token itself is sealed only until the CLI polls it once.
Left queryable: ids, usernames, public metrics, Notion tags, and filenames. The account page lists drafts by filename and shows persona and draft text after the server decrypts them for the signed-in user.
Rows are keyed by the X user id from the device token or the session. A request cannot name another user’s id.
Storage
Structured rows go to Postgres (DATABASE_URL, Neon or PlanetScale). That holds authors, tweets, articles, Notion pages, device codes, token hashes, and a per-user usage row. Postgres fits relational keys, a generation-scoped replace, and a byte/row quota checked inside the write transaction.
Persona and draft files go to Vercel Blob (BLOB_READ_WRITE_TOKEN) as private ciphertext. Those bodies are files, not rows you filter in SQL.
Upstash Redis still works for sign-in sessions when DATABASE_URL is unset. Cloud sync refuses to start without Postgres, a Blob token, and SYNC_ENCRYPTION_KEY. Redis is a poor fit for tens of thousands of tweet rows.
The usage table tf_sync_usage records bytes_stored, rows_synced, last push, and last pull. Nullable quota_bytes and quota_rows columns exist so a later billing change can override the env quota per user. This release does not write those columns.
Default quota: 50 MiB (52428800 bytes) and 50,000 rows. Optional env SYNC_QUOTA_BYTES and SYNC_QUOTA_ROWS raise or lower that ceiling (caps at 5,000,000,000 bytes and 5,000,000 rows). A push over the quota returns HTTP 402 and stores nothing from that attempt.
Account page
While signed in at tweetfactory.sh/account:
- Approve the code from
tf login. - Read the synced persona and drafts.
- Export downloads
tweetfactory-sync.json(the committed snapshot). Very large libraries return an error instead of a truncated file. - Delete synced data removes the cloud copy and device tokens. The X session remains. Local files on your machine stay.
Delete account on the score page removes the session, saved score drafts, and the synced copy when sync is configured. tf sync delete does not do that.
Provisioning for the site owner is in the repository web/README.md. The CLI only needs TF_CLOUD_ORIGIN when the API is not https://tweetfactory.sh.