Skip to content
x.md
Esc
navigateopen⌘Jpreview
On this page

Self-hosting

Run the service and its documentation from the same Vercel project.

Run locally

git clone https://github.com/pc-style/x-md.git
cd x-md
bun install
bun run dev

The Vite server runs the landing page and local API handlers. Run bun run docs:dev separately for the Blume documentation server.

Build and deploy

bun run build
vercel --prod

The build generates Blume docs, then the Vite landing page into dist. The included Vercel configuration serves /docs, the static assets, and the API rewrites together.

  • docs/
    • index.mdx
    • (api)/
      • posts.mdx
      • search.mdx
      • profiles.mdx
      • pagination.mdx
      • responses.mdx
      • reliability.mdx
      • meta.ts
    • agents.mdx
    • self-hosting.mdx
  • blume.config.ts
  • theme.css

Live search provider

Latest and Top try FxTwitter, then the custom-built live search provider, then an optional Firecrawl web fallback. Photos, Videos, and Users use the live provider directly and return 503 when it is not configured.

The provider is configured through X_SEARCH_SESSIONS_JSON; the expected shape is defined in lib/xsearch.ts. For local development, use the gitignored accounts.local.json. Treat its contents as secrets and keep them out of version control.

The provider budgets its own upstream capacity per 15-minute window, shared across post and user searches. Failed attempts and page walks count. Unhealthy entries are taken out of rotation in the current process and retried later; health is per instance.

Your own upstream, or a pool of them

Profiles, posts, and bulk imports read https://api.fxtwitter.com by default. That public instance allows about 1000 requests a minute per source IP, which caps a bulk import’s sustained fan-out around 16 chains. Point x.md at your own FxEmbed deployment instead — FxEmbed is itself a Cloudflare Worker, and Workers is its documented self-host path:

FXTWITTER_BASE_URL=https://fx.example.workers.dev

Or give it a pool. Requests rotate across every base; one that answers 429 sits out for its Retry-After while the walk continues on the others, and the import route allows 32 chains per base:

FXTWITTER_BASE_URL=https://fx-a.example.workers.dev,https://fx-b.example.workers.dev,https://api.fxtwitter.com

How high concurrency can go on your own instance depends on that deployment’s capacity and X’s own per-token limits, which FxEmbed manages; x.md still backs off on any 429 it returns. bench/RESULTS-scale.md in the repository is the reference run against the public instance; bun scripts/benchmark-import.ts --scale reproduces it against whatever FXTWITTER_BASE_URL names, so you can measure your own pool.

The caps in lib/import.ts (IMPORT_MAX_POSTS, IMPORT_MAX_CONCURRENCY_PER_BASE) and the allowances in lib/quotas.ts are the only limits; there are no hosted-only ones.

The post archive

Bulk imports store what they walk, per account, so repeat imports only fetch the gap. With the shared Redis below configured the archive is durable across instances and deploys; without it, it lives in process memory for the life of the function. Keys are hist:{handle}:index, hist:{handle}:ids (a sorted set by post time), and hist:{handle}:posts. Budget roughly 2 KB per post; a 3000-post account is about 6 MB.

Deploying the fast stack

The shape that removes every artificial ceiling:

  1. FxEmbed on Cloudflare Workers — your own upstream, no per-IP cap. FxEmbed routes by Host, so put your API hostname in its API_HOST_LIST. It works best with X account credentials (credentials.json: authToken + csrfToken per account, the same pair as x.md’s search sessions) and spreads requests across them; without credentials it runs on guest access with lower limits. Its capacity is therefore roughly the number of X accounts behind it, and one Cloudflare account on the free tier covers 100k requests a day.
  2. x.md on Vercel with Fluid Compute — the import function is declared with a 120 s maxDuration in vercel.json; Fluid allows it and keeps streaming responses open.
  3. Upstash Redis via the Vercel marketplace — the archive and the rate-limit counters.

Set FXTWITTER_BASE_URL to your pool (public instance last), deploy, and verify with one import followed by ?index=true.

Shared counters

Attach Upstash Redis through the Vercel marketplace or CLI. The service automatically uses either environment-variable pair:

  • KV_REST_API_URL and KV_REST_API_TOKEN
  • UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN

These share per-IP and provider counters across instances. Without them, counters fall back to process memory. Live-provider checks fail closed when the counter store is unavailable, protecting the shared pool; the general request limiter still fails open. Redeploy after connecting environment variables.

Historical statistics

PostHog is optional. Connect its Vercel Marketplace integration to the production environment, choose your data region, and review its billing terms. It supplies NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN and NEXT_PUBLIC_POSTHOG_HOST; the API reads these on the server only. Alternatively, set POSTHOG_PROJECT_TOKEN and POSTHOG_HOST (for example, https://eu.i.posthog.com). Redeploy after configuring them.

Production API functions send a request_completed event with the route category, method, HTTP status, duration in milliseconds, application-cache result, key-validation status, public/key access, and degraded-search flag. Use these for request volume by endpoint, error and 429 rates, latency percentiles, and application-cache hit rate. Delivery runs in the background with a three-second timeout; failures do not change API responses.

request_completed events contain no search text, URLs, client IPs, bearer tokens, or key labels. Verified keys use a pseudonymous ID derived from their internal ID and the PostHog project token; rotating that token changes the pseudonyms. Public requests have a new ID per event, so distinct-user counts and anonymous retention are not meaningful. Person profiles, geolocation enrichment, and session replay are not enabled by this instrumentation.

These statistics cover completed function responses, not requests served by Vercel’s CDN, aborted requests, or platform errors before the handler runs. They are not billing-grade totals. Local development, previews, admin routes, and CORS preflights are excluded. Redis remains the source of truth for live limits and capacity; PostHog is only for historical analysis.

Landing-page web analytics

Set VITE_POSTHOG_KEY to the project token and VITE_POSTHOG_HOST to its ingestion host or managed reverse proxy (this deployment uses https://p.pcstyle.dev). These are public build-time values, not personal API credentials. Rebuild after changing them. Capture requires a production build with VERCEL_ENV=production; local development and previews do not send events. The SDK’s ui_host points to EU PostHog. In PostHog’s Web Analytics settings, add your public site URL (for this deployment, https://x.pcstyle.dev).

The landing page sends pageviews, page leaves, conversion_requested, and skill_install_command_copied. Anonymous browser IDs persist in local storage to measure returning visitors; session and browser/device properties support Web Analytics. No person profiles are created. Event URLs contain only the landing-page origin and /, without query strings or fragments. Referrers, form contents, automatic clicks, errors, and recordings are excluded; traffic-source attribution and geographic reports are therefore limited. The proxy receives the browser’s source IP, while event properties disable IP-based enrichment.

This covers the landing page only, not docs, converted posts, or admin. API events remain separate from browser visits. Existing Vercel Web Analytics on the public landing page is unchanged. The admin dashboard includes no analytics scripts.

Optional configuration

Variable Purpose
FIRECRAWL_API_KEY Conversion fallback and degraded Latest/Top search
CONTEXT_DEV_API_KEY Additional post-conversion fallback
CACHE_TTL_SECONDS Cache lifetime; default 3600
CACHE_DISABLED Disable application caching
CACHE_PERSIST Control persistent application caching

Was this page helpful?