Web apps
Apps on Vercel, Netlify, or Cloudflare Pages
Check an app on Vercel, Netlify, or Cloudflare Pages from outside, mark its deploys, and watch its scheduled jobs.
kogut is a long-running process with a persistent disk, so it cannot run on these platforms. Run it on a small VPS, Railway, or Render, at another provider than your app; see Where to run kogut. On Render, the disk needs a paid instance type, such as Starter. kogut edge runs on Cloudflare’s free Workers plan.
Add a health route
A check of your home page only shows that the CDN answers. A health route shows that the functions run and reach the database. In a Next.js App Router project:
import { db } from "@/lib/db";
export const dynamic = "force-dynamic";
export async function GET() {
try {
await db.query("select 1");
return Response.json({ db: "ok" });
} catch {
return Response.json({ db: "error" }, { status: 500 });
}
}force-dynamic keeps Next.js and the CDN from serving a cached answer. Replace
db.query with your database client’s simplest query.
Check the app from outside
Check the production domain, not a preview URL, which changes with every branch and can sit behind deployment protection:
[[service]]
name = "shop"
[[check]]
name = "Shop health"
service = "shop"
url = "https://shop.acme.com/api/health"
every = "1m"
expect = [
{ status = "2xx" },
{ json = "$.db", equals = "ok" },
{ response_time_below = "2s", else = "degraded" },
]else = "degraded" marks a slow cold start as degraded instead of down. A check every
minute calls the function about 43,200 times in 30 days. See
Checks.
Mark deploys
Choose one way:
-
GitHub webhook with
deployment_status. Vercel and other platforms that report deploys to GitHub send this event; kogut records production deployments and skips previews. See Deployments. -
GitHub webhook with
push. When the platform deploys every push to the default branch. The marker shows when the push arrives, a few minutes before the new version is live. -
The deploy API from CI. When a CI job deploys with the platform’s command-line tool, call the deploy API after the production deploy finishes:
curl -fsS -X POST https://kogut.acme.com/api/deploys \ -H "Authorization: Bearer $KOGUT_TOKEN" \ -d service=shop -d version="$GITHUB_SHA"
Watch scheduled jobs
Create a heartbeat with the job’s schedule and store its
ping URL as a secret named
KOGUT_PING_URL. The schedules below use UTC, so leave the heartbeat’s time zone at UTC.
Set the grace period to cover the job’s runtime and any delay in starting it.
Vercel Cron
Vercel calls the route on the schedule in vercel.json. Add a CRON_SECRET environment
variable of 16 or more random characters. Vercel sends it as a bearer token, and the
route refuses other callers:
{ "crons": [{ "path": "/api/cron/cleanup", "schedule": "0 3 * * *" }] }import { cleanup } from "@/lib/cleanup";
export const dynamic = "force-dynamic";
const ping = (path = "") =>
fetch(`${process.env.KOGUT_PING_URL}${path}`).catch(() => undefined);
export async function GET(request: Request) {
const secret = process.env.CRON_SECRET;
if (!secret || request.headers.get("authorization") !== `Bearer ${secret}`) {
return new Response("Unauthorized", { status: 401 });
}
await ping("/start");
try {
await cleanup();
} catch (error) {
await ping("/fail");
throw error;
}
await ping();
return new Response("OK");
}Supabase pg_cron
Store the ping URL in Vault, then schedule the job with a pg_net request after its
statements:
select vault.create_secret('https://kogut.acme.com/ping/btp_...', 'kogut_ping_url');
select cron.schedule(
'nightly-cleanup',
'0 3 * * *',
$$
delete from sessions where expires_at < now();
select net.http_get(
(select decrypted_secret from vault.decrypted_secrets where name = 'kogut_ping_url')
);
$$
);The statements run in one transaction, so the ping is sent only when the delete succeeds.
Cloudflare Workers
Add the schedule under [triggers] with crons = ["0 3 * * *"] in the Worker’s
Wrangler configuration, and store the URL with npx wrangler secret put KOGUT_PING_URL:
import { cleanup } from "./cleanup.js";
export default {
async scheduled(controller, env, ctx) {
ctx.waitUntil(run(env));
},
};
async function run(env) {
const ping = (path = "") =>
fetch(`${env.KOGUT_PING_URL}${path}`).catch(() => undefined);
await ping("/start");
try {
await cleanup(env);
} catch (error) {
await ping("/fail");
throw error;
}
await ping();
}Errors and visits in the browser
Add the tracking script, which has a Next.js snippet.
What kogut does not see
kogut does not read function logs, build logs, or the platform’s metrics, and changes nothing on the platform. There is no agent to install there.