pb-ext

July 26, 2026 · View on GitHub

Enhanced PocketBase server with monitoring, alerting, auditing, logging & API docs.

Ask DeepWiki

Core Features

  • API Schema: Auto-generates OpenAPI docs UI for your endpoints
  • Cron Tracking: Logs and monitors scheduled cron jobs
  • System Monitoring: Real-time CPU, memory, disk, network, and runtime metrics
  • Operational Alerts: Telegram notifications for crashes, failed jobs, panics and resource saturation
  • Access Auditing: Who reached the admin surfaces, with brute-force & new-address detection
  • Structured Logging: Complete logging with error tracking and request tracing
  • Visitor Analytics: Track GDPR & PII compliant visitors, page views, device types, and browsers
  • PocketBase Integration: Uses PocketBase's auth system and styling

Access

  • Admin panel:
127.0.0.1:8090/_
  • pb-ext dashboard:
127.0.0.1:8090/_/_

The dashboard has six sections: Health (system metrics), Analytics (visitors), Alerts & Access (notification status and the admin access log), API (OpenAPI docs and tester), Cron (jobs and their logs) and Rules (collection access rules).

Quick Start

package main

import (
	"flag"
	"log"

	app "github.com/magooney-loon/pb-ext/core"
	"github.com/pocketbase/pocketbase/core"
)

func main() {
	devMode := flag.Bool("dev", false, "Run in developer mode")
	generateSpecsDir := flag.String("generate-specs-dir", "", "Generate OpenAPI specs into the provided directory and exit")
	generateSpecVersion := flag.String("generate-spec-version", "", "Optional API version to generate (requires --generate-specs-dir)")
	validateSpecsDir := flag.String("validate-specs-dir", "", "Validate OpenAPI specs from the provided directory and exit")
	flag.Parse()

	if *generateSpecsDir != "" {
		gen := app.NewSpecGeneratorWithInitializer(func() (*app.APIVersionManager, error) {
			return initVersionedSystem(), nil
		})
		if err := gen.Generate(*generateSpecsDir, *generateSpecVersion); err != nil {
			log.Fatal(err)
		}
		return
	}

	if *validateSpecsDir != "" {
		gen := app.NewSpecGeneratorWithInitializer(func() (*app.APIVersionManager, error) {
			return initVersionedSystem(), nil
		})
		if err := gen.Validate(*validateSpecsDir); err != nil {
			log.Fatal(err)
		}
		return
	}

	initApp(*devMode)
}

func initApp(devMode bool) {
	var opts []app.Option

	if devMode {
		opts = append(opts, app.InDeveloperMode())
	} else {
		opts = append(opts, app.InNormalMode())
	}

	// Option 1: Use a custom PocketBase config
	// pbConfig := &pocketbase.Config{
	// 	DefaultDev:     true,
	// 	DefaultDataDir: "./custom_pb_data",
	// }
	// opts = append(opts, app.WithConfig(pbConfig))

	// Option 2: Use an existing PocketBase instance
	// pb := pocketbase.New()
	// opts = append(opts, app.WithPocketbase(pb))

	// Set custom port programmatically
	// os.Args = []string{"app", "serve", "--http=127.0.0.1:9090"}

	// Note: WithConfig and WithPocketbase cannot be used together

	// Alerting and admin access auditing. Both work with no configuration at
	// all — alerting needs only PBEXT_TELEGRAM_BOT_TOKEN and
	// PBEXT_TELEGRAM_CHAT_ID in the environment, and auditing is on by default.
	//
	// Every available option, with its default, is documented in
	// cmd/server/alerts.go.
	opts = append(opts,
		app.WithAlerts(alertOptions()...),
		app.WithAudit(auditOptions()...),
	)

	srv := app.New(opts...)

	app.SetupLogging(srv)

	registerCollections(srv.App())
	registerRoutes(srv.App())
	registerJobs(srv.App())
	registerAlerts(srv.App())

	srv.App().OnServe().BindFunc(func(e *core.ServeEvent) error {
		app.SetupRecovery(srv.App(), e)
		return e.Next()
	})

	if err := srv.Start(); err != nil {
		srv.App().Logger().Error("Fatal application error",
			"error", err,
			"uptime", srv.Stats().StartTime,
			"total_requests", srv.Stats().TotalRequests.Load(),
			"active_connections", srv.Stats().ActiveConnections.Load(),
			"last_request_time", srv.Stats().LastRequestTime.Load(),
		)
		log.Fatal(err)
	}
}

