AdonisJS gives you a full-stack MVC framework with first-class TypeScript support. But when your app goes down in production — a failing database connection, a crashed Ace command, or a silent memory leak — how fast do you find out?
This tutorial covers production uptime monitoring for AdonisJS applications using Vigilmon. We will walk through:
- A health check route using AdonisJS routes and controllers
- Vigilmon HTTP monitoring and alert setup
- Webhook delivery to Slack or PagerDuty
- Heartbeat monitoring for Ace-based background jobs
Prerequisites
- Node.js 18+
- AdonisJS v6 application
- A free account at vigilmon.online
Part 1: Add a health check route
AdonisJS follows the MVC pattern, so we create a dedicated controller and register a route.
Generate the controller
node ace make:controller HealthController --singular
Implement the controller
// app/controllers/health_controller.ts
import type { HttpContext } from '@adonisjs/core/http'
import db from '@adonisjs/lucid/services/db'
import redis from '@adonisjs/redis/services/main'
export default class HealthController {
async show({ response }: HttpContext) {
const checks: Record<string, string> = {}
let status = 'ok'
// Database check
try {
await db.rawQuery('SELECT 1')
checks.database = 'ok'
} catch (error) {
checks.database = `error: ${error.message}`
status = 'degraded'
}
// Redis check (if installed)
try {
await redis.ping()
checks.redis = 'ok'
} catch (error) {
checks.redis = `error: ${error.message}`
status = 'degraded'
}
const httpStatus = status === 'ok' ? 200 : 503
return response.status(httpStatus).send({
status,
timestamp: new Date().toISOString(),
uptime: process.uptime(),
environment: process.env.NODE_ENV,
checks,
})
}
}
Register the route
// start/routes.ts
import router from '@adonisjs/core/services/router'
// Health check — no auth middleware
router.get('/health', [() => import('#controllers/health_controller'), 'show'])
If you apply global auth middleware, exclude the health route:
// start/kernel.ts
import router from '@adonisjs/core/services/router'
import { middleware } from './kernel.js'
router.use([middleware.session()])
// Health check excludes auth
router.get('/health', [() => import('#controllers/health_controller'), 'show'])
// All other routes require auth
router
.group(() => {
// your protected routes
})
.use(middleware.auth())
Test it:
curl http://localhost:3333/health
{
"status": "ok",
"timestamp": "2026-06-29T08:00:00.000Z",
"uptime": 120.3,
"environment": "production",
"checks": {
"database": "ok",
"redis": "ok"
}
}
A degraded dependency returns HTTP 503, which triggers a Vigilmon alert immediately.
Add a Drive check (optional)
If your application uses AdonisJS Drive for file storage:
import drive from '@adonisjs/drive/services/main'
try {
// Attempt a lightweight write and delete
const testKey = '.vigilmon-health-probe'
await drive.use().put(testKey, 'ok')
await drive.use().delete(testKey)
checks.storage = 'ok'
} catch (error) {
checks.storage = `error: ${error.message}`
status = 'degraded'
}
Part 2: Set up HTTP monitoring in Vigilmon
- Log in to vigilmon.online and click Add Monitor.
- Choose HTTP(S) monitor.
- Enter your health URL:
https://yourapp.example.com/health - Set the check interval — the free tier supports 5-minute intervals.
- Add an alert channel: email, Slack webhook, or a webhook URL.
- Click Save.
Vigilmon performs checks from multiple geographic regions. It requires consensus from a majority of regions before firing an alert, which eliminates false positives from single-region network blips. This is the main differentiator from single-location uptime checkers.
Verify the check works
After saving, trigger a test by clicking Check now in the Vigilmon dashboard. You should see a green result with response time. If you see a timeout, verify:
- Your app is publicly accessible (not localhost or private IP)
- The health route has no authentication requirement
- Your firewall allows inbound HTTPS from external IPs
Part 3: Webhook alerts
Create the webhook controller
node ace make:controller WebhooksController --singular
// app/controllers/webhooks_controller.ts
import type { HttpContext } from '@adonisjs/core/http'
import logger from '@adonisjs/core/services/logger'
import env from '#start/env'
interface VigilmonPayload {
monitor_id: string
monitor_name: string
status: 'up' | 'down'
url: string
response_code?: number
response_time_ms?: number
checked_at: string
}
export default class WebhooksController {
async vigilmon({ request, response }: HttpContext) {
const payload = request.body() as VigilmonPayload
if (payload.status === 'down') {
logger.error({ payload }, 'Vigilmon: monitor DOWN')
await this.notifyOnCall(payload)
} else {
logger.info({ monitor: payload.monitor_name }, 'Vigilmon: monitor recovered')
}
return response.noContent()
}
private async notifyOnCall(payload: VigilmonPayload) {
const slackUrl = env.get('SLACK_WEBHOOK_URL', '')
if (!slackUrl) return
await fetch(slackUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: [
`*ALERT*: ${payload.monitor_name} is DOWN`,
`URL: ${payload.url}`,
`HTTP: ${payload.response_code ?? 'timeout'}`,
`At: ${payload.checked_at}`,
].join('\n'),
}),
})
}
}
Register the webhook route:
// start/routes.ts
router.post('/webhooks/vigilmon', [
() => import('#controllers/webhooks_controller'),
'vigilmon',
])
In Vigilmon, add a webhook alert channel pointing to https://yourapp.example.com/webhooks/vigilmon.
Vigilmon's webhook payload:
{
"monitor_id": "mon_abc123",
"monitor_name": "AdonisJS API /health",
"status": "down",
"url": "https://yourapp.example.com/health",
"checked_at": "2026-06-29T08:01:00Z",
"response_code": 503,
"response_time_ms": 1205
}
Part 4: Heartbeat monitoring for Ace commands
AdonisJS Ace commands are the standard way to run scheduled jobs. HTTP monitoring will not detect a failed cron runner. Heartbeat monitoring closes that gap.
Create a heartbeat Ace command
node ace make:command SendHeartbeat
// commands/send_heartbeat.ts
import { BaseCommand } from '@adonisjs/core/ace'
import { CommandOptions } from '@adonisjs/core/types/ace'
import env from '#start/env'
export default class SendHeartbeat extends BaseCommand {
static commandName = 'heartbeat:send'
static description = 'Ping the Vigilmon heartbeat URL to confirm the scheduler is alive'
static options: CommandOptions = {
startApp: false,
}
async run() {
const url = env.get('VIGILMON_HEARTBEAT_URL', '')
if (!url) {
this.logger.warning('VIGILMON_HEARTBEAT_URL not set — skipping heartbeat')
return
}
try {
const res = await fetch(url, { signal: AbortSignal.timeout(10_000) })
if (res.ok) {
this.logger.success('Heartbeat sent')
} else {
this.logger.error(`Heartbeat ping returned ${res.status}`)
}
} catch (error) {
this.logger.error(`Heartbeat ping failed: ${error.message}`)
}
}
}
Wire into the scheduler
If you use AdonisJS Scheduler (@adonisjs/scheduler):
// start/scheduler.ts
import scheduler from '@adonisjs/scheduler/services/main'
scheduler.call(async () => {
// Your job logic
await processEmailQueue()
// Ping heartbeat only on success
const heartbeatUrl = process.env.VIGILMON_HEARTBEAT_URL
if (heartbeatUrl) {
await fetch(heartbeatUrl).catch(() => {})
}
}).everyFiveMinutes()
Alternatively, invoke the Ace command from a system cron:
# crontab -e
*/5 * * * * cd /var/www/myapp && node ace heartbeat:send >> /var/log/heartbeat.log 2>&1
Create the heartbeat monitor in Vigilmon
- In Vigilmon, click Add Monitor.
- Choose Heartbeat monitor.
- Set expected interval to 5 minutes (match your cron frequency).
- Copy the unique URL and set it as
VIGILMON_HEARTBEAT_URLin your.env.
Vigilmon alerts you if no ping arrives within the expected window, which catches a dead cron, a failed database migration that prevented the app from starting, or an unhandled exception that killed the Ace process.
Part 5: Environment configuration
Add these variables to your AdonisJS .env (and .env.example for documentation):
# .env
VIGILMON_HEARTBEAT_URL=https://vigilmon.online/heartbeat/abc123xyz
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T.../B.../...
Declare them in the env validation file so AdonisJS validates them at startup:
// start/env.ts
import { Env } from '@adonisjs/core/env'
export default await Env.create(new URL('../', import.meta.url), {
// ... existing vars
VIGILMON_HEARTBEAT_URL: Env.schema.string.optional(),
SLACK_WEBHOOK_URL: Env.schema.string.optional(),
})
Using optional() means the app starts without these variables in development, but the webhook and heartbeat features gracefully no-op.
Summary
Your AdonisJS application now has multi-layer production monitoring:
/healthcontroller — checks database, Redis, and storage; returns HTTP 503 when degraded so Vigilmon alerts automatically.- Webhook controller — receives Vigilmon DOWN/UP events with proper TypeScript types and routes them to Slack or PagerDuty.
- Ace heartbeat command — pings Vigilmon on each scheduled job execution; silence triggers an alert before users notice.
- Multi-region consensus — Vigilmon eliminates false positives by requiring agreement from multiple check regions.
Vigilmon's free tier covers up to 5 monitors with 5-minute check intervals — enough to protect your web tier, your scheduler, and a background worker from day one.
Monitor your AdonisJS app free at vigilmon.online
#adonisjs #javascript #monitoring #devops