Xport exports X (ex-Twitter) posts, threads, user timelines, and articles from a web app or server-backed CLI.
Xport is built around resumable export jobs. A browser creates a PostgreSQL-backed job, the server runs a disposable in-process worker to fetch pages from the Social API, and each page writes progress, cursors, credits, and fetched posts back to Postgres. If the process dies or the browser reloads, the job can be resumed from its jobId and stored next_cursor.
- Export threads from a tweet URL or ID.
- Export user posts from
@/username or profile URL. - Export X articles from article tweet URLs.
- Preview fetched content and media before export.
- Download Markdown and JSON where supported.
- Stop long-running fetches and export partial results.
- Resume background exports from a
jobIdURL. - Start exports from direct links such as
xport.frixaco.com/x.com/...andxport.frixaco.com/twitter.com/....
| Home | Stopped user fetch | Article fetch |
|---|---|---|
![]() |
![]() |
![]() |
- Web app: TanStack Start, TanStack Router, React, shadcn/ui, Tailwind, Nitro.
- API: server routes under
web/src/routes/api. - Jobs: PostgreSQL-backed fetch jobs and tweet snapshots.
- Billing: better-auth and Polar credits.
- CLI: no-library TypeScript CLI in
cli/src, plus older local extraction scripts incli. - Shared core: pure parsing, normalization, credit, and export-formatting helpers in
core. - Workspace: pnpm workspaces.
Social API access stays server-side through X_API_URL and X_API_KEY.
The main backend state machine lives in web/src/lib/fetch-job.ts: create, claim, fetch, store, charge, stop, resume, complete, and fail. A runner claims work with an internal runner_id, writes progress only while it owns the job, and clears runner_id when the job reaches completed, stopped, or failed.
/- main export UI./x.com/<path>and/twitter.com/<path>- direct-export redirects./auth-errorand/checkout/success- utility redirects./api/*- server API routes.
| Source | Formats | Filename |
|---|---|---|
| Thread | Markdown, JSON | <username>-thread.<ext> |
| User posts | Markdown, JSON | <username>-user-posts.<ext> |
| Partial | Markdown, JSON | Adds -partial before extension |
| Article | Markdown | <sanitized-article-title>.md |
Requirements:
- Node.js 24.x
- pnpm 11.x
- PostgreSQL
- OAuth credentials for local sign-in
- Polar credentials
- Social API credentials
pn install
pn run db:migrate
pn run devUseful commands:
pn run build
pn run check
pn run lint
pn run format
pn run db:generate
pn run db:migrateCore:
DATABASE_URLBETTER_AUTH_URLBETTER_AUTH_SECRETSITE_URLX_API_URLX_API_KEY
OAuth:
GITHUB_CLIENT_IDGITHUB_CLIENT_SECRETGOOGLE_CLIENT_IDGOOGLE_CLIENT_SECRET
Polar:
POLAR_ENVPOLAR_ACCESS_TOKENorSANDBOX_POLAR_ACCESS_TOKENPOLAR_WEBHOOK_SECRETPOLAR_CREDITS_50_CREDITS_PRODUCT_IDorSANDBOX_POLAR_CREDITS_50_CREDITS_PRODUCT_IDPOLAR_CREDITS_500_CREDITS_PRODUCT_IDorSANDBOX_POLAR_CREDITS_500_CREDITS_PRODUCT_ID
Analytics:
PUBLIC_POSTHOG_KEYPUBLIC_POSTHOG_HOST
Production runs on Railway at https://xport.frixaco.com.
Railway uses railway.json:
- Build:
pn build - Start:
pn start - Healthcheck:
/
railway service xport-web
railway up --detach
railway service status
railway logsDatabase schema is managed with Drizzle:
- Schema:
web/src/db/schema.ts - Migrations:
web/migrations/ - Generate migrations:
pn run db:generate - Apply migrations:
pn run db:migrate
The supported app CLI calls the deployed/local Xport server APIs. Social API credentials stay server-side.
Install from npm:
npm install -g @frixaco/xport
xport login
xport export --format markdown --out . "https://x.com/burakeregar/status/2020852442230120752"
xport stop <jobId>Prepare the npm package locally:
pn --filter @frixaco/xport build
pn --filter @frixaco/xport pack --pack-destination /tmpWorkspace usage:
pn --filter @frixaco/xport xport login
pn --filter @frixaco/xport xport whoami
pn --filter @frixaco/xport xport credits
pn --filter @frixaco/xport xport export --format markdown --out . "https://x.com/burakeregar/status/2020852442230120752"
pn --filter @frixaco/xport xport export --format json --stdout "@frixaco"
pn --filter @frixaco/xport xport stop <jobId>
pn --filter @frixaco/xport xport logoutCLI export options must come before the input: xport export [options] <input> is the only supported form. Thread and user exports print Job ID: <uuid> to stderr as soon as the resumable job is created, and xport stop <jobId> stops that job from another terminal. Auth uses the Better Auth device code flow; tokens are stored at ~/.config/xport/config.json with 0600 permissions. For automation, set XPORT_TOKEN; for local servers, set XPORT_BASE_URL.
Legacy local extraction scripts still read cli/.env; see cli/.env.example.
pn --filter @frixaco/xport fetch-thread -- <tweet-url-or-id>
pn --filter @frixaco/xport fetch-article -- <tweet-url-or-id>
pn --filter @frixaco/xport fetch-posts-by-username -- <username>
pn --filter @frixaco/xport fetch-user-info -- <username>
pn --filter @frixaco/xport fetch-bookmarks
pn --filter @frixaco/xport exec tsx json-to-text.ts <input.json>MIT License.