// Example models in cmd/server/collections.go
// Example routes in cmd/server/routes.go
// Example handlers in cmd/server/handlers.go
// Example cron jobs in cmd/server/jobs.go
// Example alert rules in cmd/server/alerts.go
//
// You can restructure Your project as You wish,
// just keep this main.go in cmd/server/main.go
//
// Build toolchain (pb-cli):
// go install github.com/magooney-loon/pb-ext/cmd/pb-cli@latest
//
// Need a pre-built Svelte5Kit starter template?
// https://github.com/magooney-loon/svelte-gui
//
// Ready for a production build deployment?
// https://github.com/magooney-loon/pb-deployer
go mod tidy
go install github.com/magooney-loon/pb-ext/cmd/pb-cli@latest
pb-cli --run-only

Alerts

Operational notifications over Telegram. Set two environment variables and restart — no code changes needed:

export PBEXT_TELEGRAM_BOT_TOKEN="123456789:AA..."   # from @BotFather
export PBEXT_TELEGRAM_CHAT_ID="-1001234567890"      # negative for groups
VariablePurpose
PBEXT_TELEGRAM_BOT_TOKENBot token from @BotFather
PBEXT_TELEGRAM_CHAT_IDTarget chat; negative for groups
PBEXT_TELEGRAM_TOPIC_IDOptional thread id in a forum-style group
PBEXT_ALERTS_ENABLEDfalse disables alerting regardless of the rest

With neither credential set, alerting stays disabled and every call is a no-op — nothing to guard in your own code. Alerts are also off in developer mode.

What reports itself with no configuration:

AlertTrigger
Server started / shut downLifecycle
Recovered from an unexpected exitA previous run that never reached its shutdown hook — crash, OOM kill, or a host that went away
Cron job failedAny job returning an error or panicking
Panic recoveredA panic in a request handler
Failed superuser loginA rejected admin sign-in, naming the account targeted
Repeated failed logins5 failures from one source inside 10 minutes
Sign-in from a new addressA successful admin sign-in from an address with no prior success on record
Disk / memory / swap / CPU / descriptorsSustained saturation — 90% disk, 90% memory, 80% swap, 90% CPU, 80% of RLIMIT_NOFILE

Opt-in:

app.WithAlerts(
    app.WithErrorRateAlert(10, 20), // 5xx above 10% of a 20+ request window
    app.WithTrafficSurge(5, 50),    // 5× the rolling baseline, floored at 50 req/s
)

Tune or drop the defaults with app.WithDiskAlert(95), app.WithSwapAlert(0), or app.WithoutResourceAlerts(). Ad-hoc alerts from your own code:

app.GetNotifier().Send(app.AlertMessage{
    Level: app.AlertWarn,
    Title: "Payment webhook rejected",
    Fields: map[string]string{"provider": "stripe", "code": "402"},
})

GetNotifier() never returns nil and Send does no I/O, so it is safe on any code path, including request handlers. Flood control is built in: one alert per key per 15 minutes, 20 an hour, then an hourly digest of what was held back. Custom periodic rules and every option with its default are in cmd/server/alerts.go.

What pb-ext cannot tell you

A process that has been killed cannot report its own death. An OOM kill, a log.Fatal, or a panic on an unrecovered goroutine ends the process in microseconds, while a Telegram delivery needs hundreds of milliseconds. pb-ext detects those on the next boot instead, from a heartbeat marker in the data directory, and reports roughly when the previous run stopped.

Nothing in-process can tell you about a host that never comes back. For that you need an external dead-man's switch — a cron job pinging healthchecks.io or Uptime Kuma, alerting when the ping stops.

Admin Access Auditing

Records access to the administrative surfaces: PocketBase's admin UI, pb-ext's dashboard, superuser API calls, and every superuser authentication attempt. On by default.

It exists because PocketBase's own request log cannot answer four questions: which account a failed login targeted (the attempted identity arrives in the request body and is never logged), which superuser performed an action (Logs.LogAuthId defaults to off), what happened more than 5 days ago (Logs.MaxDays), and how any of it rolls up per source.

⚠️ This is the one place pb-ext stores personal data. It keeps the client address, the user agent, and the account name an authentication attempt supplied. That is deliberate — "who tried to get into the admin panel" is unanswerable without them — and bounded: everything is deleted after 90 days.

Narrow or disable it:

app.WithAudit(
    app.WithAuditPersonalData(true, true, false), // keep IP + agent, drop the account name
    app.WithAuditRetentionDays(30),
    app.WithBruteForceAlert(3, 5*time.Minute),
)

app.WithAudit(app.WithAuditEnabled(false)) // off entirely

Passwords are never read, logged or stored.

OpenAPI Spec Generation

Dev vs Production

  • Development: Specs are generated at runtime via AST parsing - no disk files needed
  • Production: Specs are generated at build time and read from disk (dist/specs/)

Build pipeline

The pb-cli toolchain runs OpenAPI generation automatically for production builds:

pb-cli              # Development mode (no spec generation)
pb-cli --build-only # Build frontend + generate specs
pb-cli --production # Production build with specs

For programmatic usage, see pkg/scripts/README.md.

Having issues with Your API Docs?

127.0.0.1:8090/api/docs/debug/ast

Reserved Schema

pb-ext creates the following automatically on startup. Do not create collections or tables with these names in your own code.

NameDatabaseKindPurpose
_job_logspb_data/data.dbSystem collectionCron job execution logs (start time, end time, duration, status, output). Retention: 72 hours.
_analyticspb_data/auxiliary.dbPlain tableDaily aggregated page view counters (one row per path/date/device/browser). Retention: 90 days.
_alertspb_data/auxiliary.dbPlain tableAlert delivery log (level, title, transport, outcome, error). Retention: 30 days.
_admin_accesspb_data/auxiliary.dbPlain tableAdmin access log (path, outcome, client address, user agent, account). Retention: 90 days.

Schema notes:

  • The three auxiliary tables live in the same database PocketBase uses for _logs, so writing to them never contends with your application's writes. They are plain SQLite tables, not collections, and are therefore not exposed through the records API or the Collections UI.
  • _job_logs is a system collection, hidden from the PocketBase Collections UI.
  • _job_logs, _analytics and _alerts store no personal data — no IP, no user agent, no visitor ID. Analytics is GDPR-compliant by design: it counts page views as daily aggregates and never records who viewed them.
  • _admin_access does store personal data, deliberately and as the only exception — see Admin Access Auditing. Each field can be switched off individually, and the whole table is bounded by its retention window.
  • All are included in PocketBase's backups, which archive the whole data directory.
  • Schema changes ship as new migrations and are applied at startup with no manual steps required.

Reserved Routes

pb-ext registers the following routes. Do not register your own routes at these paths.

Dashboard

MethodPathAuthDescription
GET/_/_Superuserpb-ext health, analytics, alerts, access & jobs dashboard

Cron Job API

All routes require superuser authentication.

MethodPathDescription
GET/api/cron/jobsList registered cron jobs
POST/api/cron/jobs/{id}/runTrigger a job manually
DELETE/api/cron/jobs/{id}Remove a job from the scheduler
GET/api/cron/statusCron scheduler status
POST/api/cron/config/timezoneUpdate scheduler timezone
GET/api/cron/logsPaginated job execution logs
GET/api/cron/logs/{job_id}Logs for a specific job
GET/api/cron/logs/analyticsAggregated job log statistics

Alerts API

All routes require superuser authentication.

MethodPathDescription
GET/api/alerts/statusDelivery state, counters and configured target (never the bot token)
GET/api/alerts/recentRecent alert deliveries
POST/api/alerts/testSend a test message immediately; limited to one per minute

Admin Access API

All routes require superuser authentication and return at most 500 rows per request.

MethodPathDescription
GET/api/audit/statusCapture state and a 7-day summary
GET/api/audit/recentRecent admin access records
GET/api/audit/sourcesPer-address rollup, most failures first

API Docs

MethodPathDescription
GET/api/docs/versionsList registered API versions
GET/api/docs/debug/astAST parsing debug info
GET/api/docs/{version}Version metadata
GET/api/docs/{version}/specOpenAPI 3.0 spec
GET/api/docs/{version}/swaggerSwagger UI

Internal System Jobs

pb-ext registers these cron jobs automatically. They appear in the dashboard with the "System" badge.

Job IDScheduleDescription
__pbExtLogClean__0 0 * * * (daily midnight)Purge _job_logs records older than 72 hours
__pbExtAnalyticsClean__0 3 * * * (daily 3 AM)Purge _analytics rows older than 90 days
__pbExtAlertsClean__0 4 * * * (daily 4 AM)Purge _alerts rows past their retention (30 days)
__pbExtAuditClean__0 5 * * * (daily 5 AM)Purge _admin_access rows past their retention (90 days)

Reserved Files

PathPurpose
pb_data/.pbext_lastrun.jsonHeartbeat marker used to detect an unclean shutdown on the next boot. Safe to delete; the only effect is that one crash goes unreported.