tutorial

Uptime Monitoring for AdonisJS Applications with Vigilmon

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,...

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


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

  1. Log in to vigilmon.online and click Add Monitor.
  2. Choose HTTP(S) monitor.
  3. Enter your health URL: https://yourapp.example.com/health
  4. Set the check interval — the free tier supports 5-minute intervals.
  5. Add an alert channel: email, Slack webhook, or a webhook URL.
  6. 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

  1. In Vigilmon, click Add Monitor.
  2. Choose Heartbeat monitor.
  3. Set expected interval to 5 minutes (match your cron frequency).
  4. Copy the unique URL and set it as VIGILMON_HEARTBEAT_URL in 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:

  1. /health controller — checks database, Redis, and storage; returns HTTP 503 when degraded so Vigilmon alerts automatically.
  2. Webhook controller — receives Vigilmon DOWN/UP events with proper TypeScript types and routes them to Slack or PagerDuty.
  3. Ace heartbeat command — pings Vigilmon on each scheduled job execution; silence triggers an alert before users notice.
  4. 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

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →