Cloudsteading
screenshot

Nabız

HTTP uptime checks and a public status page from one Worker

Nabız is a self-hosted Better Stack/Pingdom alternative built on Cloudflare (D1, Workers). Free tier eligible within limits. Inspect the source and license in the linked repository.

Source & license

Upstream license: MIT

License TL;DR

You can use it, change it, self-host it and sell it. Keep the original copyright and license notice with copies of the code. You don’t have to publish your changes. The authors don’t promise it will work.

Explain MIT in plain English →

Summary of the main license. Separate packages and assets can have different terms.

Inspect repository ↗Read this project’s actual license ↗

Repository owner

@productdevbook

See the upstream repository for the original creator and contributors.

Maintain this project? Maintainer verification →

Cloudflare hosting

Free tier eligible within limits

A small number of monitors and low public traffic can fit Workers and D1 free allowances. Every-minute checks consume scheduled CPU and D1 writes; the monitor count and retention must be bounded against daily quotas. Optional notification providers are separate.

Hosting requirements
  • Start with a small monitor set, measure the scheduler/request CPU and actual rows written rather than assuming one row per check.
  • Keep D1 below 5M rows read/day, 100k rows written/day and 5GB total; SQL indexes can increase billed writes.
  • Source and configuration review establishes a deployment path and conditional costs; this candidate was not executed or load-tested.
Check current pricing ↗
Sources checked 01/10/2026

