12 KiB
PHP Admin Page for Caching Service
Problem
The existing Nostr-based admin API (caching_follows_status kind-23456
command) encrypts its JSON response with NIP-44, which has a hard 64KB
plaintext limit. With 679+ followed pubkeys, the response exceeds this
limit by ~2-4×, causing Encryption result code: -15
(NOSTR_ERROR_NIP44_BUFFER_TOO_SMALL) and the UI shows
"Failed to load".
Rather than paginating the NIP-44 admin command (complex, still limited), we build a separate traditional admin page using PHP + nginx that queries PostgreSQL directly. This bypasses the NIP-44 limit entirely, scales to thousands of follows, and runs in parallel with the existing Nostr admin API (no changes to the relay binary needed).
Architecture
flowchart LR
Browser[Admin Browser] -->|HTTPS| Nginx
Nginx -->|/admin/ *.php| PHP_FPM[PHP-FPM]
Nginx -->|/ and /api| Relay[C-Relay-PG :8888]
PHP_FPM -->|PDO| PostgreSQL[(PostgreSQL crelay)]
Relay -->|libpq| PostgreSQL
- nginx fronts everything on 443 (existing SSL setup).
/and/api→ proxy to C-Relay-PG (existing, unchanged)/admin/→ PHP-FPM (new)
- PHP-FPM runs as a pool (e.g.
wwwor a dedicatedrelay-adminpool). PHP files live in/opt/c-relay-pg/admin/(or similar). - PDO connects to PostgreSQL using the same
crelaycredentials the relay uses. Read-only queries for display; write queries only for config updates (with auth guard). - No relay restart needed — this is purely nginx + PHP-FPM configuration + static files.
Tech Stack
- PHP 8.x with PDO PostgreSQL extension (
php-pgsql) - PHP-FPM managed by systemd (or the existing nginx PHP-FPM pool)
- nginx
location /admin/block withfastcgi_passto PHP-FPM - Frontend: vanilla HTML + JS (no build step), same visual style as
the existing
api/index.htmladmin page. Optionally use HTMX or Alpine.js for lightweight interactivity, but vanilla JS is sufficient.
Authentication
The PHP admin page needs its own auth since it's not going through the Nostr admin command path. Options (in order of simplicity):
- HTTP Basic Auth (nginx-level) — simplest, sufficient for a
single-admin relay. Add
auth_basicto the/admin/location block with a.htpasswdfile. - Session-based login — PHP login form, password hash in a config file, session cookie. More flexible but more code.
- Nostr login (NIP-07 extension) — the page could verify a signed
auth event from the admin's browser extension, matching against the
admin_pubkeyin the config table. Most aligned with the existing system but most complex.
Recommendation: Start with HTTP Basic Auth (option 1) for the MVP, upgrade to Nostr login later if desired.
Database Access
PHP connects via PDO with the same credentials the relay uses:
host=localhost port=5432 dbname=crelay user=crelay password=crelay
Store the connection string in a PHP config file
(/opt/c-relay-pg/admin/config.php) that is NOT web-accessible (outside
the document root or protected by nginx).
Pages / Endpoints
1. Dashboard (/admin/index.php)
Overview cards:
- Service state, heartbeat age, config generation (from
caching_service_state) - Followed author count, selected relay count, connected relay count
- Events fetched, inbox inserts, inbox pending count
- Backfill progress: X/Y authors complete
- Active backfill target (pubkey + relay) from
caching_backfill_active - Error relays summary (count of rows with
consecutive_errors >= 3)
2. Follows (/admin/follows.php)
The main table that was failing via NIP-44. Paginated server-side:
- Page size: 50 or 100 follows per page
- Columns: Name (from kind-0), npub, Root?, Events in DB, Backfill Complete, Relays (expandable), Last Seen
- JOIN with
eventstable for kind-0 profile metadata (name, display_name, picture, nip05) - JOIN with
caching_backfill_relay_progressfor per-relay status - Filter: all / incomplete only / root only / error relays only
- Search by name or pubkey prefix
- Sort by: events_fetched desc, name, last_seen, backfill_complete
SQL (paginated):
SELECT fp.pubkey, fp.is_root, fp.backfill_complete, fp.events_fetched,
fp.first_seen, fp.last_seen,
e.content::json->>'name' AS name,
e.content::json->>'display_name' AS display_name,
e.content::json->>'picture' AS picture,
e.content::json->>'nip05' AS nip05,
(SELECT COUNT(*) FROM events WHERE pubkey = fp.pubkey) AS total_events
FROM caching_followed_pubkeys fp
LEFT JOIN LATERAL (
SELECT content FROM events
WHERE pubkey = fp.pubkey AND kind = 0
ORDER BY created_at DESC LIMIT 1
) e ON true
ORDER BY fp.is_root DESC, fp.events_fetched DESC
LIMIT $page_size OFFSET $offset;
3. Relay Progress (/admin/relays.php)
Per (author, relay) backfill status from
caching_backfill_relay_progress:
- Columns: Author (name + npub), Relay URL, Until Cursor, Complete, Events Fetched, Consecutive Errors, Last Status, Updated At
- Filter: incomplete only / error relays / complete / all
- Highlight rows with
consecutive_errors >= 3 - Show
last_status(now descriptive:error: tls/ws handshake failed,eose,timeout, etc.)
4. Config (/admin/config.php)
Read and edit caching config values from the config table:
- Read:
caching_root_npubs,caching_bootstrap_relays,caching_kinds,caching_admin_kinds, allcaching_*keys - Edit: form to update values, bumps
caching_config_generationon save (so the caching service hot-reloads) - This replaces the "Apply Configuration" button for caching settings
5. Inbox (/admin/inbox.php)
Monitor the caching_event_inbox queue:
- Pending count by source_class (live / backfill / discovery)
- Oldest pending age
- Recent events preview (last 20, with kind + pubkey + source)
- This helps diagnose if the poller is keeping up
File Structure
/opt/c-relay-pg/admin/ # PHP document root (or symlink)
├── config.php # DB connection config (NOT web-served)
├── index.php # Dashboard
├── follows.php # Followed pubkeys table (paginated)
├── relays.php # Per-relay backfill progress
├── config.php # Caching config editor
├── inbox.php # Inbox queue monitor
├── api/
│ ├── follows.php # JSON endpoint for follows (AJAX)
│ ├── relays.php # JSON endpoint for relay progress
│ ├── status.php # JSON endpoint for dashboard stats
│ └── config.php # JSON endpoint for config get/set
├── assets/
│ ├── style.css # Shared CSS (match existing admin theme)
│ └── app.js # Shared JS (pagination, search, refresh)
└── .htpasswd # HTTP Basic Auth passwords
In the repo, these live under admin/ (new top-level directory,
parallel to api/).
nginx Configuration
Add to the existing server block in
examples/deployment/nginx-proxy/nginx.conf:
# PHP admin page for caching service (direct PostgreSQL access)
location /admin/ {
root /opt/c-relay-pg;
index index.php;
# HTTP Basic Auth
auth_basic "Relay Admin";
auth_basic_user_file /opt/c-relay-pg/admin/.htpasswd;
# Pass .php files to PHP-FPM
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
# Deny access to config.php (contains DB credentials)
location = /admin/config.php {
deny all;
}
}
Implementation Plan
Step 1 — Server setup (one-time)
- Install PHP 8.x + PHP-FPM +
php-pgsqlon the relay server - Create
/opt/c-relay-pg/admin/directory - Configure PHP-FPM pool (or use default
wwwpool) - Add the nginx
location /admin/block - Create
.htpasswdwithhtpasswd/openssl passwd - Reload nginx
Step 2 — PHP config + DB connection helper
admin/lib/db.php— PDO connection helper, read-only by defaultadmin/config.php— connection string (protected from web access)- Test:
php -r "echo 'ok';"+ simpleSELECT 1query
Step 3 — Dashboard page (index.php)
- Query
caching_service_statefor overview stats - Query
caching_backfill_activefor active target - Query
caching_backfill_relay_progressfor error summary - Display as cards + summary table
- Auto-refresh every 15s via JS
setInterval+fetch()
Step 4 — Follows page (follows.php)
- Server-side pagination (page param in URL)
- JOIN with
eventsfor kind-0 profile metadata - Sub-query for per-pubkey event count
- Expandable relay progress per follow (AJAX load on click)
- Search + filter controls
- This is the page that replaces the broken NIP-44
caching_follows_status
Step 5 — Relay progress page (relays.php)
- Full
caching_backfill_relay_progresstable with pagination - Filter by complete/incomplete/error
- Show
last_statusandconsecutive_errorscolumns - Color-code: green=eose, yellow=timeout, red=error, gray=complete
Step 6 — Config editor page (config.php → admin/config-edit.php)
- Read all
caching_*config keys - Form to edit
caching_root_npubs,caching_bootstrap_relays,caching_kinds, etc. - On save:
UPDATE config SET value = $1 WHERE key = $2+ bumpcaching_config_generation - Confirmation dialog before applying
Step 7 — Inbox monitor page (inbox.php)
- Pending counts by source_class
- Oldest pending age
- Recent 20 events preview
- Poller stats (from relay log or a new stats table)
Step 8 — Styling + polish
- Match the existing
api/index.cssdark theme - Responsive layout (works on mobile)
- Auto-refresh for dashboard and follows pages
Security Considerations
- HTTP Basic Auth on
/admin/— minimum barrier config.phpdenied via nginxlocationblock (contains DB password)- PDO prepared statements everywhere — no SQL injection
- Read-only DB user for display pages (optional: create a
crelay_readonlyrole with SELECT-only on caching tables; use the fullcrelayuser only for the config editor) - HTTPS only — the existing nginx SSL config covers this
- No CORS needed — same origin as the relay
Out of Scope (for MVP)
- Nostr NIP-07 extension login (use HTTP Basic Auth first)
- Write operations beyond config editing (reset backfill, etc. — those stay on the Nostr admin API)
- Real-time WebSocket updates (use polling instead — 15s interval is sufficient for a monitoring dashboard)
- Multi-user / role-based access
Files to Create
| File | Purpose |
|---|---|
admin/config.php |
DB connection config (protected) |
admin/lib/db.php |
PDO connection helper |
admin/lib/helpers.php |
Shared helpers (pagination, formatting) |
admin/index.php |
Dashboard |
admin/follows.php |
Followed pubkeys table (paginated) |
admin/relays.php |
Per-relay backfill progress |
admin/config-edit.php |
Caching config editor |
admin/inbox.php |
Inbox queue monitor |
admin/api/status.php |
JSON: dashboard stats |
admin/api/follows.php |
JSON: paginated follows |
admin/api/relays.php |
JSON: relay progress |
admin/assets/style.css |
Shared CSS |
admin/assets/app.js |
Shared JS |
admin/.htpasswd |
HTTP Basic Auth passwords |
examples/deployment/nginx-proxy/nginx.conf |
Add /admin/ location block |
Deployment
- Copy
admin/to/opt/c-relay-pg/admin/on the server - Install
php8.2-fpm php8.2-pgsql(or distro-equivalent) - Create
.htpasswd:htpasswd -c /opt/c-relay-pg/admin/.htpasswd admin - Edit
admin/config.phpwith the PostgreSQL connection string - Add the nginx
/admin/location block, reload nginx - Visit
https://relay.yourdomain.com/admin/
No relay restart required. The PHP admin page runs entirely in parallel with the existing relay and Nostr admin API.