Skip to content

Repository files navigation

Xport

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.

Features

  • 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 jobId URL.
  • Start exports from direct links such as xport.frixaco.com/x.com/... and xport.frixaco.com/twitter.com/....

Demo

Home Stopped user fetch Article fetch
Xport home page screenshot Xport stopped user fetch screenshot Xport article fetch screenshot

Architecture

Resumable backend architecture

  • 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 in cli.
  • 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.

Routes

  • / - main export UI.
  • /x.com/<path> and /twitter.com/<path> - direct-export redirects.
  • /auth-error and /checkout/success - utility redirects.
  • /api/* - server API routes.

Exports

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

Local Development

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 dev

Useful commands:

pn run build
pn run check
pn run lint
pn run format
pn run db:generate
pn run db:migrate

Environment

Core:

  • DATABASE_URL
  • BETTER_AUTH_URL
  • BETTER_AUTH_SECRET
  • SITE_URL
  • X_API_URL
  • X_API_KEY

OAuth:

  • GITHUB_CLIENT_ID
  • GITHUB_CLIENT_SECRET
  • GOOGLE_CLIENT_ID
  • GOOGLE_CLIENT_SECRET

Polar:

  • POLAR_ENV
  • POLAR_ACCESS_TOKEN or SANDBOX_POLAR_ACCESS_TOKEN
  • POLAR_WEBHOOK_SECRET
  • POLAR_CREDITS_50_CREDITS_PRODUCT_ID or SANDBOX_POLAR_CREDITS_50_CREDITS_PRODUCT_ID
  • POLAR_CREDITS_500_CREDITS_PRODUCT_ID or SANDBOX_POLAR_CREDITS_500_CREDITS_PRODUCT_ID

Analytics:

  • PUBLIC_POSTHOG_KEY
  • PUBLIC_POSTHOG_HOST

Deployment

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 logs

Database 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

CLI

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 /tmp

Workspace 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 logout

CLI 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>

License

MIT License.

About

Export X (Twitter) threads, articles and user posts as Markdown for dirt cheap

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages