TUI library built from scratch with TypeScript and a Rust core, featuring a component-based API, signal-based state management, CSS-like styling API, a Rust-powered layout and rendering engine, and cell-based diffing.
measured on
full-screen animation, 176×52 terminal, m4 pro
typical frame
1.9 ms (median)
slow frames
4.3 ms at p95, 5.6 ms at p99, 8.7 ms max
frame rate
453 fps average, 115 fps worst
avg frame
typescript 0.6 ms → rust 1.2 ms → sync 0.1 ms → flush 0.3 ms
In-browser MKV video streamer and player built from scratch, featuring real-time 4K anime upscaling powered by WGSL (WebGPU) shaders ported from Anime4K.
measured on
1080p → 4k upscaling, m4 pro
per 4k frame
32.3 ms
frame rate
31 fps max (film is 24)
gpu work
38 passes from 3 shaders, run in sequence
frame budget
41.7 ms at 24 fps: cpu 0.5 ms → upload 4.5 ms → shader passes 27.3 ms, 9.4 ms spare
Paid music collaborations and song-cover campaigns for artists worldwide. Led frontend and backend development across both products, built the Next.js frontend from scratch, and owned payments, payouts, and mobile audio features. Cut data processing for 1M+ artists from nine days to seven hours and roughly halved Soundcharts costs.
AI mapping platform for satellite and aerial imagery. Built high-performance React interfaces for large imagery datasets, simplified component state and data flow, and helped move users to centralized sign-in with zero downtime.
Social content and analytics platform with shoppable feeds for brands. Led React adoption within an Angular application and improved embedded app performance for e-commerce sites across Europe. Built a custom state manager that cut the JavaScript bundle by 20% and automated package publishing.
hitide
dec 2020 - feb 2021
Online clothing brand. Fully automated stock updates with a tested pipeline that transformed Airtable inventory data and stored it in the website's database.
Online sports insurance company for individuals, tournaments, etc. Built payment and subscription workflows and internal support tools, redesigned the PostgreSQL schema, and consolidated two backends into one codebase.
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 upstream X data 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.
Demo
Home
Stopped user fetch
Article fetch
Features
Export threads from a tweet URL or ID.
Export user posts and replies from @/username or profile URL.
Export X articles from article tweet URLs.
Export your bookmarks from x.com/i/bookmarks after you connect your X account. See Bookmarks.
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/....
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: server-backed TypeScript CLI in cli/src.
Shared core: pure parsing, normalization, credit, and export-formatting helpers in core.
Workspace: pnpm workspaces.
The upstream X data API is only reachable 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.
Production safeguards
Job status, tweets, stop actions, and checkout status are available only to the authenticated owner. Unknown and cross-user resource IDs receive the same not-found response.
Browser mutations require a same-origin request (CSRF protection); bearer-authenticated CLI requests remain supported. Client errors use stable public codes — raw upstream payloads, provider diagnostics, and persisted job exception messages stay server-side.
Abuse is bounded: at most two queued or running jobs per account (serialized in PostgreSQL), 30 export preflights per minute per account, 30-second upstream timeouts, a 100-page/15-minute runner cap that preserves partial results, and one-hour retention for terminal jobs (expired rows are removed opportunistically during job creation).
Exports
Source
Formats
Filename
Thread
Markdown, JSON
<username>-thread.<ext>
User timeline
Markdown, JSON
<username>-user-posts.<ext>
User replies
Markdown, JSON
<username>-replies.<ext>
Bookmarks
Markdown, JSON
<username>-bookmarks.<ext>
Partial
Markdown, JSON
Adds -partial before extension
Article
Markdown
<sanitized-article-title>.md
Bookmarks
Bookmarks are private, so Xport reads them with the official X API on behalf of your linked X account:
Sign in to Xport.
Paste https://x.com/i/bookmarks (or run xport bookmarks) and choose Connect X account. You can also connect from the account menu.
Approve read-only access (bookmark.read, tweet.read, users.read, offline.access) on X. Xport returns with the bookmarks input filled in.
Choose how many bookmarks to export (25, 100, 400, or 800; --limit in the CLI) and start the export. Nothing is fetched or charged before you start.
Cost: bookmarks are sold at X API cost, with no margin. X charges $0.005 for each bookmark it returns, so 10 bookmarks cost 7 credits (100 bookmarks = 70 credits, about $0.56). Other exports cost 1 credit per 20 posts. Xport asks X only for the bookmarks your credits and limit cover, so it never fetches bookmarks it cannot charge for.
Limits: X returns only your 800 most recent bookmarks. Each Xport user can link one X account; Disconnect X account in the account menu removes the link.
The app needs environment variables to boot — see Environment. The minimal set to sign in and run an export: DATABASE_URL, BETTER_AUTH_URL, BETTER_AUTH_SECRET, one OAuth provider (GitHub or Google), and X_API_URL/X_API_KEY. PostHog and Polar are optional; without Polar credits, grants can be issued locally with pn --filter web run credits:grant -- <email> <credits> "reason".
Useful commands:
bash
pn run build
pn run check
pn run lint
pn run format
pn run db:generate
pn run db:migrate
Environment
Required:
DATABASE_URL - PostgreSQL connection string.
BETTER_AUTH_URL - origin used for auth; must match the deployed domain in production.
BETTER_AUTH_SECRET - auth secret.
X_API_URL, X_API_KEY - upstream X data API; server-side only.
X_CLIENT_ID / X_CLIENT_SECRET - X OAuth 2.0 app for bookmarks export. Users link their X account; register <BETTER_AUTH_URL>/api/auth/callback/twitter as a callback.
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET or GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET - at least one OAuth provider for sign-in.
Optional:
SITE_URL - canonical origin for SEO metadata; falls back to BETTER_AUTH_URL, then http://localhost:3000.
Polar (billing) - 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. Signup credits are granted locally after user creation, with an idempotent retry on session creation to recover interrupted signups. Purchased credits are granted by the verified Polar order.paid webhook; enable that event at /api/auth/polar/webhooks and set the endpoint API version to 2026-10 in both sandbox and production. The SDK pins requests to 2026-10; updating the SDK does not migrate existing webhook endpoints. The legacy product environment-variable names map to 125 and 1250 credits respectively; see web/src/lib/billing-products.ts.
BILLING_MAINTENANCE=true - blocks new billing requests, signup/purchase processing, and Polar delivery during cutover; job stop requests remain available.
PostHog (analytics) - PUBLIC_POSTHOG_KEY, PUBLIC_POSTHOG_HOST. Omitted safely when unset.
Deployment
Production runs on Railway at https://xport.frixaco.com.
Service settings live in .railway/railway.ts, a Railway IaC partial that owns only xport-web:
Build: pn build
Start: pn start
Healthcheck: /
Railway does not read this file on deploy. Preview and apply changes with the CLI:
bash
railway config plan
railway config apply
bash
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 CLI (cli/src, published as @frixaco/xport) calls the deployed or local Xport server APIs; Social API credentials stay server-side.
Release: bump version in cli/package.json, commit, then push a matching tag. .github/workflows/release.yml checks the tag against the package version and publishes to npm with trusted publishing (no npm token).
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.
Older local extraction scripts in cli/ talk to the upstream API directly and read cli/.env; see cli/.env.example.
License
MIT.
Automated tests
bash
pnpm --filter web exec playwright install chromium # once per machine
pnpm test # logic + PostgreSQL + HTTP/CLI + browser, no provider credentials
pnpm test:live # real X API + Polar sandbox checkout/webhook/delivery
pnpm test:oauth # real GitHub/Google login using signed-in Helium on :9222
pnpm check
Docker must be running. Test commands build the app and use a disposable PostgreSQL database, removed on exit. Run them sequentially because they share build output. Keep ports 3108/3210/3211 free for routine tests and 3000 for live/OAuth tests (XPORT_TEST_PORT overrides it).
The routine suite covers exports/downloads, stop/reload/recovery, billing, access controls, OAuth callbacks, CLI flows, and public UI. It uses real app routes and PostgreSQL with mocked providers. GitHub Actions runs this suite on pushes/PRs; browser coverage is Chromium, including a mobile viewport.
Live: Set sandbox Polar and X API credentials in web/.env.local with POLAR_ENV=sandbox. Disposable customers use aliases from XPORT_TEST_EMAIL or the first local database user's email; personal credits are not spent. Tests cover real exports and checkout/webhook/delivery, capped at 50 X API calls. Full exports use @frixaco; stop/reload checks save at most two pages. Test customers are deleted; sandbox orders remain.
OAuth: Set OAuth credentials and the sandbox token in web/.env.local. Use signed-in Helium on http://127.0.0.1:9222 (XPORT_BROWSER_CDP overrides it), with callbacks registered at http://localhost:3000/api/auth/callback/github and /google. Update callbacks if changing the port. Tests restore localhost auth cookies afterward; avoid another localhost Better Auth app during the run. Expired sessions or MFA may need interaction.
Reports are in web/playwright-report/; failure traces/screenshots are in web/test-results/. Artifacts may contain private data and are ignored by Git. OAuth capture is disabled for the personal browser. Live and OAuth tests run explicitly, outside CI.
For a focused billing-only run against a manually provisioned local *_test database:
bash
DATABASE_URL=postgresql://<user>:<password>@127.0.0.1:5432/xport_billing_test pnpm --filter web test:billing
Credit ledger
The schema adds user.credit_balance and xport_credit_transactions. A ledger row records each grant/debit and its optional Polar delivery payload. Nitro runs billing-delivery every minute; failed or interrupted deliveries retry using the same operation key. pnpm --filter web credits:deliver also runs delivery manually. Neither path changes local balances during delivery.
Direct article, thread, user-info, and user-tweets APIs now require POST and an Idempotency-Key header (1–128 letters, digits, _, or -). Query parameters are unchanged. Reuse the key only when retrying the same request. The ledger retains the paid direct response so a lost response can be replayed without another fetch or debit. Browser and CLI article callers generate a fresh key per export; existing CLI releases using GET must be updated alongside the server.
Billing cutover
Prepare and test the release first. This is a single maintenance-window cutover, not parallel Polar/local spending. All script commands require DATABASE_URL in the shell; snapshot and delivery additionally use the configured Polar token/environment. Node scripts do not automatically load .env.local (use Node's --env-file locally if needed).
Block new billing requests at the deployment ingress before stopping the old release: the old release does not recognize the maintenance flag. Drain or stop queued/running jobs. Pause manual grants and checkout/signup processing; allow rejected webhooks to retry later.
Apply pnpm db:migrate, then start the new release with BILLING_MAINTENANCE=true. Keep ingress maintenance active. Do not expose a zero-balance cutover before import.
With BILLING_MAINTENANCE=true in the script environment, run pnpm --filter web credits:migrate snapshot /secure/path/credits.json. Snapshot collection is read-only and refuses to overwrite an existing file. It fails on unmapped customers instead of guessing by email.
Review every snapshot row. Resolve missing/deleted/duplicate customers explicitly (a genuinely missing customer may use polarCustomerId: null and a reviewed opening balance). Wait for in-flight checkouts and Polar meter changes to settle. includedOrderIds must list exactly the paid purchases represented in originalBalance; late purchases excluded from the balance must be removed from this list so their webhook can grant them. Mark each reviewed row confirmed: true. Reconcile any negative Polar balance separately: importing zero locally does not repair it externally.
Run pnpm --filter web credits:migrate import /secure/path/credits.json. The import is atomic and replay-safe; it rejects an account with other local ledger activity. Original balances and included order IDs stay in opening-entry metadata, and imports never queue Polar grants.
Run web/scripts/reconcile-credits.sql, verify the local balances, then disable maintenance and replay outstanding purchase webhooks. Imported purchase IDs and historical signup grants are skipped. Check pending deliveries and Railway logs after reopening ingress.
For a manual positive adjustment, use a stable operation key and reuse it on retries:
bash
pnpm --filter web credits:grant user@example.com 50 "Support adjustment" --key support-ticket-123
Do not edit balances directly or send standalone negative Polar usage events. Reporting behavior follows Polar's event ingestion API; purchases use order.paid.