Honest version history. Feature flags, design rollbacks, security fixes — all of it. If we shipped something only to walk it back, you'll see that here too.
Added 56 community blocklists across 5 new categories (AI scrapers, safety, additional regional/language sets). Closes the "smaller catalog than NextDNS" gap that was a real spec-sheet objection.
Integrity pass on the marketing site: removed claims we couldn't back — "ISO 27001 certified" (we're not), "HIPAA BAA on every plan" (we don't offer one), "FedRAMP in process" (we aren't), "4,200+ security teams worldwide" (zero customers today), the fabricated company timeline with the $74M Series B, and the named testimonial from a fictional Director of Security. Replaced with honest current-state copy and a design-partner call. Pre-customer / pre-revenue is the truth; the site now says so.
End-to-end test now reads DOM nodes directly (not regex over innerText), asserts exactly 3 QA devices with expected DoH URL shape, clicks the device "copy" button and reads navigator.clipboard back, parses the expanded audit row's <pre> as JSON, and runs a separate viewer-role test that PUT /policies/filtering must 403.
Added scripts/qa_data_seed.sh so the 24h analytics window stays full across days.
Alpine's x-for requires a single root per iteration. A leftover nested <template> meant no rows ever rendered, only the header. Replaced with <tbody> per row (HTML allows multiple tbody per table). Caught by the hardened Playwright spec from v0.33.
require_scopeThe scope gate returned Ok unconditionally for any human JWT and assumed "role checks apply instead" — but no policy handler called require_admin. A viewer JWT could PUT /policies/filtering, /policies/custom-rules, etc.
Fixed: human admins still pass every scope; viewers now only pass :read scopes. :write from a non-admin JWT returns 403. Added a regression unit test and the viewer Playwright test from v0.33 that surfaced it.
/versionSince v0.23 introduced per-device DoH (<tenant_uuid>--<slug>), the where_client helper's exact-match check missed every per-device row. Summary, top-domains, top-clients, top-ai-tools, blocked, by-reason, latency, block-rate all undercounted. Switched to LIKE '<tenant_uuid>%'.
Also: new GET /version returns git SHA + build timestamp so ops can verify what's actually running.
Three new read-only screens for surfaces that previously only existed in the API:
Bundled with the v0.23 device routing fix (AGH rejected / in ClientID; switched separator to --).
x-required-scopes + 5 MCP workflow toolsThe 5-minute reconcile cron now keeps the AGH global filter pool aligned with per-tenant subscriptions automatically (the v0.25 endpoint was admin-only manual). OpenAPI spec gains x-required-scopes on every secured operation. New high-level MCP workflow tools wrap common multi-step operations (onboard-tenant, triage-typosquat) for AI agents.
Two inline classifiers run at ingest with no vendor feeds:
/analytics/top-dga; tunable threshold (default 0.7); no auto-block./analytics/top-typosquat."Scenario A" — chosen over a vendor-feeds option for explainability + zero per-query cost. Future scenarios B/C remain on the table for higher-cost / higher-recall workloads.
The filter-list IDs that drive NRD/TLD/parked toggles were hardcoded in policies.rs. Moved to a feature_flags table so they can be migrated, re-numbered, and disabled per environment without a redeploy.
POST /blocklists/reconcile — drift-correct AGH from Postgres truthAGH's loaded filters[] is a single global list shared across tenants; per-tenant subscriptions live in Postgres. Reconcile diffs them and applies adds/removes idempotently. Admin-only (tenant 1). Pairs with the cron in v0.28.
Per-tenant toggle that flips DNS resolution from "block what's bad" to "allow only what's explicitly permitted." Implemented via a catch-all wildcard block rule with the tenant suffix, layered under existing per-tenant allow entries. The "Fortune-500 healthcare desktop" mode.
Devices are first-class: each gets a unique slug + display name, and AGH ClientID becomes <tenant_uuid>--<slug>. Every analytics endpoint can now answer "which device looked up X?" Pairs with the dashboard Devices view shipped in v0.29.
Note: v0.23 originally used / as the separator, which AGH rejects as an invalid hostname-label character. Fixed in v0.29 to use --; per-device queries were SERVFAIL until then. Caught by end-to-end QA, not by a customer.
Every audit event can fan out to a webhook endpoint. Bodies signed with HMAC-SHA256 over the payload using a per-webhook secret; timestamp + event-name headers; wildcard event matching (policy.*, threat.*, etc.). Backed by a single delivery worker with retry-with-backoff.
Three GDPR/DPDP-flavored controls per tenant:
All 33 MCP tools now carry hand-written when / why / sharp edges descriptions instead of auto-generated "Executes GET /api/v1/..." fallbacks. Measurably improves how AI agents (Claude, Cursor, Continue) pick the right tool on the first try.
The four configuration screens (Policies, Categories, Users, Settings) are gone. Configuration happens via REST API or via the MCP server — agents do the typing, humans review via the Audit Log.
−326 LoC of dashboard codeOne-click toggles wrap existing primitives. Turn on newly_registered_domains to subscribe to the Hagezi NRD blocklist; pass a TLD list to block whole suffixes via wildcard rules.
Future toggles (parked-domain, DGA classifier, typosquat) are reserved in the response shape but return null — they need real classifiers and feeds, landing in a future release.
The ControlD-style Redirect feature: override any DNS answer for any domain. Five kinds — A, AAAA, CNAME, NXDOMAIN, REFUSED. Coexists cleanly with custom block/allow rules.
Implemented via AGH's $dnsrewrite modifier joined to the existing per-tenant client tag.
GET /api/v1/analytics/export?format=csv|ndjson for SOC handover and audit prep. Cap 10M rows per callAll 35 API endpoints documented in the OpenAPI 3.1 spec at /api/v1/openapi.json. MCP server uses FastMCP's RouteMap(MCPType.EXCLUDE) to hide eight humans-only operations (login, password change, key mint/rotate/delete, tenant create/delete, user delete) from agents regardless of scope grant.
Agent-native config plane. FastMCP sidecar auto-generates tools from the OpenAPI spec, forwards the caller's X-API-Key on every upstream call, and respects per-key scopes end-to-end. Both stdio (Claude Desktop, Cursor) and Streamable HTTP transports.
Single source of truth for the API. Drives MCP tool generation, external SDK codegen, and human-readable docs.
GET /tenants/me — caller's own tenant (UUID, DoH URL, endpoints)PATCH /tenants/:id — rename + contact_emailPOST /auth/change-password — self-service, JWT-onlydoh_url no longer leaks :8443Foundation for agent delegation. Twelve resource:action scopes (analytics:read, policies:write, etc.); optional expiry; one-call rotation. Every audit row records actor type (user / api_key / system) and actor id — every config change is now traceable to a specific human session or a specific agent token.
Pre-v0.11 keys are auto-promoted to a legacy:full backstop scope for backward compatibility.
Alpine.js + fetch single-page app on login.olladns.com. API and dashboard now live on separate hostnames with proper CORS.
DNSSEC validation enabled on every resolver. sysctl hardening. Cloudflare token scoped to the single zone. fail2ban watching Caddy 4xx storms. Plain port 53 no longer exposed publicly — DDoS amplification surface eliminated.
Earlier versions (v0.1–v0.8) covered the initial DNS plane, AGH integration, ClickHouse ingest, per-tenant policy/blocklist subscriptions, audit logging, and the original v0.7 switch from Vector to a Rust AGH poller. See the git history for the full record.