Repository snapshot: eebc8ac. Hosting eligibility reflects the deployment documentation and listed assumptions.

  • uptimerobot ↗

    | | | |---|---| | [Cloudflare](docs/cloudflare.md) | deploying the Worker, custom hostname, limits | | [Self-hosting](docs/self-hosting.md) | Docker, compose, from a checkout | | [Kubernetes](deploy/k8s/README.md) | manifests, one replica, seeding in-cluster | | [Monitors](docs/monitors.md) | the rows that say what is watched | | [Configuration](docs/configuration.md) | every variable, on both runtimes | | [API](docs/api.md) | JSON, RSS, badge, notices, machine access | | [Upgrading](docs/UPGRADING.md) | schema additions, moving between runtimes | | [Installing with an agent](docs/agent-install.md) | a runbook: commands, checks, and what each error means |

  • better-stack ↗

    | | | |---|---| | [Cloudflare](docs/cloudflare.md) | deploying the Worker, custom hostname, limits | | [Self-hosting](docs/self-hosting.md) | Docker, compose, from a checkout | | [Kubernetes](deploy/k8s/README.md) | manifests, one replica, seeding in-cluster | | [Monitors](docs/monitors.md) | the rows that say what is watched | | [Configuration](docs/configuration.md) | every variable, on both runtimes | | [API](docs/api.md) | JSON, RSS, badge, notices, machine access | | [Upgrading](docs/UPGRADING.md) | schema additions, moving between runtimes | | [Installing with an agent](docs/agent-install.md) | a runbook: commands, checks, and what each error means |

  • pingdom ↗

    | | | |---|---| | [Cloudflare](docs/cloudflare.md) | deploying the Worker, custom hostname, limits | | [Self-hosting](docs/self-hosting.md) | Docker, compose, from a checkout | | [Kubernetes](deploy/k8s/README.md) | manifests, one replica, seeding in-cluster | | [Monitors](docs/monitors.md) | the rows that say what is watched | | [Configuration](docs/configuration.md) | every variable, on both runtimes | | [API](docs/api.md) | JSON, RSS, badge, notices, machine access | | [Upgrading](docs/UPGRADING.md) | schema additions, moving between runtimes | | [Installing with an agent](docs/agent-install.md) | a runbook: commands, checks, and what each error means |

  • workers ↗

    e rows in D1 (see schema.sql), not config in # a file — a monitor's URL is often somebody's hostname, and this repository # is public. name = "nabiz" main = "src/worker.ts" compatibility_date = "2026-08-01" compatibility_flags = ["nodejs_compat"] [triggers] crons = ["* * * * *"] # Workers Logs, inside the free tier — a cron that fails silently is a # status page lying by omission. [observability] enabled = true [[d1_databases]] binding = "DB" database_name = "nabiz" database_id = "REPLACE_WITH_YO

  • d1 ↗

    , inside the free tier — a cron that fails silently is a # status page lying by omission. [observability] enabled = true [[d1_databases]] binding = "DB" database_name = "nabiz" database_id = "REPLACE_WITH_YOUR_D1_ID" [vars] # "en", "tr", "de", "es", "fr", "zh-CN" LANG = "en" TITLE = "nabiz" # Optional, as secrets (`wrangler secret put …`): # TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID — a message on every state change # ALERT_WEBHOOK_URL — the same, as JSON to your own endpoin

  • free-tier-eligible ↗

    # Copy this next to your own deployment notes, fill in the two blanks, and # `wrangler deploy`. Monitors are rows in D1 (see schema.sql), not config in # a file — a monitor's URL is often somebody's hostname, and this repository # is public. name = "nabiz" main = "src/worker.ts" compatibility_date = "2026-08-01" compatibility_flags = ["nodejs_compat"] [triggers] crons = ["* * * * *"] # Workers Logs, inside the free tier — a cron that fails silently is a # status page lying by omission. [observability] enabled = true [[d1_databases]] binding = "DB" database_name = "nabiz" database_id = "REPLACE_WITH_YOUR_D1_ID" [vars] # "en", "tr", "de", "es", "fr", "zh-CN" LANG = "en" TITLE = "nabiz" # Optional, as secrets (`wrangler secret put …`): # TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID — a messag

  • free-tier-eligible ↗

    | **Free** | 100,000 per day | No charge for duration | 10 milliseconds of CPU time per invocation |

  • free-tier-eligible ↗

    | Rows read | 5 million / day | First 25 billion / month included + $0.001 / million rows |

  • free-tier-eligible ↗

    | Rows written | 100,000 / day | First 50 million / month included + $1.00 / million rows |

  • MIT ↗

    MIT License Copyright (c) 2026 productdevbook Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONN

  • architecture ↗

    # Copy this next to your own deployment notes, fill in the two blanks, and # `wrangler deploy`. Monitors are rows in D1 (see schema.sql), not config in # a file — a monitor's URL is often somebody's hostname, and this repository # is public. name = "nabiz" main = "src/worker.ts" compatibility_date = "2026-08-01" compatibility_flags = ["nodejs_compat"] [triggers] crons = ["* * * * *"] # Workers Logs, inside the free tier — a cron that fails silently is a # status page lying by omission. [observability] enabled = true [[d1_databases]] binding = "DB" database_name = "nabiz" database_id = "REPLACE_WITH_YOUR_D1_ID" [vars] # "en", "tr", "de", "es", "fr", "zh-CN" LANG = "en" TITLE = "nabiz" # Optional, as secrets (`wrangler secret put …`): # TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID — a messag

  • architecture ↗

    e rows in D1 (see schema.sql), not config in # a file — a monitor's URL is often somebody's hostname, and this repository # is public. name = "nabiz" main = "src/worker.ts" compatibility_date = "2026-08-01" compatibility_flags = ["nodejs_compat"] [triggers] crons = ["* * * * *"] # Workers Logs, inside the free tier — a cron that fails silently is a # status page lying by omission. [observability] enabled = true [[d1_databases]] binding = "DB" database_name = "nabiz" database_id = "REPLACE_WITH_YO

  • architecture ↗

    , inside the free tier — a cron that fails silently is a # status page lying by omission. [observability] enabled = true [[d1_databases]] binding = "DB" database_name = "nabiz" database_id = "REPLACE_WITH_YOUR_D1_ID" [vars] # "en", "tr", "de", "es", "fr", "zh-CN" LANG = "en" TITLE = "nabiz" # Optional, as secrets (`wrangler secret put …`): # TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID — a message on every state change # ALERT_WEBHOOK_URL — the same, as JSON to your own endpoin

Upstream screenshot · productdevbook/nabiz repository contributors ↗. Depicts the upstream project. We have not deployed and tested a fresh installation here.

What it can replace

Compare the workflow you need. These mappings describe overlap; full feature parity requires a separate comparison.

UptimeRobot logoUptimeRobot ↗

HTTP availability/latency checks and a public status page; incident timelines, subscriber email and multi-region probing are explicitly outside the documented core.

See supporting source ↗
Better Stack logoBetter Stack ↗

