Ship TillPulse to production.
Two pieces. The dashboard runs on Cloudflare Pages + Workers via @opennextjs/cloudflare. The ingest API runs as a Fastify Node.js process — Docker, Railway, Fly, your own VPS, anywhere with persistent compute.
Architecture on Cloudflare
Browser
│
▼
Cloudflare Edge
│
├─ Static assets ──→ CDN (HTML, CSS, JS, images)
│
└─ Dynamic ──→ Cloudflare Worker
├─ Next.js RSC + API (via @opennextjs/cloudflare)
├─ Neon Postgres (HTTP fetch)
├─ Upstash Redis (HTTP REST)
├─ ClickHouse (HTTP)
├─ Meilisearch (HTTP)
├─ Anthropic (HTTP SSE)
└─ Resend (HTTP)The ingest API is not on Cloudflare. It needs persistent connections for high-throughput Redis Stream publishes and is best placed close to your users (in whichever region they're concentrated).
Prerequisites
- Cloudflare account (free tier works to start)
- Neon Postgres database
- Upstash Redis (REST API)
- ClickHouse Cloud or self-hosted ClickHouse over HTTPS
- Meilisearch Cloud or self-hosted
- Resend account (sender domain verified)
- Anthropic API key (for Ask Pulse + TillMind)
1. Authenticate with Cloudflare
npx wrangler login2. Create the Pages project
cd apps/web
npx wrangler pages project create <your-project> --production-branch main3. Set secrets
npx wrangler pages secret put DATABASE_URL
npx wrangler pages secret put UPSTASH_REDIS_REST_URL
npx wrangler pages secret put UPSTASH_REDIS_REST_TOKEN
npx wrangler pages secret put JWT_PRIVATE_KEY
npx wrangler pages secret put JWT_PUBLIC_KEY
npx wrangler pages secret put TOTP_ENCRYPTION_KEY
npx wrangler pages secret put ENCRYPTION_KEY
npx wrangler pages secret put RESEND_API_KEY
npx wrangler pages secret put RESEND_FROM
npx wrangler pages secret put ANTHROPIC_API_KEY
npx wrangler pages secret put CLICKHOUSE_URL
npx wrangler pages secret put CLICKHOUSE_PASSWORD
npx wrangler pages secret put MEILI_URL
npx wrangler pages secret put MEILI_MASTER_KEY
npx wrangler pages secret put SLACK_SIGNING_SECRET
npx wrangler pages secret put SLACK_ACTION_SIGNING_SECRETNon-secrets (e.g. APP_URL) go in wrangler.toml:
[vars]
APP_ENV = "production"
APP_URL = "https://app.tillpulse.io"
INGEST_HOST = "https://ingest.tillpulse.io"4. Build & deploy
# From the repo root:
pnpm run deploy:cf
# Or just the web app:
cd apps/web && pnpm run deploy:cfThis runs turbo run build (compiles @tillpulse/db), then opennextjs-cloudflare to bundle Next.js for Workers, then wrangler pages deploy.
5. Custom domain
Cloudflare dashboard → Pages → tillpulse-web → Custom domains → Set up a custom domain. Add app.tillpulse.io (or your chosen subdomain) and follow the DNS instructions.
6. Run database migrations
# From the repo root, with DATABASE_URL pointing at Neon
pnpm db:generate # turbo proxies to drizzle-kit generate
pnpm db:migrate # applies pending migrationsClickHouse: run infra/clickhouse/init.sql against your ClickHouse cluster. The bottom of the file contains idempotent ALTER TABLE statements for upgrading existing deployments.
7. Deploy the ingest API
The ingest API is a stock Node.js / Fastify process. Anywhere with persistent compute and outbound HTTPS will work — Railway, Fly.io, Render, Heroku, AWS ECS, plain VPS.
# Build
pnpm --filter @tillpulse/ingest-api build
# Run
DATABASE_URL=... \
UPSTASH_REDIS_REST_URL=... \
UPSTASH_REDIS_REST_TOKEN=... \
INGEST_PORT=3001 \
INGEST_RATE_LIMIT_PER_DSN=10000 \
node apps/ingest-api/dist/index.jsOr use the docker-compose.yml at the repo root for a one-command local deploy of Postgres + Redis + ClickHouse + Meilisearch + ingest-api + event-processor.
8. Deploy the event processor
The event processor runs alongside the ingest API. Same env, plus:
PROCESSOR_REGION=af-south-1 # pin to your region's stream
ANTHROPIC_API_KEY=... # for TillMind on new issues
GEO_DB_PATH=/path/to/GeoLite2-City.mmdb # MaxMind, optional9. CI/CD with GitHub Actions
name: Deploy
on: { push: { branches: [main] } }
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with: { version: 9 }
- uses: actions/setup-node@v4
with: { node-version: 20, cache: pnpm }
- run: pnpm install
- run: pnpm --filter @tillpulse/db build
- working-directory: apps/web
run: pnpm run deploy:cf
env:
CLOUDFLARE_API_TOKEN: $\{\{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: $\{\{ secrets.CLOUDFLARE_ACCOUNT_ID }}Troubleshooting
Function size exceeds limit
Cloudflare Workers cap script size at 1MB compressed. Drop unused deps from apps/web/package.json; check for accidental TCP-only packages (postgres, ioredis) that should be @neondatabase/serverless + @upstash/redis.
TOTP fails on the edge
Ensure compatibility_flags = ["nodejs_compat"] is in wrangler.toml. otplib needs crypto.createHmac.
Cookies not set
Make sure APP_URL matches the served origin. Cookies are SameSite=Lax; if you're proxying through a different domain (e.g. <your-project>.pages.dev served from your custom domain), the browser may drop them.