Self-hosted uptime and health monitor for personal/portfolio projects on free-tier hosts (Render, Railway, Fly.io, Vercel, etc.).
Free-tier hosts spin down idle services, causing slow cold starts or apparent downtime when someone opens a live demo. Upkeep periodically pings each project's health endpoint, keeps services warm, and shows a single dashboard with the status of everything — no more manually checking N different host dashboards.
- Backend: Supabase (Postgres, Edge Functions,
pg_cronscheduled triggers) - Frontend: Next.js dashboard, deployable independently on Vercel/Netlify/etc.
Early development. See docs/PRD.md for the full product spec.
- Health-check endpoint contract — exactly what your project's health endpoint needs to return to be classified correctly (status codes, timeouts, response-time thresholds, and the optional body/JSON/TCP/DNS/SSL check types).
- Example health-check endpoints — minimal, verified-working health-check snippets for Express, FastAPI, and Next.js, satisfying the contract above as-is.
- Outgoing webhook payload contract — the exact
JSON Upkeep POSTs to a
webhook-type notification channel on incident open/resolve. - Adding a notification channel — for contributors: exactly what adding a new channel type (Slack, SMS, etc.) touches, verified by actually doing it as a proof-of-concept.
- Adding a check type — for contributors: the same audit applied to check types (e.g. WebSocket, ping), including the plugin-architecture refactor this required.
Everything below gets you a working local instance — a Next.js dashboard talking to your own Supabase project, with the prober actually monitoring a test project you add — using nothing but this repo and a free Supabase account. Deploying the dashboard itself to Vercel/Netlify afterward is a separate, ordinary Next.js deploy (same env vars as below); this section is about standing up the Supabase backend, which every deployment target shares.
- Node.js 20.9+
- pnpm (via
corepack enableornpm i -g pnpm) - A free Supabase account and project — required for real self-hosting
(auth, the database, and every Edge Function below all depend on it). The dashboard's marketing
pages will still boot without one (
hasEnvVarsinsrc/lib/utils.tsno-ops the auth gate), but sign-in and monitoring won't work until you've completed the setup below. - (Optional) A free Resend account, only if you want email notifications.
git clone https://github.com/hasnaintypes/upkeep-app.git
cd upkeep-app
pnpm install
pnpm supabase login # opens your browser once, to authenticate the CLI -- can't be scripted
pnpm setup # guided wizard: env vars, link, schema, Edge Functions, cron secrets
pnpm dev # http://localhost:3000pnpm setup (scripts/setup.mjs) is a guided wizard covering every step in
Manual setup below: it writes .env.local, links your Supabase project, applies
every migration, regenerates the typed schema, deploys all five Edge Functions, and creates the
pg_cron secrets they share. It's safe to re-run — every step checks first or is naturally
idempotent, so stopping partway through (or running it again after a schema change) just picks up
wherever it left off. Read Manual setup if you'd rather run each step yourself,
want to understand exactly what the script does, or need to finish a step it can't complete
headlessly (supabase login's browser flow, and generating key values themselves, which are
dashboard-only either way).
-
Clone and install dependencies.
git clone https://github.com/hasnaintypes/upkeep-app.git cd upkeep-app pnpm install -
Create a Supabase project (free tier is fine) at supabase.com, then authenticate and link the CLI to it — the CLI is a pinned
devDependency, so always run it viapnpm supabase ..., never a globally installed version, to keep everyone on the same CLI version:pnpm supabase login # stores an access token in the CLI's global config, never in this repo pnpm supabase link --project-ref <ref> # <ref> is the project ref from your dashboard URL
linkonly needs to be run once per machine. -
Set your environment variables. Copy
.env.exampleto.env.local:cp .env.example .env.local
then fill in every value using the table in Environment variables below — all four come from your Supabase project's dashboard (Settings → API / Settings → API Keys), nothing needs inventing.
-
Apply the database schema. The schema lives as code in
supabase/migrations/— never hand-author tables in the Supabase dashboard:pnpm supabase db push --dry-run # preview which migrations would apply pnpm supabase db push # apply them pnpm gen:types # regenerate src/lib/supabase/types.ts from the live schema
createClient()insrc/lib/supabase/client.ts/server.tsare generic over the generatedDatabasetype — always re-rungen:typesafter a schema change, and commit the result alongside it. -
Deploy the Edge Functions. The prober backend lives under
supabase/functions/(Deno runtime) — five functions, all already scaffolded and committed as code:Function Job Schedule proberRuns every due project's health check Every 1 minute notifierSends incident-open/resolve alerts Every 1 minute digestDaily/weekly portfolio-health email 08:00 UTC daily / Mondays rollupRolls raw checks into hourly/daily aggregates Hourly at :05, daily at 00:10 UTC pruneDeletes raw checks once they're safely aggregated Daily at 00:20 UTC pnpm supabase functions deploy prober --use-api pnpm supabase functions deploy notifier --use-api pnpm supabase functions deploy digest --use-api pnpm supabase functions deploy rollup --use-api pnpm supabase functions deploy prune --use-api
No Docker/local Supabase stack needed for any of this — every command targets the linked hosted project directly.
-
Authenticate the cron jobs. Every schedule above is already committed as a
pg_cronmigration (e.g.schedule_prober_cron) — no dashboard setup needed for the schedules themselves. The one thing that genuinely can't be committed is the secret value each job authenticates with, so it's a one-time manual step afterdb push:pnpm supabase db query --linked "select vault.create_secret('https://<project-ref>.supabase.co', 'project_url');" pnpm supabase db query --linked "select vault.create_secret('<your SUPABASE_SECRET_KEY value>', 'prober_secret_key');"
Until both secrets exist, every cron job fires on schedule but fails harmlessly (visible via
select * from cron.job_run_details order by start_time desc limit 5;) since it can't build a valid request URL/header — no projects are checked, no notifications sent, until this is done. -
(Optional) Email notifications. See Email notifications (Resend) below — skip this if you only want Discord/webhook alerts.
-
Start the app.
pnpm dev # http://localhost:3000
Copy .env.example to .env.local and set:
| Variable | Where to find it | Required? |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Project Settings → API → Project URL | Yes — auth/dashboard queries throw without it |
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY |
Same page, Publishable key | Yes, same as above |
SUPABASE_SERVICE_ROLE_KEY |
Same page, service_role secret |
Only for POST /api/projects/register (programmatic project registration via a per-user API key from /dashboard/api-keys) and other trusted server code bypassing RLS |
SUPABASE_SECRET_KEY |
Settings → API Keys → Secret keys (generate one if your project only shows the legacy service_role key) |
Only for invoking auth: "secret" Edge Functions directly — the "run check now" Server Action, or manual testing. The same value also goes into the prober_secret_key vault secret in Manual setup step 6 |
If NEXT_PUBLIC_SUPABASE_URL/NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY are left unset, Supabase auth
silently no-ops (hasEnvVars in src/lib/utils.ts) — the marketing pages still boot, but sign
in/up and every protected route won't function. SUPABASE_SERVICE_ROLE_KEY/SUPABASE_SECRET_KEY
are never exposed to the client — do not prefix either with NEXT_PUBLIC_.
RESEND_API_KEY/RESEND_FROM_ADDRESS are not .env variables — they're Edge Function
secrets set via pnpm supabase secrets set ..., a separate store entirely; see
Email notifications (Resend).
- Sign up for an account at
http://localhost:3000/auth/sign-up. - Add a project (dashboard → Add project) pointing
health_urlat any endpoint returning200quickly — one of the example snippets run locally, or any real deployed service. - Either wait up to a minute for the
probercron tick to pick it up, or open the project and click Run check now for an immediate result (rate-limited to once every 30 seconds per project). - The project's status badge should flip to Up, and the check log on its detail page shows the request that just ran. If it doesn't, re-check step 6 — a missing vault secret is the most common reason a fresh instance's prober silently does nothing.
pnpm supabase migration new <name> # create a new migration -- the pattern for every schema change
pnpm supabase db push --dry-run # preview which migrations would apply
pnpm supabase db push # apply committed migrations to the linked project
pnpm gen:types # regenerate src/lib/supabase/types.ts after any schema changeTo verify a new SQL function/query without a local Postgres instance, run it directly against the
linked database: pnpm supabase db query --linked "<sql>".
pnpm supabase functions new <name> # scaffolds supabase/functions/<name>/
pnpm supabase functions deploy <name> --use-api # deploys to the linked project, no Docker neededFunctions use the withSupabase helper from @supabase/server for auth. Manually invoking a
deployed function needs a secret key from the dashboard (Settings → API Keys → Secret keys)
sent on the apikey header — the legacy service_role key won't pass auth: "secret" checks.
The email channel sends via Resend's HTTP API, not raw SMTP. Create a free
account, generate an API key (Dashboard → API Keys), then set it as an Edge Function secret — a
separate secrets store from .env/.env.local, managed via the CLI, never committed:
pnpm supabase secrets set RESEND_API_KEY=<your resend api key>Without a verified sending domain, Resend only allows sending from its shared
onboarding@resend.dev address and only to the email address your Resend account itself was
created with — which is exactly this app's own use case (a self-hosting user alerting themselves
about their own projects), so this is a permanent, free configuration, not a temporary sandbox
limitation to graduate out of. If you later verify a custom domain with Resend, override the
sender without any code change:
pnpm supabase secrets set RESEND_FROM_ADDRESS="Upkeep <alerts@yourdomain.com>"Until RESEND_API_KEY is set, an email-type notification channel simply fails gracefully (logged,
not thrown) on every dispatch attempt — same per-channel isolation as every other channel type.
pnpm lint
pnpm buildRun pnpm lint before pnpm build, or delete .next/ first — a stale build directory gets linted
too and floods the output with unrelated errors from bundled JS.
See CONTRIBUTING.md for project conventions, commit message style, and how to open an issue or PR.