HTTP availability/latency checks and a public status page; incident timelines, subscriber email and multi-region probing are explicitly outside the documented core.

See supporting source ↗
Pingdom logoPingdom ↗

HTTP availability/latency checks and a public status page; incident timelines, subscriber email and multi-region probing are explicitly outside the documented core.

See supporting source ↗
external SaaS target
varies
→ D1 + Workers
external SaaS target
varies
→ D1 + Workers
external SaaS target
varies
→ D1 + Workers

How it works

The shape of Nabız on Cloudflare, and how it stacks up against the rented tools it replaces.

Architecture

Diagram of deployment declarations at the reviewed commit. Each app has its own entrypoint; declared resources do not prove runtime calls. Follow file and line sources below.

View upstream source ↗
Public interface
Configured entry points1
nabiz
wrangler.toml
↓
App
nabiz
entry
Cloudflare Workers
Entrypoint: src/worker.tsConfigured cron (UTC): * * * * *
↓

Configuration and workflow sources

Reviewed commit eebc8acce25c. Files were read as data; upstream applications and CI jobs were not executed.

Deployment configuration · 1 files
wrangler.toml ↗

Cloudflare Workers · compatibility 2026-08-01

nabiz · default

Entrypoint: src/worker.ts

Cron triggers (UTC): * * * * *

  • DB → D1

Named environments are separate deployments. Bindings are shown only where declared. Configured routes are URL patterns, not verified application endpoints.

Runtime source · handlers, binding usage and workflow steps

Observed TypeScript/JavaScript declarations from Worker entrypoints and resolved relative imports. Calls and workflow steps may run conditionally; their listed order is not a proven end-to-end request flow. Router declarations may be mounted under a prefix or may not be registered. This shows code wiring, not a successful deployment or runtime test. Dynamic wiring, aliases and generated code may not resolve.

src/worker.ts ↗
  • L18 · fetch handler exported · calls preflight, wantsJson, url.toString, cache.match, handle, includes, res.headers.get, ctx.waitUntil, cache.put, res.clone
  • L46 · scheduled handler exported · references DB · calls tick, getUTCMinutes

Environment references: env.DB

src/lib/api.ts ↗
  • L30 · json calls (conditional paths may differ): JSON.stringify
  • L41 · statusJson calls (conditional paths may differ): rows, overall, json, toISOString, list.map, uptimeOf, REASONS.has, map, eventsView, res.headers.set
  • L81 · historyJson calls (conditional paths may differ): rows, json, list.map, r.days.map, Math.round
  • L107 · badge calls (conditional paths may differ): overall, rows
  • L132 · escXml calls (conditional paths may differ): ch.codePointAt
  • L145 · plain calls (conditional paths may differ): trim, replace, body.split
  • L161 · feed calls (conditional paths may differ): escXml, eventsView, t, entries.push, toUTCString, sevLabel, slice, plain, render, join, map, entries.toSorted
  • L302 · refused calls (conditional paths may differ): json, answer.headers.set
  • L311 · jsonBody calls (conditional paths may differ): request.json
  • L332 · forgive calls (conditional paths may differ): attempts.delete, request.headers.get
  • L341 · throttled calls (conditional paths may differ): Date.now, attempts.delete, slice, toSorted, attempts.entries, request.headers.get, attempts.get, attempts.set
  • L367 · authorized calls (conditional paths may differ): request.headers.get, enc.encode, subtle.timingSafeEqual
  • L384 · postNotice calls (conditional paths may differ): jsonBody, json, body.body.trim, isLang, SEVERITIES.has, addNotice
  • L411 · postResolve calls (conditional paths may differ): jsonBody, json, Number.isSafeInteger, resolveNotice
  • L421 · noticesJson calls (conditional paths may differ): json, list.map, toISOString, render
src/lib/tick.ts ↗
  • L24 · tick calls (conditional paths may differ): monitors, AbortSignal.timeout, Promise.all, watched.map, probe, record, Math.round, alert, langOf, Math.max, console.error, join, changes.map, prune

Environment references: env.LANG

src/lib/i18n.ts ↗
  • L322 · isLang calls (conditional paths may differ): known.has
  • L340 · langOf calls (conditional paths may differ): known.has
