tutorial

Monitoring Keystone.js with Vigilmon

Keystone is a powerful Node.js/TypeScript headless CMS with a GraphQL API, Prisma ORM, and auto-generated Admin UI — but GraphQL API outages, Admin UI failures, database connectivity issues, and broken scheduled hooks silently disrupt content operations and API consumers. Here's how to monitor Keystone's GraphQL API, Admin UI, database, and scheduled tasks with Vigilmon.

Keystone is a powerful open-source Node.js and TypeScript headless CMS and application framework. It is GraphQL-native, uses Prisma ORM for database access, and includes an auto-generated Admin UI built on Next.js — making it a popular choice for building custom content APIs, application backends, and editorial interfaces. When Keystone's GraphQL API goes down, every API consumer — whether a Next.js frontend, a mobile app, or an automated content pipeline — immediately begins failing. When the Admin UI stops responding, content editors cannot create or update content. When the Prisma database connection drops, Keystone's entire request handling fails. When scheduled task hooks stop firing, automated content processing and timed publications never execute. Vigilmon gives you external monitoring for Keystone's GraphQL API, Admin UI, database connectivity, and scheduled task execution so you catch failures before they surface as API consumer errors or missed content publications.

What You'll Set Up

  • Keystone GraphQL API availability via introspection query
  • Admin UI availability (Next.js-based /_next/static check)
  • Database connectivity via API response verification
  • Scheduled task and automated content processing health via cron heartbeats
  • SSL certificate expiry alerts for HTTPS Keystone deployments

Prerequisites

  • Keystone 6.x running in production (NODE_ENV=production)
  • PostgreSQL or SQLite database accessible from the Keystone host
  • Shell access to the Keystone host
  • A free Vigilmon account

Step 1: Monitor the Keystone GraphQL API

The Keystone GraphQL API at /api/graphql is the single entry point for all data reads and writes — content fetches, mutations, and authentication all go through GraphQL. An introspection query is the lightest possible health check: it asks GraphQL to describe its own schema, which requires the entire Keystone stack (Node.js process, GraphQL layer, Prisma, and database) to be functioning:

  1. In Vigilmon, click Add MonitorHTTP Keyword.
  2. Set the URL to https://your-keystone-domain.com/api/graphql.
  3. Set Method to POST.
  4. Set Request body to {"query": "{ __typename }"}.
  5. Set Request header Content-Type to application/json.
  6. Set Keyword to data (present in a valid GraphQL response).
  7. Set Check interval to 2 minutes.

For a shell-based check (internal or private Keystone instances):

#!/bin/bash
# /usr/local/bin/keystone-graphql-check.sh

HEARTBEAT_URL="https://vigilmon.online/heartbeat/abc123"
KEYSTONE_URL="https://your-keystone-domain.com"
TIMEOUT=15

GQL_RESPONSE=$(curl -s \
  --max-time "$TIMEOUT" \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"query": "{ __typename }"}' \
  "${KEYSTONE_URL}/api/graphql")

HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
  --max-time "$TIMEOUT" \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"query": "{ __typename }"}' \
  "${KEYSTONE_URL}/api/graphql")

if [ "$HTTP_CODE" = "200" ] && echo "$GQL_RESPONSE" | grep -q '"data"'; then
  echo "GraphQL API OK: HTTP 200, data field present"
  curl -s "$HEARTBEAT_URL"
else
  echo "GraphQL API FAILED: HTTP $HTTP_CODE, response: $GQL_RESPONSE"
  exit 1
fi

Install as a cron heartbeat:

chmod +x /usr/local/bin/keystone-graphql-check.sh
(crontab -l 2>/dev/null; echo "*/2 * * * * /usr/local/bin/keystone-graphql-check.sh >> /var/log/keystone-graphql-health.log 2>&1") | crontab -

A failed GraphQL check means all API consumers are broken — set the Vigilmon heartbeat to 3 minutes and alert immediately on the first failure.


Step 2: Monitor the Keystone Admin UI

The Keystone Admin UI is a Next.js application served by the Keystone process alongside the GraphQL API. It can fail independently of the API — a broken Next.js build artifact, a missing static file, or a React hydration error can leave the Admin UI inaccessible while the GraphQL API continues responding. Check for Next.js static asset availability as a proxy for Admin UI health:

  1. In Vigilmon, click Add MonitorHTTP.
  2. Set the URL to https://your-keystone-domain.com/api/graphql (the Admin UI root page).
  3. Set Expected status to 200.
  4. Set Check interval to 5 minutes.

For a more targeted Admin UI check using the Next.js static manifest:

#!/bin/bash
# /usr/local/bin/keystone-admin-check.sh

HEARTBEAT_URL="https://vigilmon.online/heartbeat/def456"
KEYSTONE_URL="https://your-keystone-domain.com"
TIMEOUT=15

# Check the Admin UI root page loads
ADMIN_HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
  --max-time "$TIMEOUT" \
  "${KEYSTONE_URL}")

# Check Next.js static assets are available (confirms build artifacts are intact)
STATIC_HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
  --max-time "$TIMEOUT" \
  "${KEYSTONE_URL}/_next/static/chunks/main.js" 2>/dev/null || echo 000)

# Primary check: Admin UI root returns 200
if [ "$ADMIN_HTTP_CODE" = "200" ]; then
  echo "Admin UI OK: HTTP 200"
  curl -s "$HEARTBEAT_URL"
elif [ "$ADMIN_HTTP_CODE" = "302" ] || [ "$ADMIN_HTTP_CODE" = "301" ]; then
  # Redirect to /signin is normal when not authenticated
  echo "Admin UI OK: redirect to sign-in (HTTP $ADMIN_HTTP_CODE)"
  curl -s "$HEARTBEAT_URL"
else
  echo "Admin UI FAILED: HTTP $ADMIN_HTTP_CODE (static: $STATIC_HTTP_CODE)"
  exit 1
fi
chmod +x /usr/local/bin/keystone-admin-check.sh
(crontab -l 2>/dev/null; echo "*/5 * * * * /usr/local/bin/keystone-admin-check.sh >> /var/log/keystone-admin-health.log 2>&1") | crontab -

Set the Vigilmon heartbeat to 7 minutes. Admin UI failures that persist more than two check cycles indicate a broken Next.js build or a failed Keystone restart after a code deployment.


Step 3: Monitor Database Connectivity via API Response

Keystone uses Prisma as its ORM and requires a live database connection for all GraphQL operations. Unlike a simple database ping, the best way to verify Keystone's database connectivity is via the GraphQL API itself — if the API responds successfully to a data query, Prisma's connection pool is healthy. A failed data query (while the introspection query in Step 1 succeeds) indicates a partial database issue:

#!/bin/bash
# /usr/local/bin/keystone-db-check.sh

HEARTBEAT_URL="https://vigilmon.online/heartbeat/ghi789"
KEYSTONE_URL="https://your-keystone-domain.com"
TIMEOUT=15

# Replace 'User' with a list type that exists in your Keystone schema
# This query fetches 1 record — fast and tests Prisma + DB together
DB_RESPONSE=$(curl -s \
  --max-time "$TIMEOUT" \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"query": "{ users(take: 1) { id } }"}' \
  "${KEYSTONE_URL}/api/graphql")

HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
  --max-time "$TIMEOUT" \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"query": "{ users(take: 1) { id } }"}' \
  "${KEYSTONE_URL}/api/graphql")

if [ "$HTTP_CODE" = "200" ] && echo "$DB_RESPONSE" | grep -q '"data"'; then
  # Check for GraphQL errors that indicate database issues
  if echo "$DB_RESPONSE" | grep -q '"errors"'; then
    ERROR_MSG=$(echo "$DB_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('errors',[{}])[0].get('message','unknown'))" 2>/dev/null)
    echo "GraphQL data query returned errors: $ERROR_MSG"
    exit 1
  fi
  echo "Database connectivity OK via Prisma"
  curl -s "$HEARTBEAT_URL"
else
  echo "Database connectivity check FAILED: HTTP $HTTP_CODE"
  echo "Response: $DB_RESPONSE"
  exit 1
fi

For public Keystone instances where the users query requires authentication, use a publicly accessible list type instead:

# Example: query a public content type
{ posts(take: 1) { id } }

Or query the Keystone system health endpoint if available in your Keystone version.

Install the check:

chmod +x /usr/local/bin/keystone-db-check.sh
(crontab -l 2>/dev/null; echo "*/3 * * * * /usr/local/bin/keystone-db-check.sh >> /var/log/keystone-db-health.log 2>&1") | crontab -

Set the Vigilmon heartbeat to 5 minutes. A failing database check combined with a passing introspection check (from Step 1) means the GraphQL layer is up but Prisma has lost its connection — restart the Keystone process to force a Prisma connection pool reset.


Step 4: Monitor Keystone Scheduled Tasks and Hooks

Keystone supports scheduled content operations via Node.js cron libraries (node-cron, cron, node-schedule) integrated into the Keystone configuration, as well as automated content processing through Keystone hooks (beforeOperation, afterOperation, resolveInput). When the Keystone process restarts without the scheduled task configuration re-registering, or when hook logic fails silently, automated workflows (scheduled content publish, content processing pipelines, notification triggers) break without any alert.

Monitor the Node.js process health to ensure hooks can fire:

#!/bin/bash
# /usr/local/bin/keystone-process-check.sh

HEARTBEAT_URL="https://vigilmon.online/heartbeat/jkl012"
KEYSTONE_PORT="${KEYSTONE_PORT:-3000}"

# Check the Keystone process is running and binding to its port
if pgrep -f "keystone start" > /dev/null || \
   pgrep -f "node.*keystone" > /dev/null || \
   ss -tlnp "sport = :${KEYSTONE_PORT}" 2>/dev/null | grep -q LISTEN; then
  echo "Keystone process running on port $KEYSTONE_PORT"
  curl -s "$HEARTBEAT_URL"
else
  echo "Keystone process not found on port $KEYSTONE_PORT"
  exit 1
fi

For scheduled tasks implemented with node-cron inside Keystone, add a heartbeat ping inside the task itself:

// keystone.ts — example scheduled task with Vigilmon heartbeat

import cron from 'node-cron'

export default config({
  // ... your Keystone config

  extendGraphqlSchema: graphql.extend(base => ({
    // ... your extensions
  })),

  // Run after Keystone connects
  // Use an onConnect extension point or wrap in your server startup
})

// Scheduled content publication — runs every 5 minutes
cron.schedule('*/5 * * * *', async () => {
  try {
    const context = await getContext()

    // Find content items scheduled to publish now
    const now = new Date()
    const toPublish = await context.db.Post.findMany({
      where: {
        publishAt: { lte: now },
        status: { equals: 'scheduled' },
      },
    })

    for (const post of toPublish) {
      await context.db.Post.updateOne({
        where: { id: post.id },
        data: { status: 'published', publishAt: null },
      })
    }

    // Ping Vigilmon heartbeat to confirm the job ran
    await fetch('https://vigilmon.online/heartbeat/mno345').catch(() => {})
    console.log(`Scheduled publish: ${toPublish.length} items published`)
  } catch (err) {
    console.error('Scheduled publish failed:', err)
    // Do NOT ping heartbeat — Vigilmon will alert on missing heartbeat
  }
})

For automated content processing hooks, wrap the hook in a heartbeat ping:

// Schema with hook monitoring

export const Post = list({
  fields: {
    title: text({ validation: { isRequired: true } }),
    content: text(),
    status: select({
      options: ['draft', 'scheduled', 'published'],
      defaultValue: 'draft',
    }),
  },

  hooks: {
    afterOperation: {
      create: async ({ item }) => {
        // Process new content (e.g., generate slug, send notifications)
        try {
          await processNewContent(item)
          // Ping heartbeat to confirm hook pipeline is running
          await fetch('https://vigilmon.online/heartbeat/pqr678').catch(() => {})
        } catch (err) {
          console.error('afterOperation hook failed:', err)
        }
      },
    },
  },
})

Set Vigilmon heartbeat expected intervals to match your cron schedule:

  • Process health check: 3 minutes
  • Scheduled publish job: 7 minutes (5-minute cron with 2-minute buffer)
  • Content processing hook: Depends on your content creation frequency — set to 2x expected creation interval

Step 5: Monitor SSL Certificates

Keystone runs on Node.js, typically behind an Nginx or Caddy reverse proxy that handles TLS termination. An expired SSL certificate causes all API consumers — Next.js frontends, mobile apps, and webhook listeners — to fail with TLS handshake errors. Monitor certificate expiry before renewal deadlines:

  1. In Vigilmon, click Add MonitorSSL Certificate.
  2. Set Domain to your-keystone-domain.com.
  3. Set Alert before expiry to 14 days.

For shell-based SSL monitoring:

#!/bin/bash
# /usr/local/bin/keystone-ssl-check.sh

HEARTBEAT_URL="https://vigilmon.online/heartbeat/stu901"
DOMAIN="your-keystone-domain.com"
MIN_DAYS_REMAINING=14

EXPIRY_DATE=$(echo | openssl s_client -servername "$DOMAIN" \
  -connect "${DOMAIN}:443" 2>/dev/null | \
  openssl x509 -noout -enddate 2>/dev/null | \
  cut -d= -f2)

if [ -z "$EXPIRY_DATE" ]; then
  echo "Could not retrieve SSL certificate for $DOMAIN"
  exit 1
fi

EXPIRY_EPOCH=$(date -d "$EXPIRY_DATE" +%s 2>/dev/null || \
  date -j -f "%b %d %T %Y %Z" "$EXPIRY_DATE" +%s 2>/dev/null)
NOW_EPOCH=$(date +%s)
DAYS_REMAINING=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))

if [ "$DAYS_REMAINING" -ge "$MIN_DAYS_REMAINING" ]; then
  echo "SSL OK: $DAYS_REMAINING days until expiry"
  curl -s "$HEARTBEAT_URL"
else
  echo "SSL EXPIRING SOON: $DAYS_REMAINING days remaining (threshold: $MIN_DAYS_REMAINING)"
  exit 1
fi

Run daily with a 25 hour Vigilmon heartbeat interval.


Step 6: Set Up Alert Channels

Configure Vigilmon to route Keystone alerts where your team will respond:

  1. Go to Alert Channels in Vigilmon.
  2. Add a Slack webhook to #api-alerts for the GraphQL API monitor — GraphQL failures immediately impact all API consumers.
  3. Add PagerDuty for the database connectivity monitor — a lost Prisma connection takes down the entire Keystone stack.
  4. Add email notification for the Admin UI and scheduled task monitors — these are important but rarely require on-call response.
  5. Set Consecutive failures before alert to 1 for the GraphQL API monitor — a single failure means all data fetches are breaking.
  6. Set Consecutive failures before alert to 2 for the Admin UI monitor — Keystone process restarts during deployments cause brief Admin UI unavailability.

Add maintenance windows during Keystone version upgrades or Prisma migration runs:

# Pause monitors during schema migration
curl -X POST https://vigilmon.online/api/maintenance \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{
    "monitor_id": "KEYSTONE_GRAPHQL_MONITOR_ID",
    "duration_minutes": 15,
    "reason": "Keystone schema migration"
  }'

# Run Prisma migration
npx keystone migrate deploy

# Resume monitoring automatically after the maintenance window

Summary

| Monitor | Target | What It Catches | |---|---|---| | HTTP keyword (GraphQL API) | /api/graphql introspection | Node.js crash, GraphQL layer failure, process exit | | Cron heartbeat (Admin UI) | Admin root page | Next.js build failure, Admin UI render error | | Cron heartbeat (database) | GraphQL data query | Prisma connection loss, database crash | | Cron heartbeat (process) | Port check / pgrep | Keystone process death, port binding failure | | In-app heartbeat (scheduled tasks) | node-cron job ping | Scheduler not re-registered after restart | | In-app heartbeat (hooks) | afterOperation ping | Hook pipeline silence, processing gap | | SSL certificate | your-keystone-domain.com | Certificate expiry, Nginx TLS config issue |

Keystone's single-process architecture means a crash takes down both the GraphQL API and the Admin UI simultaneously — but database connection loss and hook failures can occur independently, leaving some functionality intact while others fail silently. Vigilmon's combination of external HTTP checks and in-process heartbeat pings gives you full-stack visibility: you know within minutes whether the problem is a process crash, a database issue, or a silent scheduled task failure.

Start monitoring for free at vigilmon.online.

Monitor your app with Vigilmon

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

Start free →