OSINT aggregation engine that scans an email address, username, domain, IP, or phone number across 500+ sources in parallel, streams results live via Server-Sent Events, and renders them as an interactive force-directed identity graph.
🔗 Live demo: https://swift.micutu.com · Interactive API: https://swift.micutu.com/docs/
- Production-hosted on Ubuntu behind nginx + Cloudflare. The hardened
systemd, backup and atomic-release configuration is prepared in the repository,
but the live recovery/configuration gaps are rollout blockers—not completed
controls. See the current
security audit,live delta,rollout runbookandroadmap. - 25 OSINT plugins orchestrated by a
TaskGroupwith a hard 120 s deadline; results streamed live via SSE as each plugin completes. - Cross-plugin result cache with per-plugin TTL (1 h for volatile threat intel, 24 h for breach data) — cuts external API calls dramatically on repeated scans of the same target.
- GDPR self-service — every user can request an account data bundle
(
GET /account/export) or permanently delete it with confirmation-gatedDELETE /account. - Prometheus
/metricsendpoint (text exposition 0.0.4) — scan counts by status, cache hit/miss ratio, notification attempts, queue depth and DLQ. Bearer token auth viaMETRICS_TOKENenv var so scrapers don't need an admin session. - Multi-channel monitoring for scheduled scans — Discord, Telegram, Slack, email, generic webhook — silent by default, with enriched diff messages that list which sources surfaced new findings. Automatic delivery uses an encrypted PostgreSQL outbox, cross-process leases, bounded retry and an operator-visible dead-letter queue.
- Bounded asynchronous exports — owner-scoped JSON, GraphML, Markdown, HTML and PDF jobs use paged reads, encrypted artifacts/manifests, cross-process leases, hard quotas, integrity-checked downloads, cancellation and expiry.
- Executable authorization inventory — every Vapor method/path has an exact API-key decision, unknown routes fail closed, and CI exercises a randomized 37-request cross-tenant mutation/read matrix plus 11 collection leak checks.
- Blocking supply-chain gate — full-history redacted secret scanning,
local/offline SAST rules with positive controls, dependency-diff policy,
grouped Dependabot coverage, and validated SPDX + CycloneDX SBOM artifacts.
See the
supply-chain runbook. - Real-browser quality gate — Chromium + axe-core exercise WCAG 2.2 AA,
keyboard/focus, reduced motion, DOM-XSS fixtures, production CSP and overflow
at 320, 375, 768 and 1440 px. See the
browser runbook. - Timeline intelligence — normalized RDAP, certificate-transparency,
Wayback, breach and account-creation events with bounded provenance,
confidence, recurrence and conflicting-date review. The responsive UI is
category-filterable and DOM-XSS safe; see the
timeline contract. - Hermetic test suite running on every push (
swift testin CI with in-memory SQLite; 209 tests at the August 2026 audit), SwiftLint enforced, OpenAPI 3 spec rendered as a hosted Swagger UI.
| Layer | Technology |
|---|---|
| Backend | Swift 6.2 toolchain + Vapor 4 (async/await, TaskGroup concurrency) |
| Database | PostgreSQL (Fluent ORM, versioned migrations) |
| Frontend | Vanilla JS + Tailwind CSS + D3.js v7 + Leaflet |
| Reverse proxy | nginx (rate limiting, CSP, HSTS, security headers) |
| Deployment | systemd (swift-vapor.service) on Ubuntu VPS, optional Docker Compose |
| Network | Cloudflare DNS + proxy (DDoS shield, TLS termination) |
| Observability | Prometheus-compatible /metrics, structured swift-log |
| Raw SMTP (Mailcow / SendGrid compatible) | |
| Tests + Lint | XCTest, SwiftLint, GitHub Actions CI |
Browser
│
▼
Cloudflare (TLS termination, DDoS protection)
│
▼
nginx (rate limiting, CSP, HSTS, XSS headers)
│ /api/* → strip prefix → Vapor :8085
│ /* → static files (frontend/)
▼
Vapor 4
├── Middleware: Sessions, APIKey, CSRF, CORS, NoCache
├── Controllers
│ ScanController, StatsController, AuthController, UserController,
│ AdminController, APIKeyController, BulkScanController,
│ CorrelationController, DiffController, TimelineController, ExportController, ExportJobController,
│ HealthController, NotificationController, ReportController,
│ ScheduledScanController, ShareController, TagController
└── Plugin Pipeline — parallel TaskGroup, 120s timeout
Email → GravatarCheck, HaveIBeenPwned, Pastebin, BulkEmailOSINT (holehe)
Username → GitHub, GitLab, Reddit, Twitter, Keybase, Telegram,
Mastodon, HackerNews, Steam, Npm, PyPI, BulkOSINT (Sherlock 481)
Domain/IP → DomainOSINT (DNS+WHOIS+geo), CertificateTransparency (crt.sh),
PassiveDNS, Shodan, VirusTotal, AbuseIPDB, WHOIS
Phone → PhoneOSINT (AbstractAPI)
│
▼
PostgreSQL
├── users (id, username, email, password_hash, is_admin,
│ retention_days, webhook_url, notify_*, created_at)
├── scans (id, input, status, created_at, completed_at, user_id FK)
├── results (id, scan_id, source, type, confidence_score, raw_data)
├── tags / scan_tags
├── scheduled_scans
├── scan_notifications
├── notification_outbox_events (encrypted, idempotent producer event)
├── notification_delivery_jobs (per-channel lease/retry/DLQ state)
├── export_jobs (encrypted artifact + manifest, lease/progress/expiry)
├── api_keys (SHA-256 hashed token)
├── shared_reports (hashed token, expires_at)
└── audit_logs
- Email — Gravatar, HaveIBeenPwned (breaches + pastes), Pastebin, holehe (400+ sites)
- Username — GitHub (enriched: name/bio/location/Twitter), GitLab, Reddit, Twitter/X, Keybase, Telegram, Mastodon, HackerNews, Steam, npm, PyPI, Sherlock (481 sites)
- Domain / IP — DNS A/MX/TXT/SPF + reverse PTR + WHOIS + IP geolocation, certificate transparency (crt.sh), passive DNS, Shodan, VirusTotal, AbuseIPDB
- Phone — E.164 detection, carrier + country via AbstractAPI
- Parallel execution — all plugins run concurrently in a Swift
TaskGroupwith a 120 s hard timeout - Risk score — confidence-weighted aggregate (Low / Medium / High / Critical) with breach + credential findings weighted 3×
- Register / Login / Logout — session-based (HttpOnly + Secure + SameSite=Strict cookie)
- Admin seeded from
ADMIN_USERNAME+ADMIN_PASSWORDon first start - My Scans — authenticated users see their own scan history
- Admin panel (
/admin.html) — full scan history + per-user stats - BCrypt password hashing (cost factor 12)
- Auth rate limiting on
/auth/loginand/auth/register - CSRF middleware on state-changing requests
- Per-user API keys, stored as SHA-256 hashes (cleartext shown once on creation)
- Bearer auth via
Authorization: Bearer <key>with deny-by-default route authorization - Six least-privilege scopes:
scans:read,scans:write,automation:read,automation:write,investigations:read,investigations:write - Keys expire after 1–365 days (90 by default); account/admin/auth controls are always browser-session-only, regardless of scope
- The exact method/path policy is checked against Vapor's live route registry;
see
docs/ROUTE_AUTHORIZATION.md - Issue / revoke endpoints under
/api/auth/api-keys
Security migration note: API keys created before scoped expiry support have no expiry timestamp and are rejected after upgrade. Reissue them deliberately.
- Scheduled scans — hourly / daily / weekly recurrence, run via
ScheduledScanRunnerlifecycle hook - Bulk scan — submit a list of inputs in one request, scans run in parallel
- Diff — compare two scans of the same target to see what changed
- Webhook (per-user URL), Discord, Telegram, Slack, SMTP email
- Per-channel toggles stored on the user (
notify_discord,notify_telegram, etc.)
- Generate a public link to any scan; token is stored hashed, expires after a configurable window
- PDF generation server-side via
scripts/generate_report.py
- Owner-scoped JSON, GraphML, Markdown, HTML and PDF jobs with recent-auth or
scans:readAPI-key authorization - Stable UUID pagination, result/source/artifact ceilings and atomic per-user quotas
- Encrypted artifact + completeness manifest with result-set/artifact SHA-256
- Cross-process lease recovery, cooperative subprocess cancellation and 24-hour expiry
- Durable Server-Sent Events — each result has a persistent, per-scan monotonic ID
- Native
Last-Event-IDresume, duplicate-safe rendering and bounded 100-row replay pages - Proxy-safe reconnect boundaries plus automatic fallback to 3-second polling if SSE stays unavailable
- SSE connection limit (30 concurrent per process) via a locked counter
- Stats dashboard — total scans, last 24 h / 7 d activity, top sources bar chart
- Identity graph — D3.js v7 force-directed; centre = target, leaves = sources, edge colour = confidence
- Timeline intelligence — responsive oldest-first account, breach, RDAP, CT and Wayback chronology with confidence, source provenance, recurrence, conflict indicators, category filtering and bounded expansion
- Filters & sorting — by type (social_media, breach_data, dns_record, …), confidence, source A–Z
- Exports — CSV, JSON, PDF
- Dark / light mode, keyboard shortcuts, skip navigation, reduced-motion support and mobile-responsive layouts validated at four viewport widths
- Share link, force-rescan (bypass cache), input-type auto-detection (EMAIL / USERNAME / DOMAIN / PHONE)
- Scan history in localStorage (last 20 scans)
- CSP with inline-script hash,
'self'-only sources Strict-Transport-Security,X-Frame-Options,X-Content-Type-Options,Referrer-Policy,X-Robots-Tag: noindex- nginx rate limiting: 10 req/s on
/scan, 30 req/s on all API routes - Input sanitisation: character whitelist + 255-char limit before any plugin runs
rawDatacapped at 8 KB,source/typeat 64 UTF-8 bytes,confidenceScoreclamped[0.0, 1.0]- All plugins use
URLSession(Foundation) — safe from Vapor lifecycle races - Holehe subprocess bounded by a 60 s kill timer via
DispatchSemaphore - PII masked in audit logs:
***@domain.com,use*** - API keys & share-report tokens stored hashed, never in plaintext
- Session IDs rotate after password and 2FA authentication; the old server row
is deleted.
__Host-cookies expire after 7 days with a 24-hour idle limit. - Sensitive controls use a 10-minute step-up window refreshed via
POST /api/auth/reauth(password plus TOTP/recovery code when 2FA is enabled). - Disabling 2FA requires the password and a fresh TOTP/recovery code, consumes the factor once, and rotates the authenticated session.
- Versioned AES-256-GCM envelopes for scan targets/results, investigation boards, notification credentials, scheduler targets, notifications, export artifacts/manifests, audit details, and plugin-cache payloads. V2 derives separate encryption and blind- index keys with HKDF-SHA256, embeds a key ID, and authenticates the storage field plus row UUID as AAD. Readers accept v1 and v2 during staged rotation; production refuses to boot without a valid keyring and verifies a persistent marker before serving traffic.
- SMTP header-injection guard (CRLF stripped from
From/To/Subject) - Per-user
retention_days+ dailyScanCleanupLifecycle(default 30 d)
The full OpenAPI 3 spec is served at /openapi.yaml and rendered as an
interactive explorer at /docs/ (Swagger UI, bundled locally — no CDN).
Open it in a browser to try requests against the live API straight from the
page (cookie auth is persisted across reloads).
POST /api/scan— start scan or return cached result.{ "input": "...", "force": false }GET /api/stream/:id— resumable SSE stream ofPluginResultevents (Last-Event-IDsupported)GET /api/results/:id— full scan resultGET /api/identity/:id— synthesized identity with rich timeline eventsGET /api/scans/:id/timeline— bounded chronology, confidence, provenance, breach recurrence and conflicting-date summaryGET /api/scans/:id/exposure-diff— owner-scoped attack-surface deltaGET /api/stats— aggregate stats
POST /api/auth/registerPOST /api/auth/loginPOST /api/auth/logoutPOST /api/auth/reauth— refresh recent authentication for sensitive actionsPOST /api/auth/2fa/disable— requires both the current password and a fresh TOTP/recovery codeGET /api/auth/me
GET /api/my-scans— last 50 scans for the authenticated user (input masked)GET /api/admin/scans— last 100 scans across all users (admin only)GET /api/admin/notification-deliveries— bounded delivery/DLQ metadata (recent-auth admin only)POST /api/admin/notification-deliveries/:id/retry— audited DLQ replayPOST /api/export-jobs— queue a bounded owner-scoped exportGET /api/export-jobs/:id— progress and bounded failure statusGET /api/export-jobs/:id/manifest— completeness and integrity manifestGET /api/export-jobs/:id/download— authenticated, expiring attachmentPOST /api/export-jobs/:id/cancel— cancellation with publish-safe CAS
/api/auth/api-keys, /api/scheduled-scans, /api/scan/bulk, /api/share, /api/diff,
/api/correlate, /api/tags, /api/notifications, /api/export, /api/export-jobs, /api/report/:id, /api/health
Bulk scans, recurring scans, heavy plugins, and watched-board monitoring require a verified account. Bulk requests accept at most 10 unique targets and are capped at 2 requests/minute and 20/hour per account.
cp .env.docker.example .env # then edit values (passwords + ENCRYPTION_KEY)
docker compose up -d --build # builds image, starts Postgres + app
open http://localhost:8085The app container runs read-only, dropped capabilities, non-root user, with /tmp on tmpfs.
Postgres data persists in the pgdata named volume.
Encryption rollout is reader-first: leave ENCRYPTION_WRITE_VERSION=1 for the
first deployment, give the active key a stable ENCRYPTION_KEY_ID, and switch
writes to 2 only after every web/worker process runs the dual reader. During a
key change, expose the old root only through the bounded
ENCRYPTION_PREVIOUS_KEYS=id=64hex keyring. Treat that variable exactly like the
active key; it must not be committed, logged, or retained after verified rewrap.
The resumable command, verification gates, and v1 rollback sequence are in
docs/ENCRYPTION_KEY_ROTATION.md.
- Swift 6.2 toolchain
- PostgreSQL
- Python 3 venv populated from the hash-locked
requirements-runtime.txt(fpdf2reports + holehe)
python3 -m venv .venv
.venv/bin/pip install --require-hashes --requirement requirements-runtime.txtgit clone https://github.com/Alexandru2984/Digital-Footprint-Tracker
cd Digital-Footprint-Tracker
install -m 600 /dev/null .env # then add the values below.env — minimal set:
PORT=8085
DATABASE_HOST=localhost
DATABASE_USERNAME=footprint_user
DATABASE_PASSWORD=your_password
DATABASE_NAME=footprint_db
ENCRYPTION_KEY=64_hex_characters_generated_with_openssl_rand_hex_32
ENCRYPTION_KEY_ID=primary
ENCRYPTION_WRITE_VERSION=1
AUDIT_SIGNING_KEY=another_independent_64_hex_character_key
AUDIT_SIGNING_KEY_ID=audit-primary
AUDIT_COMMITMENT_KEY=a_third_independent_stable_64_hex_character_key
HOLEHE_PATH=/home/micu/swift+vapor/.venv/bin/holehe
REPORT_PYTHON_PATH=/home/micu/swift+vapor/.venv/bin/python3
ADMIN_USERNAME=admin
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=your_strong_password
# Optional plugin keys:
HIBP_API_KEY=...
ABSTRACT_PHONE_API_KEY=...
SHODAN_API_KEY=...
VIRUS_TOTAL_API_KEY=...
ABUSEIPDB_API_KEY=...
# Optional SMTP for email notifications:
SMTP_HOST=...
SMTP_PORT=587
SMTP_USER=...
SMTP_PASS=...
SMTP_FROM=noreply@example.com
# Optional bounded durable-delivery tuning:
NOTIFICATION_WORKER_ENABLED=true
NOTIFICATION_MAX_ATTEMPTS=5
NOTIFICATION_POLL_SECONDS=2
NOTIFICATION_LEASE_SECONDS=60
NOTIFICATION_RETENTION_DAYS=30
# Optional bounded asynchronous-export tuning:
EXPORT_WORKER_ENABLED=true
EXPORT_POLL_SECONDS=2
EXPORT_LEASE_SECONDS=120
EXPORT_RETENTION_HOURS=24
EXPORT_MAX_OUTSTANDING_PER_USER=3
EXPORT_MAX_JOBS_PER_USER_PER_DAY=20
EXPORT_MAX_RESULTS=10000
EXPORT_BATCH_SIZE=250
EXPORT_MAX_SOURCE_MIB=10
EXPORT_MAX_ARTIFACT_MIB=20
EXPORT_MAX_ATTEMPTS=2
Every secret consumed by the application also accepts a file-backed form such
as DATABASE_PASSWORD_FILE, ENCRYPTION_KEY_FILE, AUDIT_SIGNING_KEY_FILE,
METRICS_TOKEN_FILE, SMTP_PASS_FILE and <PLUGIN>_API_KEY_FILE. Configure
the inline value or its _FILE companion, never both. Credential files must be
absolute, non-symlink, mode 0600/0400 regular files containing one bounded
UTF-8 value. Production uses systemd encrypted credentials; the flat .env
form is intended for local development only.
Automatic notification semantics, rollout and recovery are documented in
docs/NOTIFICATION_DELIVERY.md.
Signed-ledger configuration, verification and key rotation are documented in
docs/AUDIT_INTEGRITY.md; the independent signing
and commitment keys are mandatory in production and must not be reused as
ENCRYPTION_KEY or as each other.
Asynchronous export bounds, state transitions, rollout and rollback are in
docs/ASYNC_EXPORTS.md.
swift build -c release
swift run Run serve # migrations run automatically on startupswift test --enable-test-discoverySources/App/
├── Controllers/ — 16 controllers (Auth, Scan, Stats, Admin, APIKey,
│ BulkScan, Correlation, Diff, Export, Health,
│ Notification, Report, ScheduledScan, Share,
│ Tag, User)
├── Middleware/ — APIKeyMiddleware, CSRFMiddleware
├── Migrations/ — 18 migrations
├── Models/ — User, Scan, Result, Tag, ScanTag, ScheduledScan,
│ ScanNotification, APIKey, SharedReport, AuditLog
├── Plugins/ — 25 OSINT plugins + sherlock_data.json (481 sites)
├── Services/ — AuditLogger, AuthRateLimiter, EmailService,
│ NotificationDispatcher, RiskScorer, TokenEncryption,
│ ScanCleanupLifecycle, ScanProgressTracker,
│ ScanRateLimiter, ScheduledScanRunner, NoCacheMiddleware
├── configure.swift
└── routes.swift
frontend/
├── index.html — single-page app
├── admin.html / admin.js — admin dashboard
├── login.html / register.html
├── openapi.yaml — full API spec
├── tailwind.css — compiled Tailwind (SRI hash in HTML)
└── d3.min.js — D3.js v7 (local, no CDN)
scripts/
└── generate_report.py — server-side PDF report generator
Tests/AppTests/AppTests.swift — 27 XCTests
- Create
Sources/App/Plugins/YourPlugin.swift:
struct YourPlugin: FootprintPlugin {
let name = "YourSource"
func scan(input: String, on app: Application) async throws -> [PluginResult] {
// return [] to skip, or [PluginResult(...)] for a hit
}
}- Add it to the
pluginsarray inScanController.swift. Plugins run concurrently — no further wiring required.
Production uses immutable, commit-named bundles under
/srv/swift-vapor/releases/ and a single /srv/swift-vapor/current symlink
shared by systemd and nginx. A restricted key for the dedicated swift-deploy
identity forces a root-owned copy of scripts/deploy.sh; its isolated source
checkout lives under /var/lib/swift-deploy, while the personal checkout is
never served or deployed. The script refuses dirty/diverged source, requires a recent verified encrypted
backup, builds from git archive, runs migrations through a sandboxed oneshot
unit, transitions CSP hashes, switches the symlink, and verifies both backend
and served frontend. A failure restores the prior release automatically
(database migrations are intentionally never auto-reverted).
The initial conversion from the legacy live checkout is a privileged maintenance
operation. Follow docs/PRODUCTION_ROLLOUT.md; do not point nginx/systemd at the
new symlink until its release manifest passes scripts/release-lib.sh validation.
scripts/backup.sh streams pg_dump | gzip directly into authenticated
AES-256 GPG encryption, then verifies decryption and gzip integrity before the
partial file becomes a retained backup. No plaintext dump is written to disk.
The dedicated swift-backup identity writes mode-0600 artifacts under
/var/lib/swift-vapor-backup/artifacts/ and keeps seven generations by default
(BACKUP_RETENTION accepts 1 through 365). swift-deploy can list/stat these
files for the deployment gate through swift-backup-check, but cannot read
their encrypted contents. The hardened timer runs daily at 02:00 UTC with a
10-minute jitter and catches up after downtime.
First store a portable, 32-character-or-longer passphrase in an owner-controlled
password manager/offline recovery kit. A host-bound encrypted credential alone
is not recoverable after loss of the VPS. Stage the saved value in /run,
encrypt it with the exact credential name, remove plaintext immediately, and
install the dedicated identity, storage, script and units. Before enabling the
timer, /etc/swift-vapor/app.env must contain only the non-secret database
routing settings from the committed template, and the encrypted
database-password credential from the production rollout must already exist:
sudo systemd-sysusers ops/sysusers.d/swift-vapor.conf \
ops/sysusers.d/swift-vapor-backup.conf
sudo systemd-tmpfiles --create ops/tmpfiles.d/swift-vapor.conf \
ops/tmpfiles.d/swift-vapor-backup.conf
sudo install -d -o root -g root -m 0700 /run/swift-vapor-credential-staging \
/etc/credstore.encrypted
sudoedit /run/swift-vapor-credential-staging/backup-passphrase
sudo chmod 0600 /run/swift-vapor-credential-staging/backup-passphrase
sudo systemd-creds encrypt --name=backup-passphrase \
/run/swift-vapor-credential-staging/backup-passphrase \
/etc/credstore.encrypted/swift-vapor-backup-passphrase
sudo chmod 0600 /etc/credstore.encrypted/swift-vapor-backup-passphrase
sudo rm -f /run/swift-vapor-credential-staging/backup-passphrase
sudo install -d -o root -g root -m 0755 /usr/local/libexec/swift-vapor
sudo install -o root -g root -m 0755 scripts/backup.sh scripts/check-backup.sh \
/usr/local/libexec/swift-vapor/
sudo install -o root -g root -m 0644 ops/systemd/swift-vapor-backup.service \
ops/systemd/swift-vapor-backup.timer /etc/systemd/system/
sudo test -r /etc/swift-vapor/app.env
sudo test -f /etc/credstore.encrypted/swift-vapor-database-password
sudo systemctl daemon-reload
sudo systemctl enable --now swift-vapor-backup.timer
sudo systemctl list-timers swift-vapor-backup.timerDo not duplicate the database password plaintext into app.env. Trigger an
on-demand backup and verify it as the narrowly scoped checker identity:
sudo systemctl start swift-vapor-backup.service
sudo journalctl -u swift-vapor-backup.service -n 30 --no-pager
sudo -u swift-deploy /usr/local/libexec/swift-vapor/check-backup.sh \
--directory /var/lib/swift-vapor-backup/artifacts \
--status-file /var/lib/swift-vapor-backup/status/last-successThe job publishes /var/lib/swift-vapor-backup/status/last-success only after
authenticated decryption and gzip verification succeed. The read-only checker
also verifies that the newest dump exists, is private, non-trivial and recent.
This is a freshness gate, not a restore test.
Existing .sql.gz files are plaintext and are deliberately not deleted or
rewritten by the new job. Re-encrypt and verify them during the controlled
rollout. Copy a freshly restore-tested encrypted artifact and its recovery
manifest to an owner-approved immutable off-host destination; local rotation
alone does not cover disk loss or total host compromise.
/metrics exposes counters + gauges in Prometheus text format
(text/plain; version=0.0.4). Set METRICS_TOKEN in .env to a random
secret, then point your Prometheus at it:
scrape_configs:
- job_name: swift-vapor
metrics_path: /metrics
scheme: https
static_configs:
- targets: [swift.micutu.com]
authorization:
type: Bearer
credentials: <value of METRICS_TOKEN from .env>Available series: swift_vapor_scans, swift_vapor_scans_by_status{status="..."},
swift_vapor_scans_last_24h, swift_vapor_users, swift_vapor_results,
swift_vapor_scheduled_scans_active, swift_vapor_plugin_cache_rows,
swift_vapor_plugin_cache_hits_total, swift_vapor_plugin_cache_misses_total,
swift_vapor_notification_deliveries_total{channel="...",outcome="..."},
swift_vapor_notification_job_transitions_total{status="..."},
swift_vapor_notification_jobs_pending, swift_vapor_notification_jobs_processing,
swift_vapor_notification_jobs_dead_letter, swift_vapor_notification_expired_leases,
swift_vapor_notification_oldest_pending_age_seconds,
swift_vapor_export_jobs_pending, swift_vapor_export_jobs_processing,
swift_vapor_export_jobs_failed, swift_vapor_export_expired_leases,
swift_vapor_export_oldest_pending_age_seconds,
swift_vapor_export_jobs_total{status="..."},
swift_vapor_backup_last_success_unixtime, swift_vapor_backup_age_seconds
and swift_vapor_backup_fresh. Starter availability and backup alerts live in
ops/prometheus/swift-vapor-alerts.yml; validate and load them through the
Prometheus rule-file mechanism used by your monitoring installation.
If METRICS_TOKEN is unset, the endpoint falls back to requiring an admin
session cookie (legacy behaviour).
Built with Swift + Vapor · Deployed on a hardened Ubuntu VPS · Protected by Cloudflare