src/lib/markdown.ts ↗
  • L5 · esc calls (conditional paths may differ): replace, s.replace
  • L18 · stars calls (conditional paths may differ): replace, s.replace
  • L26 · inline calls (conditional paths may differ): held.push, text.replace, Number, replace, s.replace, hold, put, stars
  • L57 · render calls (conditional paths may differ): split, esc, md.trim, join, blocks.map, block.split, lines.every, startsWith, l.trimStart, lines.map, inline, slice
src/lib/render.ts ↗
  • L12 · esc calls (conditional paths may differ): s.replace
  • L23 · percent calls (conditional paths may differ): toFixed, Math.floor, plain.replace
  • L39 · when calls (conditional paths may differ): slice, replace, toISOString
  • L46 · bars calls (conditional paths may differ): days.map, slice, toISOString, Date.now, byDay.get, cells.push, esc, t, percent, cells.join
  • L63 · uptimeLabel calls (conditional paths may differ): uptimeOf, percent
  • L71 · path calls (conditional paths may differ): join, xy.map, x.toFixed, y.toFixed
  • L79 · sparkline calls (conditional paths may differ): points.filter, Math.max, Math.min, points.forEach, runs.push, run.push, at, join, runs.map, map, runs.filter, path, ex.toFixed, sx.toFixed, t, ey.toFixed
  • L119 · sevLabel calls (conditional paths may differ): t
  • L132 · callout calls (conditional paths may differ): esc, sevLabel, when, render
  • L140 · pastNotice calls (conditional paths may differ): t, when, render
  • L148 · serviceRow calls (conditional paths may differ): t, sparkline, uptimeLabel, esc, bars, replace, String
  • L189 · eventLine calls (conditional paths may differ): esc, t, when
src/lib/shape.ts ↗
  • L45 · groupOf calls (conditional paths may differ): trim
  • L62 · medianDays calls (conditional paths may differ): byDay.get, seen.push, byDay.set, members.toSorted, out.push, out.toSorted
  • L110 · rows calls (conditional paths may differ): shaped.get, shape, shaped.set
  • L118 · shape calls (conditional paths may differ): data.monitors.forEach, groupOf, grouped.get, held.members.push, grouped.set, data.states.get, out.push, data.days.get, data.latency.get, data.spark.get, members.filter, isDown, medianDays, members.map, map, out.toSorted
  • L179 · overall calls (conditional paths may differ): list.filter, known.filter, known.some
  • L217 · eventsView calls (conditional paths may differ): monitors.map, groupOf, groups.get, states.get, held.down.add, groups.set, byId.get, out.push, Boolean, isDown, group.down.add, group.down.delete
src/lib/store.ts ↗
  • L19 · monitors calls (conditional paths may differ): all, db.prepare
  • L26 · utcDay calls (conditional paths may differ): slice, toISOString
  • L33 · record calls (conditional paths may differ): Date.now, utcDay, writes.push, bind, db.prepare, db.batch, all, states.map, stateWrites.push, known.get, put, changes.push, event, Math.max, Math.round, Boolean, forget
  • L180 · prune calls (conditional paths may differ): Date.now, db.prepare, map, watched.filter, Boolean, out.push, join, ids.slice, db.batch, bind, before, slice, toISOString, orphans, reconcile, forget
  • L238 · recentEvents calls (conditional paths may differ): all, bind, db.prepare
  • L309 · holdFor calls (conditional paths may differ): Math.max
  • L314 · forget calls (conditional paths may differ): recent.delete
  • L319 · forPage calls (conditional paths may differ): recent.get, Date.now, read, recent.set
  • L332 · asDay calls (conditional paths may differ): slice, toISOString
  • L336 · read calls (conditional paths may differ): monitors, Date.now, asDay, db.batch, db.prepare, bind, states.set, Boolean, days.get, list.push, days.set, latency.set, Math.floor, spark.get, Array.from, spark.set
  • L432 · notices calls (conditional paths may differ): all, bind, db.prepare
  • L453 · addNotice calls (conditional paths may differ): first, bind, db.prepare, Date.now, forget
  • L469 · resolveNotice calls (conditional paths may differ): run, bind, db.prepare, Date.now, forget
