Deployment Guide
Deploy XActions anywhere — Cloudflare, Railway, Fly.io, Render, Docker, or self-host. All free tiers supported.
Architecture
XActions has two deployable components:
| Component | What | Needs |
|---|---|---|
| Static Frontend | dashboard/ — HTML/CSS/JS pages |
Any static host (CDN) |
| API Backend | api/server.js — Express + Puppeteer + WebSocket |
Node.js 20+, Chromium, Postgres, Redis |
You can deploy them together (Docker, Fly.io, Railway) or split them across services (Cloudflare Workers for the site and edge API + Railway for the optional reads/analytics backend). X account actions run in the browser extension, not on any backend.
Quick Start
# Auto-detect platform and deploy
./scripts/deploy.sh
# Or specify a platform
./scripts/deploy.sh railway
./scripts/deploy.sh cloudflare
./scripts/deploy.sh fly
./scripts/deploy.sh docker
Platform Comparison
| Platform | Frontend | Backend | Free Tier | Config File |
|---|---|---|---|---|
| Cloudflare Pages/Workers | ✅ | Edge API + proxy | 100k req/day | wrangler.toml |
| Google Cloud Run | ❌ | ✅ | Pay-per-use, no idle cost | deploy/gcp/cloudbuild-api.yaml |
| Vercel | ✅ | ❌ | 100GB bandwidth | vercel.json |
| Railway | ❌ | ✅ | $5 credit/mo | railway.json |
| Fly.io | ❌ | ✅ | 3 shared VMs | fly.toml |
| Render | ✅ | ✅ | 750 hrs/mo web | render.yaml |
| Docker | ✅ | ✅ | Free (self-host) | docker-compose.yml |
| Coolify | ✅ | ✅ | Free (self-host) | docker-compose.coolify.yml |
Recommended combo (what xactions.app runs today): Cloudflare Pages (static site + dashboard) + Google Cloud Run (xactions-api, backed by Cloud SQL Postgres and a shared Memorystore Redis instance). Railway, Fly, and Render remain fully supported for self-hosters who prefer them.
Cloudflare Workers (Full Site + Edge API)
xactions.app runs on a single Cloudflare Worker. It serves the entire public surface (landing page, dashboard, docs, blog, static assets) from Workers static assets, and handles the dynamic surface natively.
The hosted model
| Surface | Where it runs |
|---|---|
| All pages, docs, blog, assets | Workers static assets (global CDN) |
/api/health, /api/ai/health, /api/ai/pricing |
Worker, at the edge |
/openapi.json, /.well-known/x402 |
Worker, at the edge |
/api/ai/* x402 payment gate (paid reads: scrape, analytics) |
Worker, at the edge → API_ORIGIN |
| X account actions (follow, unfollow, like, reply, post) | The browser extension |
Other /api/* (auth, user, analytics) |
Proxied to API_ORIGIN when set |
Account actions are never executed server-side by the hosted service.
Follow/unfollow/like/reply need your logged-in X session; running them from a
server would mean custodying your session token and driving your account from a
datacenter IP (an account-safety and X-ToS problem). Those routes return 501
with a pointer to the extension, which performs them locally in your own
browser session. Self-hosters who want server-side execution can still run the
full Node backend (below) and set API_ORIGIN.
Deploy
npx wrangler login # once
npm run deploy:cloudflare # builds dist-cloudflare/ and deploys the Worker
npm run build:cloudflare assembles dist-cloudflare/ from site/,
dashboard/, public/, and the llms*.txt files, mirroring the old
vercel.json route table with plain file placement.
Connect a backend (optional)
Reads (server-side scraping, analytics) and account features can run on the full Node backend. Deploy it to Google Cloud Run (below, what production uses), Railway, Fly, or Docker, then point the Worker/Pages proxy at it:
npx wrangler deploy --var API_ORIGIN:https://your-api.example.com
or set API_ORIGIN in wrangler.toml. On Cloudflare Pages, the same wiring
is a dashboard/_redirects rule (/api/* https://your-api.example.com/api/:splat 200).
Until it is set, those routes return a 503 explaining exactly this, and
everything else keeps working.
Custom Domain
Cloudflare dashboard → Workers & Pages → xactions → Settings → Domains &
Routes → Add → xactions.app. If the DNS zone is already on Cloudflare this is
one step; TLS is automatic. (Delete any leftover apex A/CNAME record pointing
at a previous host first, or the attach is refused.)
Google Cloud Run (Backend, current production)
xactions.app's /api/* traffic is proxied to a Cloud Run service named
xactions-api, backed by a dedicated Cloud SQL Postgres instance and a
Memorystore Redis instance shared with other services on the same GCP
project (Bull queue keys are namespaced with a xactions prefix so they
never collide — see api/services/jobQueue.js).
One-time setup
gcloud auth login # once per machine — needs a browser
bash deploy/gcp/provision-api.sh
This creates the Cloud SQL instance, Secret Manager entries
(xactions-database-url, xactions-jwt-secret, xactions-session-secret,
xactions-admin-api-key), the required IAM bindings, then builds and deploys
the image via Cloud Build. It prints the Cloud Run URL and the command to map
api.xactions.app to it.
Redeploy after a code change
gcloud builds submit --config deploy/gcp/cloudbuild-api.yaml \
--region us-central1 --project aerial-vehicle-466722-p5
Health check
curl https://api.xactions.app/api/health
Railway (Backend)
Free: $5 credit/month (no credit card needed), auto-sleep on inactivity.
Deploy
- Go to Railway → New Project → Deploy from GitHub
- Select
nirholas/XActions - Railway auto-detects
railway.json+nixpacks.toml - Add services:
- PostgreSQL — Click "Add" → Database → PostgreSQL
- Redis — Click "Add" → Database → Redis
- Railway auto-wires
DATABASE_URLandREDIS_URL - Set environment variables:
JWT_SECRET=<generate with: openssl rand -hex 32> SESSION_SECRET=<generate with: openssl rand -hex 32> NODE_ENV=production FRONTEND_URL=https://xactions.pages.dev
CLI Deploy
npm install -g @railway/cli
railway login
railway link # connect to your project
railway up
GitHub Actions (Automatic)
Set GitHub secret:
RAILWAY_TOKEN— Get from Railway dashboard → Account → Tokens
Deploys automatically on push to main when api/, src/, or prisma/ change.
Fly.io (Full Stack)
Free: 3 shared-cpu VMs, 256MB RAM each, 3GB persistent storage.
Deploy
# Install Fly CLI
curl -L https://fly.io/install.sh | sh
# Login
fly auth login
# First deploy
fly launch --no-deploy
fly postgres create --name xactions-db
fly postgres attach xactions-db
fly redis create --name xactions-redis
# Set secrets
fly secrets set JWT_SECRET=$(openssl rand -hex 32)
fly secrets set SESSION_SECRET=$(openssl rand -hex 32)
# Deploy
fly deploy
# Check status
fly status
fly logs
Custom Domain
fly certs create yourdomain.com
# Add CNAME record: yourdomain.com → xactions.fly.dev
Render (Full Stack)
Free: 750 hours/month web services, free PostgreSQL (90 days), static sites.
Deploy
- Go to Render Dashboard
- Click New → Blueprint
- Connect GitHub repo → Select
nirholas/XActions - Render reads
render.yamland creates:xactions-api— Docker web servicexactions-worker— Background workerxactions-dashboard— Static sitexactions-db— PostgreSQL
- Click Apply
All environment variables are auto-configured via the blueprint.
Note: Render's free PostgreSQL expires after 90 days. Either recreate or upgrade to $7/mo.
Docker (Self-Host)
Free on any VPS (Oracle Cloud free tier, Hetzner, DigitalOcean, etc.)
Quick Start
# Clone the repo
git clone https://github.com/nirholas/XActions.git
cd XActions
# Copy and edit environment variables
cp .env.example .env
# Edit .env — set JWT_SECRET, SESSION_SECRET at minimum
# Start everything
docker compose up -d
# Check status
docker compose ps
docker compose logs -f api
# Run database migrations
docker compose exec api npx prisma migrate deploy
Services
| Service | Port | Description |
|---|---|---|
api |
3001 | Express API + Dashboard |
worker |
— | Background job processor |
postgres |
5432 | PostgreSQL 16 |
redis |
6379 | Redis 7 |
Useful Commands
# View logs
docker compose logs -f api
# Restart API
docker compose restart api
# Update to latest
git pull
docker compose up -d --build
# Database shell
docker compose exec postgres psql -U xactions
# Stop everything
docker compose down
# Stop and delete data
docker compose down -v
Coolify (Self-Host with UI)
Coolify is a free, self-hosted Heroku/Vercel alternative.
Deploy
- Install Coolify on your VPS:
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash - In Coolify dashboard → New Resource → Docker Compose
- Paste contents of
docker-compose.coolify.yml - Set environment variables in Coolify UI
- Deploy
Coolify auto-configures Traefik reverse proxy + Let's Encrypt SSL.
Docker Image (GitHub Container Registry)
Pre-built images are published automatically on every push to main:
# Pull latest
docker pull ghcr.io/nirholas/xactions:main
# Run with external Postgres + Redis
docker run -d \
-p 3001:3001 \
-e NODE_ENV=production \
-e DATABASE_URL="postgresql://..." \
-e JWT_SECRET="your-secret" \
-e REDIS_HOST="your-redis-host" \
ghcr.io/nirholas/xactions:main
Environment Variables
See .env.example for the full list. Required for production:
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
✅ | PostgreSQL connection string |
JWT_SECRET |
✅ | Auth token signing key |
SESSION_SECRET |
Recommended | Session encryption key |
REDIS_HOST |
Recommended | Redis host for job queue |
REDIS_PORT |
Recommended | Redis port (default: 6379) |
NODE_ENV |
Recommended | Set to production |
FRONTEND_URL |
Optional | CORS origin for frontend |
PORT |
Optional | API port (default: 3001) |
Generate secrets:
openssl rand -hex 32
CI/CD Workflows
All workflows are in .github/workflows/:
| Workflow | Trigger | Action |
|---|---|---|
ci.yml |
Push/PR to main | Tests + build check + Docker build |
deploy-railway.yml |
Push to main (api/src/prisma) | Deploy API to Railway |
deploy-cloudflare.yml |
Push to main (dashboard) | Deploy frontend to Cloudflare Pages |
docker-publish.yml |
Push to main + tags | Build & push to GitHub Container Registry |
Required GitHub Secrets
| Secret | For | How to get |
|---|---|---|
RAILWAY_TOKEN |
Railway deploy | Railway dashboard → Account → Tokens |
CLOUDFLARE_API_TOKEN |
Cloudflare deploy | Cloudflare dashboard → API Tokens |
CLOUDFLARE_ACCOUNT_ID |
Cloudflare deploy | Cloudflare dashboard sidebar |
GITHUB_TOKEN is automatic — no setup needed for Docker registry.
Monitoring
After deployment, verify everything works:
# Health check
curl https://your-api-url/api/health
# Expected response:
# {"status":"ok","service":"xactions-api","timestamp":"..."}
Dashboard should be accessible at your frontend URL with all routes working (clean URLs like /docs, /features, /mcp).