This document describes how to deploy packrun.dev to Railway with multi-region support.
┌─────────────────────────────────────────────────────────────────────────────┐
│ Cloudflare CDN │
│ (Global edge caching, DDoS protection) │
└─────────────────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────────────────────┐
│ Users │
├────────────────┬────────────────────────┬───────────────────────────────────┤
│ EU Users │ US East Users │ US West / APAC Users │
└───────┬────────┴───────────┬────────────┴──────────────────┬────────────────┘
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Railway │ │ Railway │ │ Railway │
│ Amsterdam │ │ Virginia │ │ California │
│ (web) │ │ (web+api+ │ │ (web) │
│ │ │ worker) │ │ │
└───────┬───────┘ └───────┬───────┘ └───────┬───────┘
│ │ │
└────────────────────┼───────────────────────────────┘
│
▼
┌──────────────────────────────┐
│ API Server │
│ (MCP + REST endpoints) │
│ │
│ ┌────────┐ ┌──────────┐ │
│ │ Redis │ │Typesense │ │
│ │ Cache │ │ Index │ │
│ └────────┘ └──────────┘ │
└──────────────────────────────┘
User Search → Web App → API /api/search → Typesense → Results
│
└──→ (fallback) npm registry → Queue for sync
User Request → Web App (ISR) → API /api/package/:name/health
│
▼
┌────────────────┐
│ Redis Cache │ ← 1 hour TTL
│ (health) │
└───────┬────────┘
│ miss
▼
┌────────────────┐
│ Typesense │ ← Core package data
│ (index) │
└───────┬────────┘
│ miss
▼
┌────────────────┐
│ npm Registry │ ← Fetch + queue for sync
└───────┬────────┘
│
▼
┌────────────────┐
│ Enrichment │
│ (GitHub, │
│ npms.io, │
│ OSV) │
└────────────────┘
┌──────────────────┐ ┌──────────────┐ ┌──────────────┐
│ npm Changes Feed │────▶│ Listener │────▶│ Redis │
│ (CouchDB stream) │ │ (producer) │ │ (BullMQ) │
└──────────────────┘ └──────────────┘ └──────┬───────┘
│
┌───────────────────────┘
│
▼
┌──────────────────┐ ┌──────────────┐ ┌──────────────┐
│ API Server │────▶│ Processor │────▶│ Typesense │
│ (queues on miss) │ │ (worker) │ │ (index) │
└──────────────────┘ └──────────────┘ └──────────────┘
packrun.dev/
├── apps/
│ ├── web/ # Next.js web app (package pages, search UI)
│ ├── api/ # Hono API server (MCP + REST)
│ └── worker/ # BullMQ sync worker (listener + processor)
│
└── packages/
├── clients/ # Shared API clients (npm, GitHub, OSV, npms.io)
├── decisions/ # Package comparison & health scoring logic
├── readme-renderer/ # README HTML rendering
└── typescript-config/ # Shared TypeScript configs
Centralized API clients used by both worker and API:
| Client | Purpose |
|---|---|
@packrun/data/npm |
npm registry metadata & downloads |
@packrun/data/github |
GitHub repo data (stars, issues, commits) |
@packrun/data/osv |
Vulnerability data from OSV |
@packrun/data/npms |
Quality scores from npms.io |
@packrun/data/bundlephobia |
Bundle size metrics |
| Layer | What | TTL | Location |
|---|---|---|---|
| Cloudflare | HTML pages, static assets | 1 hour (stale-while-revalidate: 1 day) | Edge (300+ locations) |
| Next.js ISR | Package pages | 1 hour | Railway (3 regions) |
| In-memory LRU | Health data, scores, GitHub data | 1 hour - 7 days | API instances (3 regions) |
| Typesense | Search index | Real-time sync | Cloud SDN (3 regions) |
| Key Pattern | Data | TTL |
|---|---|---|
pkg:{name}:health |
Full health response | 1 hour |
pkg:{name}:scores |
npms.io scores | 7 days |
pkg:{name}:github |
GitHub repo data | 1 day |
pkg:{name}:details |
Enriched package details | 1 day |
pkg:{name}:security |
Security signals | 1 day |
pkg:{name}:trend |
Download trend analysis | 1 day |
queued:{name} |
Queue deduplication flag | 5 minutes |
Note: Cache is per-instance (not shared across regions) and cleared on restart.
- Railway Pro Plan - Required for multi-region replicas
- Typesense Cloud Account - With SDN (Search Delivery Network) enabled
- Cloudflare Account - Free tier is sufficient
- GitHub Repository - Connected to Railway
- Go to Typesense Cloud Dashboard
- Select your cluster or create a new one
- Enable Search Delivery Network (SDN)
- Select 3 regions:
- Frankfurt (EU)
- N. Virginia (US East)
- Oregon (US West)
- Note down your API keys:
- Admin API Key (for server-side operations)
- Search-only API Key (for client-side, safe to expose)
- Note the Nearest Node hostname (e.g.,
xxx.a1.typesense.net)
packrun (Project)
├── web (Service) - 3 replicas (Amsterdam, Virginia, California)
├── api (Service) - 3 replicas (Amsterdam, Virginia, California) [in-memory cache]
├── listener (Service) - npm changes listener (producer)
├── processor (Service) - job processor (worker)
└── redis-queue (Database) - BullMQ queue (central, US East)
┌─────────────────────────────────────────────────────────────────┐
│ Web (3 regions) │
│ Amsterdam Virginia California │
└───────────────┬──────────────┬──────────────┬───────────────────┘
│ │ │
▼ ▼ ▼
┌───────────────────┐ ┌───────────────┐ ┌───────────────────┐
│ API (Amsterdam) │ │ API (Virginia)│ │ API (California) │
│ [in-memory LRU] │ │[in-memory LRU]│ │ [in-memory LRU] │
└─────────┬─────────┘ └───────┬───────┘ └─────────┬─────────┘
│ │ │
└───────────────────┼───────────────────┘
│ (queue only, async)
▼
┌───────────────────────────────────┐
│ Virginia (US East) │
│ ┌─────────────┐ ┌──────────┐ │
│ │ Queue Redis │◀──│ Worker │ │
│ │(redis-queue)│ │ │ │
│ └─────────────┘ └──────────┘ │
└───────────────────────────────────┘
Key points:
- In-memory LRU cache - Each API instance has fast local cache (~0ms)
- Queue Redis (1x) - Central BullMQ queue in US East
- Queue operations are async/non-blocking, so cross-region latency doesn't affect response time
- Cache is cleared on restart (acceptable - data refetched from Typesense/npm)
Railway auto-detects JavaScript monorepos. When you import the repo:
- Go to Railway Dashboard
- Select Deploy from GitHub repo
- Choose the
packrun.devrepository - Railway will auto-detect services from
railway.jsonfiles - Click Deploy to create all services
If auto-import doesn't work:
- Create an empty project
- Add four services from the same repo:
- Web:
/apps/web/railway.json - API:
/apps/api/railway.json - Listener:
/apps/worker/railway.listener.json - Processor:
/apps/worker/railway.processor.json
- Web:
- Add one Redis database:
redis-queue(Virginia region) - BullMQ queue
- Configure environment variables (see below)
| Variable | Description |
|---|---|
TYPESENSE_API_KEY |
Admin API key from Typesense Cloud |
TYPESENSE_HOST |
Typesense Cloud nearest node host |
NEXT_PUBLIC_TYPESENSE_SEARCH_API_KEY |
Search-only API key (public) |
NEXT_PUBLIC_API_URL |
API server URL (e.g., https://api.packrun.dev) |
REVALIDATE_TOKEN |
Secret token for on-demand ISR cache revalidation |
| Variable | Description |
|---|---|
PORT |
Port to listen on (default: 3001) |
REDIS_URL |
Queue Redis URL (${{redis-queue.REDIS_URL}}) |
TYPESENSE_API_KEY |
Admin API key from Typesense Cloud |
TYPESENSE_HOST |
Typesense Cloud nearest node host |
Note: API uses in-memory LRU cache. Redis is only used for the BullMQ queue.
| Variable | Description |
|---|---|
TYPESENSE_API_KEY |
Admin API key from Typesense Cloud |
TYPESENSE_HOST |
Typesense Cloud nearest node host |
REDIS_URL |
Queue Redis URL (${{redis-queue.REDIS_URL}}) |
WEB_URL |
(Optional) Web app URL for auto-revalidation |
REVALIDATE_TOKEN |
(Optional) Token for triggering ISR revalidation |
- Go to Cloudflare Dashboard
- Add your domain (e.g.,
packrun.dev) - Update nameservers at your registrar
| Record | Name | Target | Proxy |
|---|---|---|---|
| CNAME | @ |
your-web-app.up.railway.app |
Proxied |
| CNAME | api |
your-api-app.up.railway.app |
Proxied |
| CNAME | mcp |
your-api-app.up.railway.app |
DNS Only (gray cloud) |
Note: The mcp subdomain must be DNS-only (not proxied) to bypass Cloudflare's 100-second SSE timeout. See apps/api/CLOUDFLARE_MCP_FIX.md for details.
| Setting | Value |
|---|---|
| SSL/TLS | Full (strict) |
| Always Use HTTPS | On |
| Auto Minify | JS, CSS, HTML |
| Brotli | On |
| Browser Cache TTL | Respect Existing Headers |
- In Railway, go to Web service → Settings → Networking
- Click Add Custom Domain
- Enter your domain (e.g.,
packrun.dev) - For API service, add two custom domains:
api.packrun.dev(for REST API endpoints with Cloudflare caching)mcp.packrun.dev(for MCP endpoint, bypasses Cloudflare to avoid SSE timeout)
curl https://packrun.dev/api/healthExpected:
{
"status": "healthy",
"timestamp": "2026-02-02T12:00:00.000Z",
"region": "europe-west4"
}curl https://api.packrun.dev/healthExpected:
{
"status": "healthy",
"timestamp": "2026-02-02T12:00:00.000Z"
}curl https://api.packrun.dev/api/package/react/health | jq '.health'Expected:
{
"score": 95,
"grade": "A",
"status": "active",
"signals": {
"positive": ["Active development", "High downloads"],
"negative": [],
"warnings": []
}
}curl -I https://packrun.dev/reactLook for:
cache-control: public, s-maxage=3600, stale-while-revalidate=86400
cf-cache-status: HIT
- Request count and bandwidth
- Cache hit ratio (target: >80%)
- Threats blocked
- Performance metrics by region
- CPU and Memory usage per service
- Request count and latency
- Logs from all regions
- Search requests per region
- Latency by region
- Index size and document count
- Queue stats (waiting, active, failed jobs)
- Cache hit/miss ratio
- Memory usage
- $20/month base
- Usage-based pricing for compute
- ~$15-30/month for 4 services + Redis
- Pricing varies by cluster size
- SDN adds ~30% premium for 3-region setup
- Check Typesense Pricing
- Free tier is sufficient for most use cases
- Pro ($20/month) for advanced features
- Check if the app builds successfully
- Verify environment variables are set
- Check deployment logs for errors
- Verify
REDIS_URLis set and Redis is running - Check
TYPESENSE_API_KEYandTYPESENSE_HOST - Check API logs for connection errors
- Verify Redis connection is working
- Check processor logs for errors
- Verify Typesense credentials
- Check queue stats:
[Stats] Sync: X waiting, Y active, Z failed
| Issue | Solution |
|---|---|
| Jobs stuck in waiting | Check if processor is running |
| Many failed jobs | Check processor logs for error details |
| Redis connection errors | Verify Redis URL and service status |
| Deduplication not working | Check queued:{name} keys in Redis |
- Check Cloudflare cache hit ratio (should be >80%)
- Verify requests route to nearest region
- Check Redis cache hit ratio
- Ensure Typesense uses
nearestNodeoption
- Check
Cache-Controlheaders in response - Verify Cloudflare proxy is enabled (orange cloud)
- Check for
Set-Cookieheaders (prevents caching) - Use
cf-cache-statusheader to debug