src/lib/alert.ts ↗
  • L11 · held calls (conditional paths may differ): Math.floor, t, say, replace
  • L42 · oneLine calls (conditional paths may differ): ch.codePointAt
  • L59 · to calls (conditional paths may differ): sending, console.error
  • L78 · alert calls (conditional paths may differ): changes.map, oneLine, t, held, lines.join, toISOString, sends.push, to, fetch, JSON.stringify, AbortSignal.timeout, Promise.all, answers.filter

Environment references: env.TELEGRAM_BOT_TOKEN · env.TELEGRAM_CHAT_ID · env.ALERT_WEBHOOK_URL

src/lib/probe.ts ↗
  • L35 · within calls (conditional paths may differ): AbortSignal.timeout, any
  • L51 · firstOf calls (conditional paths may differ): chunk.subarray, controller.enqueue, controller.terminate, text, body.pipeThrough
  • L73 · probe calls (conditional paths may differ): Date.now, within, fetch, firstOf, body.includes
Build and deployment pipeline · 2 GitHub Actions workflows

Repository CI declarations, separate from runtime request processing. Job dependencies and conditions are shown as written; long commands are shortened with an ellipsis; a workflow file does not prove a recent successful run.

CI · .github/workflows/ci.yml ↗

Triggers: push, pull_request

check · no job dependencies declared

  1. actions/checkout@v5actions/checkout@v5
  2. oven-sh/setup-bun@v2oven-sh/setup-bun@v2
  3. Shell commandbun install --frozen-lockfile
  4. Shell commandbun run typecheck
  5. Shell commandbunx oxlint --deny-warnings
  6. Shell commandbunx oxfmt --check src test
  7. Shell commandbun test
  8. Shell commandbun run build
  9. Shell commandbunx wrangler deploy -c dist/server/wrangler.json --dry-run
  10. Shell commandbun run build:server

image · no job dependencies declared

  1. actions/checkout@v5actions/checkout@v5
  2. Shell commanddocker build -t nabiz:ci .
  3. it answers on the port it saysdocker run -d --name nabiz-ci -p 8080:8080 nabiz:ci for i in $(seq 1 30); do code=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/health || true) [ "$code" = "204" ] && break sleep 1 done test "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/)" = "200" test "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/health)" = "204" docker logs nabiz-ci
  4. Shell commandkubectl kustomize deploy/k8s > /dev/null
Image · .github/workflows/release.yml ↗

Triggers: push, workflow_dispatch

image · no job dependencies declared

  1. actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09
  2. oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76
  3. the tag, the package and the header agreetag="${GITHUB_REF_NAME#v}" pkg=$(node -p "require('./package.json').version") src=$(grep -oP 'VERSION = "\K[^"]+' src/lib/version.ts) echo "tag=$tag package.json=$pkg src=$src" test "$tag" = "$pkg" test "$tag" = "$src"Condition: github.ref_type == 'tag'
  4. Shell commandbun install --frozen-lockfile
  5. Shell commandbun run check
  6. docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130
  7. docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6fdocker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f
  8. docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9
  9. Shell commandtop=$(git tag --list 'v*' --sort=-v:refname | head -1) echo "newest=$([ "$top" = "$GITHUB_REF_NAME" ] && echo yes || echo no)" >> "$GITHUB_OUTPUT" echo "this=$GITHUB_REF_NAME newest=$top"Condition: github.ref_type == 'tag'
  10. docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051
  11. docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8
  12. it answers on the port it saysdocker run -d --name nabiz-smoke -p 8080:8080 nabiz:smoke for i in $(seq 1 30); do code=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/health || true) [ "$code" = "204" ] && break sleep 1 done test "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/)" = "200" test "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/health)" = "204" # The build says which build it is, and it is this one: the gate # above reads the checkout, this reads what was actually bu…
  13. docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8
package.json ↗
  • build: astro build
  • build:server: NABIZ_TARGET=node astro build
  • deploy: astro build && wrangler deploy -c dist/server/wrangler.json

Full upstream document by @productdevbook · README.md · snapshot eebc8ac

nabiz

nabiz

A status page that keeps beating when your server does not —
one Cloudflare Worker, or one container on a machine you own.

CI latest release container image MIT

screenshot

What it is

Astro renders the page, a probe checks every monitor once a minute, and the history lives in a database next to it. On Cloudflare that is one Worker with a cron trigger and D1, inside the free tier. On your own hardware it is one container with an interval and a SQLite file — same source, same schema. nabız is Turkish for "pulse"; the ASCII spelling nabiz is used throughout.

Documentation

Cloudflare deploying the Worker, custom hostname, limits
Self-hosting Docker, compose, from a checkout
Kubernetes manifests, one replica, seeding in-cluster
Monitors the rows that say what is watched
Configuration every variable, on both runtimes
API JSON, RSS, badge, notices, machine access
Upgrading schema additions, moving between runtimes
Installing with an agent a runbook: commands, checks, and what each error means

Features

  • One probe per monitor per minute: method, expected status, timeout, optional body match.
  • Anti-flap: a watched monitor is called down after fail_threshold consecutive failures (default 2); recovery is immediate, and a monitor that is already down when first seen is believed at once.
  • 90-day uptime bars, and a latency sparkline once there are probes in two different hours — it averages by the hour, so a fresh deployment has no waveform until the clock crosses one.
  • Grouped monitors: one row saying how the group is, never how many it speaks for and never their names.
  • Operator notices in markdown, with severity and per-language targeting.
  • Alerts to Telegram and a webhook on state changes.
  • Six languages (en, tr, de, es, fr, zh-CN), light and dark, auto-refresh.
  • JSON API, RSS feed, SVG badge, llms.txt.

Not included: incident timelines, subscriber emails, multi-region probes.

Quick start

On your own machine

docker run -d --name nabiz -p 8080:8080 -v nabiz:/data \
  -e NABIZ_TITLE="status" -e ADMIN_TOKEN="$(openssl rand -hex 32)" \
  ghcr.io/productdevbook/nabiz:latest

On Cloudflare

git clone https://github.com/productdevbook/nabiz && cd nabiz && bun install
bunx wrangler d1 create nabiz                        # id goes into wrangler.toml
bunx wrangler d1 execute nabiz --remote --file schema.sql
bun run deploy

Either way the page comes up empty and working — a heading with nothing under it is the correct first sight. Monitors are rows you add next, and the first bar appears within one probe interval: see Monitors. Keep the token you generated above; it is what lets you write notices. Nothing on the page prints it again — docker inspect nabiz will, which is also a reason not to leave it on a machine other people can reach.

Development

bun install
bunx wrangler d1 execute nabiz --local --file schema.sql   # once
bun run dev                                                # :4321, local D1
bun run check                                              # what CI runs

Everything under src/lib/ is plain TypeScript against a narrow database interface (src/lib/db.ts) that D1 already satisfies and SQLite is made to (src/lib/sqlite.ts). That seam is the whole difference between the two runtimes: the page, the probes and the state machine are one copy.

License

MIT. Icons from Lucide (ISC).

Frequently asked about Nabız

What is Nabız?+

Nabız is a self-hosted Better Stack/Pingdom alternative built on the Cloudflare developer platform. HTTP uptime checks and a public status page from one Worker

What does Nabız replace?+

Nabız is listed as an alternative to Better Stack, Pingdom, UptimeRobot. Compare the features and tradeoffs before migrating.

What Cloudflare primitives does Nabız use?+

Nabız is built on D1, Workers.

How much does Nabız cost to run?+

A small number of monitors and low public traffic can fit Workers and D1 free allowances. Every-minute checks consume scheduled CPU and D1 writes; the monitor count and retention must be bounded against daily quotas. Optional notification providers are separate. Start with a small monitor set, measure the scheduler/request CPU and actual rows written rather than assuming one row per check. Keep D1 below 5M rows read/day, 100k rows written/day and 5GB total; SQL indexes can increase billed writes. Source and configuration review establishes a deployment path and conditional costs; this candidate was not executed or load-tested. Check current Cloudflare pricing before deploying.

Is Nabız open source?+

The upstream repository declares the MIT license. Read its terms at https://raw.githubusercontent.com/productdevbook/nabiz/eebc8acce25c5db5001398b483ba7a8016c3c5aa/LICENSE. Source code and contributor credit are available at https://github.com/productdevbook/nabiz.

Discussion · 0

sign in to comment →
No comments yet — be the first.