Select the types of activity you want to include in your feed.
A simple tool which lets you scrape twitter accounts and crosspost them to bluesky accounts! Comes with a CLI and a webapp for managing profiles! Works with images/videos/link embeds/threads.
···11# tweets-2-bsky
22-This repo can also be found on Tangled at [j4ck.xyz/tweets2bsky](https://tangled.org/j4ck.xyz/tweets2bsky)
22+33+Cross-post from Twitter/X to Bluesky with thread support, media handling, account mapping, and a web dashboard.
3444-Crosspost posts from Twitter/X to Bluesky with thread support, media handling, account mapping, and a web dashboard.
55+This repo is also mirrored on Tangled: [j4ck.xyz/tweets2bsky](https://tangled.org/j4ck.xyz/tweets2bsky)
66+77+## How It Works (Simple)
88+99+1. You connect one or more Twitter/X source accounts to a Bluesky account.
1010+2. The app reads tweets from X using `@the-convocation/twitter-scraper` with your cookies (`auth_token` + `ct0`).
1111+3. It posts to Bluesky using the official AT Protocol client (`@atproto/api`).
1212+4. It tracks what was already posted in SQLite so it does not repost duplicates.
1313+5. A scheduler runs automatically, and you can also trigger `Run now` from the dashboard or CLI.
1414+1515+## Installation (Pick One Path)
1616+1717+Use either:
1818+1919+- Docker (recommended)
2020+- Source install (PM2 or manual runtime)
52166-## Quick Start (Recommended: Docker)
2222+Do not do both on the same machine unless you intentionally want two separate deployments.
72388-Most people should use Docker. It is the simplest setup and includes full feature parity (backend API + scheduler + frontend dashboard + Chromium).
2424+### Option A: Docker (Recommended)
9251010-Prerequisite: Docker Desktop (Windows/macOS) or Docker Engine (Linux). On Windows, use Docker Desktop in Linux container mode.
2626+Prerequisite: Docker Desktop (macOS/Windows) or Docker Engine (Linux).
11271212-Prefer using the included compose file so the named volume is always attached:
2828+Start with the included compose file:
13291430```bash
1531docker compose up -d
1632```
17331818-### 1) Run the latest image
3434+Open `http://localhost:3000`.
19352020-macOS/Linux (bash):
3636+If you prefer `docker run`:
21372238```bash
2339docker run -d \
···2844 j4ckxyz/tweets-2-bsky:latest
2945```
30463131-Windows (PowerShell):
4747+Important: keep a persistent volume (`-v tweets2bsky_data:/app/data`) so mappings/history survive container recreation.
32483333-```powershell
3434-docker run -d --name tweets-2-bsky -p 3000:3000 -v tweets2bsky_data:/app/data --restart unless-stopped j4ckxyz/tweets-2-bsky:latest
3535-```
3636-3737-Open `http://localhost:3000`.
3838-3939-Important: keep `-v tweets2bsky_data:/app/data` (or an equivalent bind mount). Without a persistent volume, mappings and history are lost when the container is recreated.
4040-4141-If port `3000` is already in use, change only the first port (example: `-p 3001:3000`).
4242-4343-### 2) Complete first-time setup
4444-4545-1. Register the first user (this user becomes admin).
4646-2. Add Twitter cookies in Settings.
4747-3. Add at least one mapping via the guided "Add account" flow (Twitter sources -> Bluesky account -> credential validation -> verify email + create).
4848-4. Click `Run now`.
4949-5050-### 3) Useful Docker commands
4949+Useful Docker commands:
51505251```bash
5352docker logs -f tweets-2-bsky
···5655docker start tweets-2-bsky
5756```
58575959-### 4) Update to newest release
5858+Update Docker deployment:
60596160```bash
6261docker pull j4ckxyz/tweets-2-bsky:latest
···7069 j4ckxyz/tweets-2-bsky:latest
7170```
72717373-Alternative image registry: `ghcr.io/j4ckxyz/tweets-2-bsky:latest`.
7272+Alternative image: `ghcr.io/j4ckxyz/tweets-2-bsky:latest`.
74737575-## Source Install Quick Start (No Docker)
7474+### Option B: Source Install (PM2 or Manual)
76757777-If you prefer running from source and do not want to manage PM2 manually, use the installer script.
7676+Prerequisites:
78777979-### 1) Clone the repo
7878+- `git`
7979+- Bun 1.x+ (the installer auto-installs/upgrades Bun when needed)
8080+- PM2 (optional, but recommended for background runtime)
8181+8282+Clone and install:
80838184```bash
8285git clone https://github.com/j4ckxyz/tweets-2-bsky
8386cd tweets-2-bsky
8484-```
8585-8686-### 2) Run install + background start
8787-8888-```bash
8987chmod +x install.sh
9088./install.sh
9189```
92909393-What this does by default:
9191+`install.sh` does install/build/start and uses:
94929595-- auto-installs Bun (latest stable for your platform) when missing
9696-- auto-upgrades Bun to latest stable before install/build
9797-- installs dependencies
9898-- builds server + web dashboard
9999-- creates/updates `.env` with sensible defaults (`PORT=3000`, generated `JWT_SECRET` if missing)
100100-- starts in the background
101101- - uses PM2 if installed
102102- - otherwise uses `nohup`
103103-- prints your local web URL (for example `http://localhost:3000`)
9393+- PM2 when PM2 is available
9494+- `nohup` when PM2 is not installed
10495105105-### Useful installer commands
9696+Useful installer commands:
1069710798```bash
108108-./install.sh --no-start
9999+./install.sh --status
100100+./install.sh --stop
109101./install.sh --start-only
110110-./install.sh --stop
111111-./install.sh --status
102102+./install.sh --no-start
112103./install.sh --port 3100
113113-./install.sh --host 127.0.0.1
114114-./install.sh --skip-native-rebuild
115104```
116105117117-If you prefer full manual source setup details, skip to [Manual Setup](#manual-setup-technical).
118118-119119-After source install starts, open `http://localhost:3000` and follow the first-time setup steps in [Quick Start](#quick-start-recommended-docker).
120120-121121-## Docker (Single-Container, Backend + Frontend + Scheduler)
122122-123123-This repo now includes a single `Dockerfile` that runs:
124124-125125-- the backend API
126126-- the scheduler/worker loop
127127-- the built frontend dashboard
128128-- Chromium (for quote-tweet screenshot fallback support)
129129-130130-The container aims for feature parity with normal installs while giving one-command startup.
131131-132132-Tip: the repository includes `docker-compose.yml` with a named `tweets2bsky_data` volume, so `docker compose up -d` is the safest default.
133133-134134-### 1) Pull and run (recommended)
135135-136136-After publishing an image (see [Publishing](#publishing-multi-platform-images-linuxamd64--linuxarm64)), run:
137137-138138-```bash
139139-docker run -d \
140140- --name tweets-2-bsky \
141141- -p 3000:3000 \
142142- -v tweets2bsky_data:/app/data \
143143- --restart unless-stopped \
144144- j4ckxyz/tweets-2-bsky:latest
145145-```
146146-147147-Open `http://localhost:3000`.
148148-149149-PowerShell equivalent:
150150-151151-```powershell
152152-docker run -d --name tweets-2-bsky -p 3000:3000 -v tweets2bsky_data:/app/data --restart unless-stopped j4ckxyz/tweets-2-bsky:latest
153153-```
154154-155155-Alternative registry mirror: `ghcr.io/j4ckxyz/tweets-2-bsky:latest`.
156156-157157-### 2) Build locally (if you do not want to pull)
158158-159159-```bash
160160-docker build -t tweets-2-bsky:local .
161161-162162-docker run -d \
163163- --name tweets-2-bsky \
164164- -p 3000:3000 \
165165- -v tweets2bsky_data:/app/data \
166166- --restart unless-stopped \
167167- tweets-2-bsky:local
168168-```
169169-170170-### 3) Environment variables
171171-172172-Pass environment values with `-e` or `--env-file` (same values as normal install):
173173-174174-```bash
175175-docker run -d \
176176- --name tweets-2-bsky \
177177- -p 3000:3000 \
178178- -v tweets2bsky_data:/app/data \
179179- --env-file .env \
180180- j4ckxyz/tweets-2-bsky:latest
181181-```
182182-183183-Common variables:
184184-185185-- `PORT` (default `3000`)
186186-- `JWT_SECRET` (recommended to set explicitly)
187187-- `JWT_EXPIRES_IN`
188188-- `CORS_ALLOWED_ORIGINS`
189189-- `BSKY_APPVIEW_URL` (optional override)
190190-- `SCHEDULED_ACCOUNT_TIMEOUT_MS` (default `480000` / 8 minutes, forces a skip when one source account hangs during scheduled checks)
191191-- `TWEETS2BSKY_DATA_DIR` (default `/app/data` in Docker; keep aligned with your mounted data volume path)
192192-193193-### 4) Persistent data inside Docker
194194-195195-Store all app state in `/app/data` (mounted via volume):
196196-197197-- `/app/data/config.json` (mappings, users, credentials)
198198-- `/app/data/database.sqlite`
199199-- `/app/data/.jwt-secret`
200200-201201-The app reads persistent files from `TWEETS2BSKY_DATA_DIR` (defaults to `/app/data` in Docker), so a single mounted volume preserves everything important.
202202-203203-### 5) CLI usage in container
204204-205205-You can run CLI commands without leaving Docker:
106106+#### PM2 Manual Runtime (if you want direct PM2 control)
206107207108```bash
208208-docker exec -it tweets-2-bsky bun dist/cli.js status
209209-docker exec -it tweets-2-bsky bun dist/cli.js run-now
210210-docker exec -it tweets-2-bsky bun dist/cli.js list
211211-```
212212-213213-### 6) Updating Docker deployments
214214-215215-For Docker installs, update by pulling a newer image and recreating the container with the same volume:
216216-217217-```bash
218218-docker pull j4ckxyz/tweets-2-bsky:latest
219219-docker stop tweets-2-bsky
220220-docker rm tweets-2-bsky
221221-docker run -d \
222222- --name tweets-2-bsky \
223223- -p 3000:3000 \
224224- -v tweets2bsky_data:/app/data \
225225- --restart unless-stopped \
226226- j4ckxyz/tweets-2-bsky:latest
227227-```
228228-229229-### 7) Debug logs (especially useful on Raspberry Pi)
230230-231231-If runs appear stuck, stream logs live:
232232-233233-```bash
234234-docker logs -f tweets-2-bsky
235235-```
236236-237237-For source installs, use whichever runtime you started with:
238238-239239-```bash
109109+bun install
110110+bun run build
111111+pm2 start "$HOME/.bun/bin/bun" --name tweets-2-bsky --cwd "$PWD" -- dist/index.js
240112pm2 logs tweets-2-bsky
241241-# or
242242-tail -f data/runtime/nohup.out
243243-```
244244-245245-If an account hangs during a scheduled cycle, the scheduler now times out that account and moves on automatically. You can tune this with `SCHEDULED_ACCOUNT_TIMEOUT_MS`.
246246-247247-### 8) Platform support
248248-249249-The Docker build is designed for multi-platform images:
250250-251251-- `linux/amd64` (typical Linux servers, many Windows machines)
252252-- `linux/arm64` (Apple Silicon Macs, ARM Linux servers)
253253-254254-This means the same image tag can be pulled on Docker Desktop (Windows/macOS) and Linux hosts.
255255-On Windows, use Docker Desktop in **Linux container** mode.
256256-257257-### Publishing (multi-platform images: linux/amd64 + linux/arm64)
258258-259259-Automatic publishing is included via GitHub Actions:
260260-261261-- `.github/workflows/docker-publish.yml` for GHCR
262262-- `.github/workflows/docker-publish-dockerhub.yml` for Docker Hub (only runs when Docker Hub secrets are set)
263263-264264-- pushes to `master` or `main` publish fresh multi-arch images and update `:latest`
265265-- tags like `v2.0.0` publish versioned tags (`:2.0.0`, `:2.0`)
266266-- manual publish is available with **Actions -> Publish Docker Image -> Run workflow**
267267-- after first publish, set GHCR package visibility to **Public** so anyone can pull
268268-269269-To enable automatic Docker Hub publishing with GitHub CLI:
270270-271271-```bash
272272-gh secret set DOCKERHUB_USERNAME --body "<dockerhub-username>"
273273-gh secret set DOCKERHUB_TOKEN --body "<dockerhub-access-token>"
113113+pm2 save
274114```
275115276276-Users can always pull the newest build with:
116116+#### Manual Foreground Runtime (no PM2)
277117278118```bash
279279-docker pull j4ckxyz/tweets-2-bsky:latest
280280-```
281281-282282-#### Option A: GitHub Container Registry (GHCR)
283283-284284-```bash
285285-docker login ghcr.io -u <github-username>
286286-docker buildx create --name t2b-builder --use
287287-docker buildx inspect --bootstrap
288288-289289-docker buildx build \
290290- --platform linux/amd64,linux/arm64 \
291291- -t ghcr.io/j4ckxyz/tweets-2-bsky:latest \
292292- -t ghcr.io/j4ckxyz/tweets-2-bsky:2.0.0 \
293293- --push .
294294-```
295295-296296-Then set the GHCR package visibility to **Public** in GitHub package settings.
297297-298298-#### Option B: Docker Hub
299299-300300-```bash
301301-docker login
302302-docker buildx create --name t2b-builder --use
303303-docker buildx inspect --bootstrap
304304-305305-docker buildx build \
306306- --platform linux/amd64,linux/arm64 \
307307- -t <dockerhub-user>/tweets-2-bsky:latest \
308308- -t <dockerhub-user>/tweets-2-bsky:2.0.0 \
309309- --push .
310310-```
311311-312312-Once published, users only need `docker pull` + `docker run`.
313313-314314-## Linux VPS Without Domain (Secure HTTPS via Tailscale)
315315-316316-If you host on a public VPS (Linux) and do not own a domain, use the server installer:
317317-318318-```bash
319319-chmod +x install-server.sh
320320-./install-server.sh
321321-```
322322-323323-What this does:
324324-325325-- runs the normal app install/build/start flow
326326-- auto-selects a free local app port if your chosen/default port is already in use
327327-- forces the app to bind locally only (`HOST=127.0.0.1`)
328328-- installs and starts Tailscale if needed
329329-- configures `tailscale serve` on a free HTTPS port so your dashboard is reachable over Tailnet HTTPS
330330-- prints the final Tailnet URL to open from any device authenticated on your Tailscale account
331331-332332-Optional non-interactive login:
333333-334334-```bash
335335-./install-server.sh --auth-key <TS_AUTHKEY>
336336-```
337337-338338-Optional fixed Tailscale HTTPS port:
339339-340340-```bash
341341-./install-server.sh --https-port 443
342342-```
343343-344344-Optional public exposure (internet) with Funnel:
345345-346346-```bash
347347-./install-server.sh --funnel
348348-```
349349-350350-Notes:
351351-352352-- this does **not** replace or delete `install.sh`; it wraps server-hardening around it
353353-- normal updates still use `./update.sh` and keep your local `.env` values
354354-- if you already installed manually, this is still safe to run later
355355-356356-## What This Project Does
357357-358358-- crossposts tweets and threads to Bluesky
359359-- handles images, videos, GIFs, quote tweets, and link cards
360360-- stores processed history in SQLite to avoid reposting
361361-- supports multiple Twitter source usernames per Bluesky target
362362-- provides both:
363363- - web dashboard workflows
364364- - CLI workflows (including cron-friendly mode)
365365-366366-## Requirements
367367-368368-Recommended runtime:
369369-370370-- Docker Desktop / Docker Engine
371371-372372-If running from source instead of Docker:
373373-374374-- Bun 1.x+ (auto-installed/upgraded by `install.sh` and `update.sh`)
375375-- git
376376-377377-Optional but recommended for source installs:
378378-379379-- PM2 (for managed background runtime)
380380-- Chrome/Chromium (used for some quote-tweet screenshot fallbacks)
381381-- build tools for native modules (`better-sqlite3`) if your platform needs source compilation
382382-383383-## Manual Setup (Technical)
384384-385385-### Standard run (foreground)
386386-387387-```bash
388388-git clone https://github.com/j4ckxyz/tweets-2-bsky
389389-cd tweets-2-bsky
390119bun install
391120bun run build
392121bun run start
393122```
394123395395-Open: [http://localhost:3000](http://localhost:3000)
396396-397397-### Set environment values explicitly
124124+#### Manual Nohup Runtime (no PM2)
398125399126```bash
400400-cat > .env <<'EOF'
401401-PORT=3000
402402-JWT_SECRET=replace-with-a-strong-random-secret
403403-# Optional: auth token lifetime (jsonwebtoken format), default is 30d.
404404-# JWT_EXPIRES_IN=30d
405405-# Optional: comma-separated browser origins allowed to call the API.
406406-# Leave unset to allow all origins (default/backward-compatible).
407407-# CORS_ALLOWED_ORIGINS=https://your-tailnet-host.ts.net,https://localhost:3000
408408-EOF
127127+mkdir -p data/runtime
128128+nohup bun run start > data/runtime/tweets-2-bsky.log 2>&1 &
129129+echo $! > data/runtime/tweets-2-bsky.pid
409130```
410131411411-### Rebuild native modules
132132+Stop nohup process:
412133413134```bash
414414-bun run rebuild:native
415415-bun run build
135135+kill "$(cat data/runtime/tweets-2-bsky.pid)"
416136```
417137418418-## First-Time Setup via CLI (Alternative to Web Forms)
138138+## First-Time Setup (After Install)
419139420420-```bash
421421-bun run cli -- setup-twitter
422422-bun run cli -- add-mapping
423423-bun run cli -- run-now
424424-```
140140+1. Open `http://localhost:3000`.
141141+2. Register the first user (this account becomes admin).
142142+3. In Settings, add Twitter cookies (`auth_token`, `ct0`; backup pair optional).
143143+4. Add a mapping (Twitter source usernames -> Bluesky account).
144144+5. Click `Run now`.
425145426426-`add-mapping` now runs a guided onboarding flow:
146146+## Twitter/X Integration Notes
147147+148148+- This project does not use Twitter's paid official API.
149149+- It uses `@the-convocation/twitter-scraper` and authenticated browser cookies to read account/tweet data.
150150+- Required cookies: `auth_token` and `ct0`.
151151+- If cookies expire, update them in Settings.
152152+- Keep cookies private; they are sensitive credentials.
427153428428-1. enter one or more Twitter source usernames
429429-2. create Bluesky account (or use existing)
430430-3. enter Bluesky identifier + app password (+ optional custom PDS URL)
431431-4. verify email, then create mapping and auto-sync profile metadata from Twitter
154154+For some quote-tweet screenshot fallbacks, Chromium is used (bundled in Docker, optional dependency for source installs).
432155433433-## Recommended Command Examples
156156+## CLI Quick Commands
434157435435-Always invoke CLI commands as:
158158+Always run CLI commands as:
436159437160```bash
438161bun run cli -- <command>
439162```
440163441441-### Status and basic operations
164164+Common commands:
442165443166```bash
444167bun run cli -- status
445168bun run cli -- list
446446-bun run cli -- recent-activity --limit 20
447447-```
448448-449449-### Credentials and configuration
450450-451451-```bash
452452-bun run cli -- setup-twitter
453453-bun run cli -- setup-ai
454454-bun run cli -- set-interval 5
455455-```
456456-457457-### Mapping management
458458-459459-```bash
460460-bun run cli -- add-mapping
461461-bun run cli -- sync-profile <mapping-id-or-handle> --source <twitter-username>
462462-bun run cli -- edit-mapping <mapping-id-or-handle>
463463-bun run cli -- remove <mapping-id-or-handle>
464464-```
465465-466466-### Running syncs
467467-468468-```bash
469169bun run cli -- run-now
470170bun run cli -- run-now --dry-run
471471-bun run cli -- run-now --web
472472-```
473473-474474-### Backfill and history import
475475-476476-```bash
171171+bun run cli -- add-mapping
477172bun run cli -- backfill <mapping-id-or-handle> --limit 50
478478-bun run cli -- import-history <mapping-id-or-handle> --limit 100
479479-bun run cli -- clear-cache <mapping-id-or-handle>
480480-```
481481-482482-### Dangerous operation (admin workflow)
483483-484484-```bash
485485-bun run cli -- delete-all-posts <mapping-id-or-handle>
486486-```
487487-488488-### Config export/import
489489-490490-```bash
491491-bun run cli -- config-export ./tweets-2-bsky-config.json
492492-bun run cli -- config-import ./tweets-2-bsky-config.json
493493-```
494494-495495-Mapping references accept:
496496-497497-- mapping ID
498498-- Bluesky handle/identifier
499499-- Twitter username
500500-501501-## Cron / CLI-Only Operation
502502-503503-Run every 5 minutes:
504504-505505-```cron
506506-*/5 * * * * cd /path/to/tweets-2-bsky && /usr/local/bin/bun run cli -- run-now >> /tmp/tweets-2-bsky.log 2>&1
507507-```
508508-509509-Run one backfill once:
510510-511511-```bash
512512-bun run cli -- backfill <mapping-id-or-handle> --limit 50
513513-```
514514-515515-## Background Runtime Options
516516-517517-### Option A: use `install.sh` (recommended)
518518-519519-```bash
520520-./install.sh
521521-./install.sh --status
522522-./install.sh --stop
523523-```
524524-525525-### Option B: manage PM2 directly
526526-527527-```bash
528528-pm2 start "$HOME/.bun/bin/bun" --name tweets-2-bsky --cwd "$PWD" -- dist/index.js
529529-pm2 logs tweets-2-bsky
530530-pm2 restart tweets-2-bsky --update-env
531531-pm2 save
532532-```
533533-534534-Do not use `--interpreter bun` with `dist/index.js` on PM2 installs that cannot `require()` async ESM modules. Use Bun as the process command instead (example above).
535535-536536-### PM2 migration help (older manual installs)
537537-538538-If you manually created PM2 processes on older versions, migrate once to the Bun binary launcher:
539539-540540-```bash
541541-pm2 delete tweets-2-bsky || true
542542-pm2 delete twitter-mirror || true
543543-pm2 start "$HOME/.bun/bin/bun" --name tweets-2-bsky --cwd "$PWD" -- dist/index.js
544544-pm2 save
545545-```
546546-547547-If your existing process must keep the legacy name:
548548-549549-```bash
550550-pm2 start "$HOME/.bun/bin/bun" --name twitter-mirror --cwd "$PWD" -- dist/index.js
551551-```
552552-553553-### Option C: no PM2 (nohup)
554554-555555-```bash
556556-mkdir -p data/runtime
557557-nohup bun run start > data/runtime/tweets-2-bsky.log 2>&1 &
558558-echo $! > data/runtime/tweets-2-bsky.pid
559559-```
560560-561561-Stop nohup process:
562562-563563-```bash
564564-kill "$(cat data/runtime/tweets-2-bsky.pid)"
565173```
566174567175## Updating
568176569569-Use:
177177+Source installs:
570178571179```bash
572180./update.sh
573181```
574182575575-`update.sh`:
576576-577577-- stashes local uncommitted changes before pull and restores them after update
578578-- pulls latest code (supports non-`origin` remotes and detached-head recovery)
579579-- ensures Bun is installed and upgraded to latest stable
580580-- installs dependencies
581581-- rebuilds native modules when runtime/dependencies changed
582582-- builds server + web dashboard
583583-- restarts existing runtime for PM2 **or** nohup mode
584584-- normalizes PM2 runtime to Bun binary launcher mode (avoids Bun interpreter crash loops on some PM2 builds)
585585-- preserves local `config.json` and `.env` with backup/restore
586586-587587-Useful update flags:
183183+Useful flags:
588184589185```bash
590186./update.sh --no-restart
591187./update.sh --skip-install --skip-build
592592-./update.sh --remote origin --branch main
593188```
594189595595-## Data, Config, and Security
596596-597597-Local files:
598598-599599-- `config.json`: mappings, credentials, users, app settings (sensitive; do not share)
600600-- `data/database.sqlite`: processed tweet history and metadata
601601-- `data/.jwt-secret`: auto-generated local JWT signing key when `JWT_SECRET` is not set (sensitive; keep private)
602602-- `.env`: runtime environment variables (`PORT`, `JWT_SECRET`, `JWT_EXPIRES_IN`, optional overrides)
190190+## Data and Security
603191604604-Security notes:
192192+Important files:
605193606606-- first registered dashboard user is admin
607607-- after bootstrap, only admins can create additional dashboard users
608608-- users can sign in with username or email
609609-- non-admin users only see mappings they created by default
610610-- admins can grant fine-grained permissions (view all mappings, manage groups, queue backfills, run-now, etc.)
611611-- only admins can view or edit Twitter/AI provider credentials
612612-- admin user management never exposes other users' password hashes in the UI
613613-- if `JWT_SECRET` is missing, server generates and persists a strong secret in `data/.jwt-secret` so sessions survive restarts
614614-- set `JWT_SECRET` in `.env` if you prefer explicit secret management across hosts
615615-- auth tokens default to `30d` expiry (`JWT_EXPIRES_IN`), configurable via `.env`
616616-- auth endpoints (`/api/login`, `/api/register`) are rate-limited per IP to reduce brute-force risk
617617-- prefer Bluesky app passwords (not your full account password)
194194+- `config.json` (mappings, credentials, users)
195195+- `data/database.sqlite` (processed history)
196196+- `data/.jwt-secret` (generated signing key when `JWT_SECRET` is unset)
197197+- `.env` (runtime env values)
618198619619-### Multi-User Access Control
199199+Security basics:
620200621621-- bootstrap account:
622622- - the first account created through the web UI becomes admin
623623- - open registration is automatically disabled after this
624624-- admin capabilities:
625625- - create, edit, reset password, and delete dashboard users
626626- - assign role (`admin` or `user`) and per-user permissions
627627- - filter the Accounts page by creator to review each user's mappings
628628-- deleting a user:
629629- - disables that user's mappings so crossposting stops
630630- - leaves already-published Bluesky posts untouched
631631-- self-service security:
632632- - every user can change their own password
633633- - users can change their own email after password verification
201201+- First registered user becomes admin.
202202+- Prefer Bluesky app passwords instead of your full Bluesky password.
203203+- Set an explicit `JWT_SECRET` in `.env` for predictable secret management.
204204+- Keep `config.json`, cookie values, and `.env` private.
634205635206## Development
636636-637637-### Start backend/scheduler from source
638207639208```bash
640209bun run dev
641641-```
642642-643643-### Start Vite web dev server
644644-645645-```bash
646210bun run dev:web
647647-```
648648-649649-### Build and quality checks
650650-651651-```bash
652211bun run build
653212bun run typecheck
654213bun run lint
···656215657216## Troubleshooting
658217659659-See: `TROUBLESHOOTING.md`
218218+See `TROUBLESHOOTING.md`.
660219661661-Common recovery when native modules fail to load:
220220+Common native module recovery:
662221663222```bash
664223bun run rebuild:native