Skip to content

ollybee/push8030

Repository files navigation

push8030

A self-hosted web push notification gateway. Single Go binary, embedded BoltDB, no external dependencies. You register one or more browsers, create named send endpoints, then POST to an endpoint URL from any script or webhook to deliver a browser notification — no app, no email, no third-party service.

Open source. A public instance runs at push8030.com, but anyone can self-host their own.

Purpose

Designed to bridge tools like Grafana, Alertmanager, or custom shell scripts to browser notifications without middleware. Each send endpoint has its own URL and configurable behaviour: preset field values, control over which fields callers can override, choice of input format (JSON / form / HTTP headers), and optional token authentication.

How It Works

  1. Visit the landing page and click Get started — the server assigns a random memorable ID (e.g. happy-otter-42) and registers the current browser (stores a Web Push subscription).
  2. Rename the profile inline at the top of /{username} if you want something different — old URL 404s after rename.
  3. Create a send endpoint (get back a URL like /api/send/<token>) and/or a web form (get back a URL like /{username}/<slug>).
  4. POST to the endpoint URL from anywhere; fill in the form from any browser — all targeted browsers receive the notification via Web Push.
  5. The service worker (sw.js) receives the push event and calls showNotification.

Browser registration uses a pairing-code flow so the same profile can be accessed from multiple browsers without a password. A profile and its username are freed when the last browser is removed.

Usernames must be 2–32 characters (letters, digits, hyphens, underscores). A blocklist of reserved names is enforced at registration and rename time — these include names that conflict with top-level routes (api, docs, sw) and administratively sensitive names (admin, root, support, etc.).

Self-hosting

Build

go build -o push8030 .

The result is a single statically-linked binary with all HTML/JS/CSS embedded. Copy it to a server, make it executable, and run it. All state lives in a single BoltDB file (push8030.db) that is created on first run.

Run

PORT=8080 DB_PATH=/var/lib/push8030/push8030.db ./push8030

Environment variables:

Variable Default Description
PORT 8080 HTTP listen port
DB_PATH push8030.db Path to BoltDB file
PUSH8030_ADMIN_SLUG (unset) URL path for the admin page (e.g. secret-admin/secret-admin)
PUSH8030_ADMIN_PASSWORD (unset) Passphrase to log in to the admin page

TLS / reverse proxy

The binary speaks plain HTTP only — there is no built-in TLS. Web Push requires HTTPS (browsers refuse to register service workers over plain HTTP on non-localhost origins), so you must run it behind a reverse proxy (Caddy, nginx, Traefik, etc.) that terminates TLS. Example with Caddy:

push.example.com {
    reverse_proxy 127.0.0.1:8080
}

systemd unit

A minimal unit file:

[Unit]
Description=push8030 web push gateway
After=network.target

[Service]
User=push8030
Group=push8030
WorkingDirectory=/var/lib/push8030
ExecStart=/opt/push8030/push8030
Environment=PORT=8080
Environment=DB_PATH=/var/lib/push8030/push8030.db
Restart=on-failure

[Install]
WantedBy=multi-user.target

Backup

The whole service state is one file — push8030.db. Snapshot it while the service is stopped, or use bbolt to copy it live.

Core Files

File Role
main.go Globals, bucket names, resource caps, initDB, initTemplates, newMux, main, and the four static handlers (/, /docs, /sw.js, /api/vapid-public-key)
types.go All data structs and type constants (homeRecord, subRecord, EndpointConfig, browserInfo, etc.)
ratelimit.go In-memory rate limiter and per-route limit configs
helpers.go Shared utilities: clientIP, scheme, token/code generation, validUsername, labelFromUA, profileVapidKeys, applyConfigDefaults
usernames.go Adjective/noun wordlists and generateRandomUsername used for zero-friction signup
handlers_auth.go VAPID key endpoints, profile page, browser registration (/api/register, /api/join, etc.), authBrowser, pairing code
handlers_browsers.go Browser CRUD (list, rename, delete, prune), guest subscribe/unsubscribe, profile settings
handlers_endpoints.go Send endpoint and web form CRUD, endpoint config, form slug update
handlers_send.go handleSend, handleFormSend, form page/config, event helpers (appendEvent, countRecentEvents), field parsing and resolution
handlers_admin.go Admin dashboard — stats aggregation, passphrase auth (cookie-based), profile impersonation
static/profile.html Single-page profile UI — browser list, endpoint list, config panel, curl example, test button. Rendered server-side as a Go template (injects username). All subsequent data loaded via fetch.
static/form.html Web form page — allows anyone with the form URL to send a notification via a browser form. Rendered server-side (injects username and formToken).
static/index.html Landing page — one-click browser registration; server assigns a random memorable profile ID
static/sw.js Service worker — handles push events and notificationclick. Uses skipWaiting + clients.claim for immediate activation.

Data Model (types.go, main.go)

All state is in a single BoltDB file (push8030.db). Ten buckets:

Bucket Key Value
homes username homeRecord — profile creation time, guest signups flag, per-profile VAPID key pair
subs browser token (hex) subRecord — Web Push subscription (endpoint + auth/p256dh keys), label, guest flag, last push status/time, push count
endpoints_index push endpoint URL browser token (dedup guard)
send_endpoints send token (hex) sendEndpRecord — name, username, browser token list, EndpointConfig, form token, form_only flag, send/form counts
paircodes 6-digit code pairCodeRecord — username + expiry (5 min TTL)
vapid "private" / "public" Global VAPID key pair (generated once on first run; used as fallback for profiles without their own keys)
form_tokens username/formToken send endpoint token — maps form slugs to their send endpoint
endp_events token + timestamp empty — timestamped invocation log for per-hour endpoint counts
browser_events token + timestamp empty — timestamped push log for per-hour browser counts
form_events token + timestamp empty — timestamped form submission log for per-hour form counts

