Vercel Controller: a Node.js microservice for Vercel cache and deployments
Express 4 service with async job queue, smart deduplication, LRU cache, Helmet.js security, and 129 tests — called by Travel Panel whenever content changes
Overview
Vercel Controller is a Node.js microservice (Express 4) that wraps the Vercel API behind a stable interface. It is called by the Travel Panel core platform whenever content changes: a new offer published, a hotel updated, an inventory adjustment, or a brand-level config change. The controller takes that signal, purges CloudFront cache for the affected paths, triggers a redeploy of the dependent Vercel frontends, and reports status back through an async job API.
It exists because operating a production front-end on Vercel at scale eventually means you need more than the Vercel dashboard. You need to trigger cache purges and deploys from CI, from webhooks, from operators, and from other services in the platform. You need to do it without tripping Vercel's rate limits or duplicating work. And you need to know whether the operation actually landed.
What it solves
Manual Vercel operations. Click, wait, click again. Bad for ops, invisible to automation.
Rate limit pain. Vercel imposes a 60-second interval on purge operations. Naive scripts that fire off multiple purges in a row fail silently or get throttled. The controller serializes operations behind a queue that respects the interval.
Duplicate jobs. If two systems trigger a cache purge for the same project within the same minute, you waste one call and risk inconsistency. Smart deduplication detects an in-flight job for the same project + operation key and attaches new requests to it instead of queuing a duplicate.
Visibility. Knowing whether an async operation finished and whether it succeeded requires polling, and polling requires a stable interface.
API endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /health |
Health check with full stats (no auth) |
| GET | /projects |
List all team projects (cached) |
| POST | /purge/{project} |
Purge cache (async by default) |
| POST | /purge-all |
Purge cache for all projects |
| POST | /redeploy/{project} |
Redeploy a project (async by default) |
| GET | /jobs |
List recent jobs |
| GET | /jobs/{jobId} |
Get job status / result |
Three purge types supported: all cache, data cache only, or CDN only. Redeploys target production or preview by default.
Health check output
The health endpoint returns everything an operator or monitoring system needs:
{
"status": "ok",
"service": "vercel-controller",
"version": "1.3.5",
"uptime": 3600,
"memory": {"used": "45MB", "total": "128MB"},
"config": {
"team": "moon-holidays",
"auth": "enabled",
"swagger": "/docs",
"rateLimit": "100 req/60s"
},
"cache": {
"size": 5,
"hits": 150,
"misses": 20,
"hitRate": "88%"
},
"queue": {
"pending": 2,
"processing": 1,
"completed": 45,
"failed": 0
}
}
Security model
Every endpoint except /health and /docs requires Bearer token authentication. The token comparison is timing-safe to prevent side-channel leakage. Beyond auth:
- Helmet.js 7 for security headers (CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy)
- express-rate-limit 7 with configurable per-IP limits (default: 100 req/min)
- CORS with explicit allowed-origin list
- Strict input validation on every parameter (project ID, purge type, deploy target, IDs)
- Request size limits of 10 KB max JSON payload to prevent oversize DoS
- Startup validation: the service fails fast if
VERCEL_TOKEN,VERCEL_TEAM_SLUG, orVERCEL_ORG_IDare missing — no running without credentials - Graceful shutdown on SIGTERM/SIGINT so in-flight requests complete and no queued jobs get stranded
- Type confusion prevention in the validation layer
Testing
129 tests in total, organized by concern:
- Validation functions (projectId, purge type, deploy target, secure compare)
- API endpoints (health, auth, purge, redeploy, jobs, cache)
- Queue service (enqueueing, deduplication, smart keys, stats)
- Cache service (TTL expiration, LRU eviction, stats)
- Job rate limiter (client isolation, window reset)
- CORS wildcard support
- Security (headers, error handling, type confusion prevention)
The jest config enforces 50% minimum coverage on branches, functions, lines, and statements as a floor, with the actual numbers higher across the critical modules.
Docker + auto-healing
Two docker-compose configurations:
| Feature | docker-compose.yml |
docker-compose.dev.yml |
|---|---|---|
| Swagger UI | disabled | /docs |
| Autoheal | enabled | disabled |
| Image size | lighter | larger (swagger-ui-express) |
| Restart policy | always | unless-stopped |
Production includes autoheal: a sidecar monitors container health every 30 seconds and automatically restarts unhealthy containers. Zero-touch recovery for the common failure modes.
How Travel Panel uses it
When a Travel Panel operator publishes a new offer, updates a hotel listing, or adjusts inventory, the FastAPI backend makes an authenticated POST to Vercel Controller. Vercel Controller enqueues a purge + redeploy job for the affected frontend projects (TravelOffer, B2B portal, operator portal if its static bundle needs a refresh), deduplicates against any in-flight job, and returns a job ID immediately.
Travel Panel does not wait. The background job flushes CloudFront and triggers the Vercel deploys on its own cadence, respecting the 60-second purge interval and never doubling up work. Operators see a banner when a cache refresh is in progress and another when it lands.
This is the quietest piece of infrastructure in the whole Moon Holidays stack. You do not notice it unless it is missing.
Tech stack
Node.js, Express 4.18, Helmet.js 7, express-rate-limit 7, CORS, Swagger UI Express (dev only), in-memory LRU cache, in-memory job queue, Jest 29 with Supertest, Docker, Docker Compose with autoheal sidecar.
Version: 1.3.5. Port: 3500. License: Proprietary — Copyright © 2026 George Khananaev. All rights reserved.
Takeaway
Wrapping a third-party API behind a service you own pays off the moment you need to add deduplication, rate limiting, or observability. Vercel Controller started as a small script and grew into the right place to solve every Vercel-operations problem for the whole platform. Small, focused services like this are my favorite kind of infrastructure: they are easy to reason about, easy to replace, and they earn their keep every time someone skips a manual click.
For other small, focused utilities I've open-sourced, see py-image-compressor (batch image optimization CLI) and FastAPI DocShield (HTTP Basic auth for docs endpoints). Same philosophy, different problems.