Event buckets store keys of the form <32-byte-token> + 0x00 + <8-byte big-endian nanosecond timestamp>. Only events from the last 2 hours are retained; older entries are pruned on write.

Endpoint Configuration (EndpointConfig)

Each send endpoint carries an EndpointConfig with eight notification fields (msg, title, ttl, urgency, topic, icon, url, tag). Each field is a FieldConfig{Value, Override}:

  • Value — preset sent on every notification regardless of caller
  • Override: true — caller is allowed to supply/replace the value

Three request formats: json (default), form, headers (reads X-Msg, X-Title, etc.).
Two auth modes: header or query — both compare a key/value token.

Default for new endpoints: msg has Override: true, Value: "Hello World" — so the curl example and test button are immediately usable.

API Routes

GET  /                                                         landing page
GET  /{username}                                               profile page
GET  /{username}/{formToken}                                   web form page
GET  /sw.js                                                    service worker
GET  /api/vapid-public-key                                     global VAPID public key
GET  /api/profile/{username}/vapid-public-key                  per-profile VAPID public key (used when subscribing)
GET  /api/profile/{username}/vapid-keys                        per-profile VAPID key pair — public + private (owner auth required)

POST /api/check-browser                                        check if browser is registered
POST /api/register                                             register new profile + browser
POST /api/recover                                              start pairing from existing profile
POST /api/join                                                 complete pairing with code

PATCH  /api/profile/{username}                                 rename profile (old URL 404s after rename)

POST   /api/profile/{username}/link-code                       generate pairing code
GET    /api/profile/{username}/browsers                        list browsers
PATCH  /api/profile/{username}/browsers/{token}                rename browser
DELETE /api/profile/{username}/browsers/{token}                remove browser (cleans up profile if last)
DELETE /api/profile/{username}/browsers                        prune all dead (410/404) browsers

GET    /api/profile/{username}/settings                        get profile settings (guest signup toggle)
PUT    /api/profile/{username}/settings                        update profile settings

POST   /api/profile/{username}/subscribe                       register a guest browser on this profile
DELETE /api/profile/{username}/subscribe                       unregister a guest browser

GET    /api/profile/{username}/endpoints                       list send endpoints and web forms
POST   /api/profile/{username}/endpoints                       create send endpoint
POST   /api/profile/{username}/forms                           create standalone web form
PATCH  /api/profile/{username}/endpoints/{token}               rename / retarget endpoint or form
PUT    /api/profile/{username}/endpoints/{token}/config        update EndpointConfig
PATCH  /api/profile/{username}/endpoints/{token}/form          update form slug
DELETE /api/profile/{username}/endpoints/{token}               delete endpoint or form

GET  /api/form/{username}/{formToken}                          get public form config (no auth credentials)
POST /api/form/{username}/{formToken}/send                     send notification via web form

POST /api/send/{endpointToken}                                 send notification (unauthenticated by default)

Profile management routes require a X-Browser-Endpoint header matching a registered (non-guest) browser on that profile.

Guest browsers

Any visitor can subscribe to receive notifications from a profile via POST /api/profile/{username}/subscribe. Guest browsers:

  • Appear in the owner's browser list (marked is_guest: true)
  • Receive pushes from all send endpoints that include their token
  • Cannot access profile management routes
  • Can unsubscribe themselves via DELETE /api/profile/{username}/subscribe
  • Are cleaned up automatically when the last owner browser removes the profile

The profile owner can disable guest signups via the settings API.

Web forms

Web forms are first-class objects, created independently from send endpoints via POST /api/profile/{username}/forms. Each form gets its own URL at /{username}/{formToken} — anyone with the URL can submit a notification via their browser. The form slug defaults to a random hex token and can be customised. Auth credentials are never sent to the form page.

Forms are stored in the same send_endpoints bucket as API endpoints and share the same per-profile limit. They are distinguished by form_only: true in their record — a flag that suppresses the webhook URL and hides request-format and authentication config (which only apply to webhook sends).

Per-Profile VAPID Keys

Each profile gets its own VAPID key pair, generated at registration time and stored in the profile's homeRecord. This isolates push abuse by user — a compromised or revoked profile's key pair has no effect on any other profile. The global key pair (stored in the vapid bucket) is kept only as a fallback for profiles that pre-date per-profile key generation.

Exporting for offline use

The profile page provides two download actions, both accessible only to authenticated profile owners:

  • Download VAPID keys — downloads vapid-keys-{username}.json containing the profile's public_key and private_key.
  • Download (per-browser row) — downloads push-{label}.json containing everything needed to send a push notification to that browser from any machine without access to this server:
{
  "vapid_public_key": "...",
  "vapid_private_key": "...",
  "subscription": {
    "endpoint": "https://fcm.googleapis.com/...",
    "keys": {
      "auth": "...",
      "p256dh": "..."
    }
  }
}

This bundle can be used directly with any Web Push library (e.g. webpush-go, web-push for Node) to deliver notifications offline — without routing through this server.

Admin

An optional admin dashboard is available when both PUSH8030_ADMIN_SLUG and PUSH8030_ADMIN_PASSWORD are set. If either is unset, the admin routes are not registered and the page is unreachable.

The dashboard lists all profiles with basic stats (owner browsers, guest subscribers, send endpoints, total pushes) and lets you silently impersonate any profile — viewing and managing it exactly as the owner would, without needing their browser subscription.

Dependencies

  • github.com/SherClockHolmes/webpush-go — VAPID signing and Web Push dispatch
  • go.etcd.io/bbolt — embedded key/value store
  • No frontend build step; all JS is vanilla, inlined in profile.html

License

MIT — see LICENSE.

About

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages