From 301402e2b15c09f08a790b6ccacd889e5bd6c606 Mon Sep 17 00:00:00 2001 From: Aiden Smith <29802327+DevVoxel@users.noreply.github.com> Date: Tue, 24 Feb 2026 18:23:09 -0500 Subject: [PATCH] Add documentation pages with interactive architecture diagram - /docs route with sidebar navigation and index page - Architecture section: system overview with React Flow diagram, data flow, database schema - API reference, configuration guide, and self-hosting docs for Go DNS server - Feature planning document for future development - Added docs link to landing page nav --- app/docs/api/page.tsx | 412 +++++++++++++++++++++++ app/docs/architecture/data-flow/page.tsx | 311 +++++++++++++++++ app/docs/architecture/database/page.tsx | 314 +++++++++++++++++ app/docs/architecture/page.tsx | 215 ++++++++++++ app/docs/configuration/page.tsx | 398 ++++++++++++++++++++++ app/docs/layout.tsx | 72 ++++ app/docs/page.tsx | 73 ++++ app/docs/self-hosting/page.tsx | 343 +++++++++++++++++++ app/page.tsx | 11 +- bun.lock | 39 +++ components/docs/architecture-diagram.tsx | 193 +++++++++++ components/docs/sidebar.tsx | 85 +++++ docs/feature-plan.md | 111 ++++++ package.json | 1 + 14 files changed, 2577 insertions(+), 1 deletion(-) create mode 100644 app/docs/api/page.tsx create mode 100644 app/docs/architecture/data-flow/page.tsx create mode 100644 app/docs/architecture/database/page.tsx create mode 100644 app/docs/architecture/page.tsx create mode 100644 app/docs/configuration/page.tsx create mode 100644 app/docs/layout.tsx create mode 100644 app/docs/page.tsx create mode 100644 app/docs/self-hosting/page.tsx create mode 100644 components/docs/architecture-diagram.tsx create mode 100644 components/docs/sidebar.tsx create mode 100644 docs/feature-plan.md diff --git a/app/docs/api/page.tsx b/app/docs/api/page.tsx new file mode 100644 index 0000000..5da51e3 --- /dev/null +++ b/app/docs/api/page.tsx @@ -0,0 +1,412 @@ +import type { Metadata } from "next"; +import { FileCode, ArrowRight, CheckCircle, XCircle } from "lucide-react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Separator } from "@/components/ui/separator"; + +export const metadata: Metadata = { + title: "API Reference", +}; + +function Method({ method }: { method: string }) { + const colors: Record = { + GET: "bg-blue-500/10 text-blue-400 border-blue-500/20", + POST: "bg-green-500/10 text-green-400 border-green-500/20", + }; + return ( + + {method} + + ); +} + +function CodeBlock({ code }: { code: string }) { + return ( +
+      {code.trim()}
+    
+ ); +} + +function StatusBadge({ code }: { code: number }) { + const isOk = code < 400; + return ( + + {isOk ? ( + + ) : ( + + )} + {code} + + ); +} + +export default function ApiReferencePage() { + return ( +
+ {/* Header */} +
+
+ + Go Server + + API Reference +
+

API Reference

+

+ Complete reference for the VectorDNS Go server REST API. +

+
+ + {/* Base URL & Auth */} + + + + Base URL & Authentication + + + +
+

Base URL

+ +
+
+

+ All requests (except{" "} + /health) require an API + key header: +

+ +
+

+ Set{" "} + + API_KEY + {" "} + in your server environment to enable authentication. If left empty, + auth is disabled (not recommended for production). +

+
+
+ + {/* Endpoints */} +
+

Endpoints

+ + {/* DNS Lookup */} + + +
+ + + /dns/lookup + +
+ + Resolve DNS records for a domain. Query one or more record types + in a single request. + +
+ +
+

Request

+ +
    +
  • + + domain + {" "} + + required + {" "} + — The domain to query. +
  • +
  • + + types + {" "} + + optional + {" "} + — Record types to query. Defaults to all 9 supported types if + omitted. +
  • +
  • + + nameserver + {" "} + + optional + {" "} + — Specific nameserver to query. Defaults to system resolver. +
  • +
+
+ +
+
+

Response

+ +
+ +
+
+
+ + {/* DNS Propagation */} + + +
+ + + /dns/propagation + +
+ + Check DNS propagation across multiple public resolvers. Queries + resolvers in parallel and compares results. + +
+ +
+

Request

+ +
    +
  • + + domain + {" "} + + required + {" "} + — The domain to check. +
  • +
  • + + type + {" "} + + required + {" "} + — The record type to check (e.g.{" "} + A,{" "} + MX). +
  • +
  • + + resolvers + {" "} + + optional + {" "} + — List of resolver IPs. Defaults to a built-in list of public + resolvers. +
  • +
+
+ +
+
+

Response

+ +
+ +
+
+
+ + {/* DNSSEC Validate */} + + +
+ + + /dns/validate + +
+ + DNSSEC validation for a domain. Checks the AD flag and verifies + the signature chain. + +
+ +
+

Request

+ +
    +
  • + + domain + {" "} + + required + {" "} + — The domain to validate. +
  • +
+
+ +
+
+

Response

+ +
+ +
+
+
+ + {/* Health */} + + +
+ + /health +
+ + Health check endpoint. No authentication required. Use this for + uptime monitoring. + +
+ +
+
+

Response

+ +
+ +
+
+
+
+ + {/* Errors */} +
+

+ Error Responses +

+

+ All errors return a consistent JSON body: +

+ + + + + + + + + + + + + {[ + { + status: 400, + code: "INVALID_DOMAIN", + desc: "Malformed or missing domain", + }, + { + status: 400, + code: "INVALID_TYPE", + desc: "Unsupported record type", + }, + { + status: 401, + code: "UNAUTHORIZED", + desc: "Missing or invalid API key", + }, + { + status: 500, + code: "DNS_ERROR", + desc: "Upstream DNS query failed", + }, + { + status: 500, + code: "INTERNAL", + desc: "Unexpected server error", + }, + ].map((row) => ( + + + + + + ))} + +
StatusCodeDescription
+ + + {row.code} + {row.desc}
+
+
+
+
+ ); +} diff --git a/app/docs/architecture/data-flow/page.tsx b/app/docs/architecture/data-flow/page.tsx new file mode 100644 index 0000000..d63bea9 --- /dev/null +++ b/app/docs/architecture/data-flow/page.tsx @@ -0,0 +1,311 @@ +import type { Metadata } from "next"; +import { ArrowRightLeft, ArrowDown } from "lucide-react"; +import { + Card, + CardContent, + CardHeader, + CardTitle, + CardDescription, +} from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Separator } from "@/components/ui/separator"; + +export const metadata: Metadata = { + title: "Data Flow", +}; + +const dnsQuerySteps = [ + { + step: "1", + actor: "User", + action: "Submits a domain name in the VectorDNS UI", + detail: "Browser sends a request to a Next.js API route on Vercel.", + }, + { + step: "2", + actor: "Next.js API Route", + action: "Proxies the request to the Go DNS service", + detail: + "Adds the shared API key header and forwards to the Go server over HTTPS.", + }, + { + step: "3", + actor: "Go Microservice", + action: "Resolves DNS records via UDP/TCP", + detail: + "Uses miekg/dns to query resolvers (Google 8.8.8.8, Cloudflare 1.1.1.1) or authoritative nameservers directly.", + }, + { + step: "4", + actor: "DNS Resolvers", + action: "Return DNS records", + detail: + "Authoritative nameservers or recursive resolvers respond with the requested record types.", + }, + { + step: "5", + actor: "Go Microservice", + action: "Returns structured JSON to Next.js", + detail: "Parsed and normalized DNS records are sent back over HTTPS.", + }, + { + step: "6", + actor: "Next.js", + action: "Stores results in Supabase & returns to client", + detail: + "DNS history is written to Supabase for authenticated users. The response is returned to the browser.", + }, +]; + +const monitoringSteps = [ + { + step: "1", + actor: "Go Cron Job", + action: "Triggers on a schedule (daily by default)", + detail: + "A native Go cron scheduler runs inside the VPS process — no serverless timeouts.", + }, + { + step: "2", + actor: "Go Microservice", + action: "Fetches monitored domains from Supabase", + detail: + "Reads the saved_domains table to get the list of domains and their last-known DNS snapshot.", + }, + { + step: "3", + actor: "Go Microservice", + action: "Re-queries DNS for each domain", + detail: + "Performs fresh DNS lookups and diffs the result against the stored snapshot.", + }, + { + step: "4", + actor: "Go Microservice", + action: "Detects changes", + detail: + "If records changed, writes a new entry to dns_history and availability_history.", + }, + { + step: "5", + actor: "Go Microservice", + action: "Triggers notifications", + detail: + "Writes to the notifications table. Next.js picks these up for in-app display; Resend handles email delivery.", + }, +]; + +const whoIsSteps = [ + { + step: "1", + actor: "User", + action: "Requests WHOIS data for a domain", + detail: "Next.js handles this entirely — no Go service involved.", + }, + { + step: "2", + actor: "Next.js API Route", + action: "Calls the whoiser library", + detail: + "whoiser queries the appropriate WHOIS server directly from Vercel.", + }, + { + step: "3", + actor: "Next.js", + action: "Returns parsed WHOIS data to the client", + detail: "Registrar, expiry, nameservers, and registration details.", + }, +]; + +type FlowStep = { + step: string; + actor: string; + action: string; + detail: string; +}; + +function FlowSteps({ steps }: { steps: FlowStep[] }) { + return ( +
+ {steps.map((s, i) => ( +
+
+
+
+ {s.step} +
+ {i < steps.length - 1 && ( +
+
+ +
+ )} +
+
+
+ + {s.actor} + + {s.action} +
+

{s.detail}

+
+
+
+ ))} +
+ ); +} + +export default function DataFlowPage() { + return ( +
+ {/* Page header */} +
+
+ +

Data Flow

+
+

+ How requests travel through VectorDNS — from the browser through + Next.js and the Go DNS service to resolvers, and how results are + stored. +

+
+ + + + {/* DNS query flow */} +
+
+

+ DNS Record Lookup +

+

+ User-initiated DNS query flow. +

+
+ + +
+              User → Next.js API Route → Go DNS API → DNS Resolvers
+            
+
+
+ + + + + +
+ + + + {/* Monitoring flow */} +
+
+

+ Domain Monitoring +

+

+ Scheduled background job — runs on the VPS, no user trigger + required. +

+
+ + +
+              Go Cron → Supabase (read) → DNS Resolvers → Supabase (write) →
+              Notifications
+            
+
+
+ + + + + +
+ + + + {/* WHOIS flow */} +
+
+

+ WHOIS Lookups +

+

+ Handled entirely by Next.js — the Go service is not involved. +

+
+ + +
+              User → Next.js API Route (whoiser) → WHOIS Servers
+            
+
+
+ + + + + +
+ + + + {/* Key design notes */} +
+

+ Key Design Notes +

+
+ + + No direct client → Go + + The browser never calls the Go service directly. All requests go + through Next.js API routes, which validate the session and add + the API key. + + + + + + + Persistent process on VPS + + + Unlike serverless functions, the Go service runs continuously — + enabling native cron jobs and long-running monitoring without + timeout constraints. + + + + + + UDP/TCP, not DoH + + DNS queries use direct UDP/TCP to resolvers or authoritative + nameservers via miekg/dns — faster and more capable than + DNS-over-HTTPS. + + + + + + + Supabase as source of truth + + + Both Next.js and the Go service read/write Supabase. Row Level + Security ensures users only access their own data. + + + +
+
+
+ ); +} diff --git a/app/docs/architecture/database/page.tsx b/app/docs/architecture/database/page.tsx new file mode 100644 index 0000000..8c1e53e --- /dev/null +++ b/app/docs/architecture/database/page.tsx @@ -0,0 +1,314 @@ +import type { Metadata } from "next"; +import { Database } from "lucide-react"; +import { + Card, + CardContent, + CardHeader, + CardTitle, + CardDescription, +} from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Separator } from "@/components/ui/separator"; + +export const metadata: Metadata = { + title: "Database Schema", +}; + +type Column = { + name: string; + type: string; + notes?: string; +}; + +type Table = { + name: string; + description: string; + columns: Column[]; +}; + +const currentTables: Table[] = [ + { + name: "profiles", + description: + "Extends Supabase auth.users with app-specific user preferences and settings.", + columns: [ + { name: "id", type: "uuid", notes: "FK → auth.users" }, + { name: "email", type: "text" }, + { name: "display_name", type: "text", notes: "nullable" }, + { name: "avatar_url", type: "text", notes: "nullable" }, + { name: "created_at", type: "timestamptz" }, + { name: "updated_at", type: "timestamptz" }, + ], + }, + { + name: "saved_domains", + description: + "Domains the user has added to their watchlist for monitoring.", + columns: [ + { name: "id", type: "uuid" }, + { name: "user_id", type: "uuid", notes: "FK → profiles" }, + { name: "domain", type: "text" }, + { name: "tags", type: "text[]", notes: "nullable" }, + { name: "last_checked_at", type: "timestamptz", notes: "nullable" }, + { name: "created_at", type: "timestamptz" }, + ], + }, + { + name: "dns_history", + description: + "Snapshots of DNS records captured at each monitoring check. Used for change detection and history browsing.", + columns: [ + { name: "id", type: "uuid" }, + { name: "saved_domain_id", type: "uuid", notes: "FK → saved_domains" }, + { name: "user_id", type: "uuid", notes: "FK → profiles" }, + { name: "records", type: "jsonb", notes: "Full DNS snapshot" }, + { name: "resolver", type: "text", notes: "e.g. 8.8.8.8" }, + { name: "checked_at", type: "timestamptz" }, + ], + }, + { + name: "availability_history", + description: + "Tracks domain availability (registered / available) over time.", + columns: [ + { name: "id", type: "uuid" }, + { name: "saved_domain_id", type: "uuid", notes: "FK → saved_domains" }, + { name: "user_id", type: "uuid", notes: "FK → profiles" }, + { name: "available", type: "boolean" }, + { name: "checked_at", type: "timestamptz" }, + ], + }, + { + name: "notifications", + description: + "In-app notification feed. Written by the Go monitoring service when changes are detected.", + columns: [ + { name: "id", type: "uuid" }, + { name: "user_id", type: "uuid", notes: "FK → profiles" }, + { name: "saved_domain_id", type: "uuid", notes: "FK → saved_domains" }, + { name: "type", type: "text", notes: "e.g. dns_change, availability" }, + { name: "message", type: "text" }, + { name: "read", type: "boolean" }, + { name: "created_at", type: "timestamptz" }, + ], + }, +]; + +type PlannedTable = { + name: string; + for: string; + description: string; + keyColumns: string[]; +}; + +const plannedTables: PlannedTable[] = [ + { + name: "teams", + for: "Team / org accounts", + description: + "Supports shared watchlists and role-based access for organizations.", + keyColumns: ["id", "name", "owner_id", "created_at"], + }, + { + name: "team_members", + for: "Team membership & roles", + description: + "Maps users to teams with a role (admin / viewer). Enables team-aware RLS policies.", + keyColumns: ["team_id", "user_id", "role", "joined_at"], + }, + { + name: "domain_folders", + for: "Folder organization", + description: + "Lets users organize monitored domains into named folders. saved_domains will gain an optional folder_id FK.", + keyColumns: ["id", "user_id", "name", "created_at"], + }, + { + name: "shared_snapshots", + for: "Public shareable links", + description: + "Stores DNS snapshots accessible via a unique token — no auth required to view.", + keyColumns: [ + "id", + "saved_domain_id", + "share_token", + "dns_snapshot", + "expires_at", + ], + }, + { + name: "notification_channels", + for: "Webhook / Slack / Discord", + description: + "User-configured alert destinations beyond in-app and email. Config stored as JSONB.", + keyColumns: ["id", "user_id", "type", "config", "enabled"], + }, + { + name: "subscriptions", + for: "Billing tier tracking", + description: + "Tracks the active plan (free / pro / team) per user for feature gating.", + keyColumns: ["id", "user_id", "plan", "valid_until", "created_at"], + }, +]; + +function TableCard({ table }: { table: Table }) { + return ( + + +
+ {table.name} + + current + +
+ {table.description} +
+ +
+ + + + + + + + + + {table.columns.map((col) => ( + + + + + + ))} + +
+ Column + + Type + + Notes +
+ {col.name} + + {col.type} + + {col.notes ?? "—"} +
+
+
+
+ ); +} + +export default function DatabaseSchemaPage() { + return ( +
+ {/* Page header */} +
+
+ +

Database Schema

+
+

+ VectorDNS uses Supabase (managed Postgres) with Row Level Security. + There are currently 5 tables in production, with 6 more planned as + features are built out. +

+
+ + + + {/* Current tables */} +
+
+

+ Current Tables +

+ {currentTables.length} tables +
+
+ {currentTables.map((table) => ( + + ))} +
+
+ + + + {/* Planned tables */} +
+
+

+ Planned Tables +

+ {plannedTables.length} planned +
+

+ These tables will be added as the corresponding features are + implemented. Schema details may evolve. +

+
+ {plannedTables.map((table) => ( + + +
+ + {table.name} + + + planned + +
+ + {table.for} + + + {table.description} + +
+ +
+ {table.keyColumns.map((col) => ( + + {col} + + ))} +
+
+
+ ))} +
+
+ + + + {/* RLS note */} +
+

+ Row Level Security +

+ + +

+ All tables have RLS enabled. Policies enforce that users can only + read and write their own rows ( + + user_id = auth.uid() + + ). Future team-aware policies will extend this to allow team + members access to shared resources based on their role. +

+
+
+
+
+ ); +} diff --git a/app/docs/architecture/page.tsx b/app/docs/architecture/page.tsx new file mode 100644 index 0000000..0e8d135 --- /dev/null +++ b/app/docs/architecture/page.tsx @@ -0,0 +1,215 @@ +import type { Metadata } from "next"; +import { Layers, Globe, Server, Database } from "lucide-react"; +import { ArchitectureDiagram } from "@/components/docs/architecture-diagram"; +import { + Card, + CardContent, + CardHeader, + CardTitle, + CardDescription, +} from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Separator } from "@/components/ui/separator"; + +export const metadata: Metadata = { + title: "System Overview", +}; + +const services = [ + { + icon: Globe, + title: "Next.js (Vercel)", + badge: "Frontend", + badgeVariant: "secondary" as const, + items: [ + "All UI rendering (SSR + client)", + "Authentication via Supabase", + "WHOIS lookups (whoiser library)", + "Domain availability checks (IANA RDAP)", + "Dashboard, notifications, settings pages", + "Proxies DNS queries to the Go service", + ], + }, + { + icon: Server, + title: "Go Microservice (VPS)", + badge: "DNS API", + badgeVariant: "outline" as const, + items: [ + "DNS record lookups via miekg/dns (UDP/TCP, not DoH)", + "Query specific or authoritative nameservers directly", + "DNSSEC validation", + "DNS propagation checking across multiple resolvers", + "Scheduled monitoring (native cron, no serverless time limits)", + "Change detection (diff DNS snapshots, notify on changes)", + ], + }, + { + icon: Database, + title: "Supabase", + badge: "Database & Auth", + badgeVariant: "secondary" as const, + items: [ + "Managed Postgres database", + "Auth (OAuth + email/password + magic link)", + "Row Level Security (RLS) for data isolation", + ], + }, +]; + +const tradeoffs = [ + { + concern: "Frontend hosting, SSR, auth", + solution: "Vercel (serverless, zero-ops)", + }, + { + concern: "DNS resolution, monitoring", + solution: "Go on VPS (persistent process, no cold starts)", + }, + { + concern: "Database, auth state", + solution: "Supabase (managed Postgres)", + }, +]; + +const goAdvantages = [ + "Direct UDP/TCP DNS queries — faster, no middleman", + "Can query authoritative nameservers directly", + "Supports DNSSEC validation, AXFR, propagation checks", + "No cold starts, consistent latency", + "No Vercel function timeout limits for monitoring jobs", +]; + +export default function ArchitectureOverviewPage() { + return ( +
+ {/* Page header */} +
+
+ +

System Overview

+
+

+ VectorDNS uses a hybrid architecture: a Next.js frontend on Vercel and + a Go DNS microservice on a VPS, backed by Supabase for auth and + storage. +

+
+ + + + {/* Architecture diagram */} +
+

+ Architecture Diagram +

+ +
+ + {/* Services */} +
+

+ What Each Service Handles +

+
+ {services.map((service) => ( + + +
+ + {service.badge} +
+ {service.title} +
+ +
    + {service.items.map((item) => ( +
  • + + {item} +
  • + ))} +
+
+
+ ))} +
+
+ + + + {/* Why hybrid */} +
+

Why Hybrid?

+ + +
+ {tradeoffs.map(({ concern, solution }) => ( +
+ + {concern} + + + {solution} + +
+ ))} +
+
+
+
+ + {/* Communication */} +
+

Communication

+

+ Next.js API routes call the Go service over HTTPS. The Go service URL + is configured via{" "} + + GO_DNS_API_URL + {" "} + and requests are authenticated with a shared API key via{" "} + + GO_DNS_API_KEY + + . +

+ + +
+              Next.js API route → HTTPS → Go DNS API → UDP/TCP → DNS resolvers
+            
+
+
+
+ + {/* Why Go over DoH */} +
+

+ Why Go Over DoH? +

+ + + + VectorDNS uses a custom Go microservice for DNS resolution instead + of DNS-over-HTTPS providers like Cloudflare's Tangerine. + + + +
    + {goAdvantages.map((item) => ( +
  • + + {item} +
  • + ))} +
+
+
+
+
+ ); +} diff --git a/app/docs/configuration/page.tsx b/app/docs/configuration/page.tsx new file mode 100644 index 0000000..bf48330 --- /dev/null +++ b/app/docs/configuration/page.tsx @@ -0,0 +1,398 @@ +import type { Metadata } from "next"; +import { + Settings, + ArrowRight, + Shield, + Globe, + Gauge, + Database, +} from "lucide-react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Separator } from "@/components/ui/separator"; + +export const metadata: Metadata = { + title: "Configuration", +}; + +function CodeBlock({ code }: { code: string }) { + return ( +
+      {code.trim()}
+    
+ ); +} + +function EnvVar({ + name, + defaultVal, + required, + description, +}: { + name: string; + defaultVal: string; + required?: boolean; + description: string; +}) { + return ( + + + {name} + + + {defaultVal ? ( + + {defaultVal} + + ) : ( + + )} + + + {required ? ( + + required + + ) : ( + + optional + + )} + + + {description} + + + ); +} + +export default function ConfigurationPage() { + return ( +
+ {/* Header */} +
+
+ + Go Server + + Configuration +
+

Configuration

+

+ All server configuration is done via environment variables or a{" "} + .env{" "} + file in the project root. +

+
+ + {/* .env example */} + + + Example .env + + + +

+ Copy{" "} + + .env.example + {" "} + to{" "} + + .env + {" "} + to get started:{" "} + + cp .env.example .env + +

+
+
+ + {/* Environment Variables reference */} +
+

+ Environment Variables +

+ + + + + + + + + + + + + + + + + +
VariableDefaultDescription
+
+
+
+ + + + {/* API Key Auth */} +
+
+ +

+ API Key Authentication +

+
+

+ The Go server uses a shared API key for authentication. This is + intentional: the server is designed to be an internal service called + only by your Next.js backend — not directly by end users. +

+ + + How it works + + +

+ When{" "} + + API_KEY + {" "} + is set, every request (except{" "} + + GET /health + + ) must include the header: +

+ +

+ Requests missing or sending an incorrect key receive a{" "} + + 401 UNAUTHORIZED + {" "} + response. +

+ +
+
+ + + Calling from Next.js + + + +

+ Store{" "} + + DNS_API_KEY + {" "} + as a secret environment variable in your Next.js deployment. Never + expose it client-side. +

+
+
+
+ + + + {/* CORS */} +
+
+ +

CORS

+
+

+ CORS is handled by{" "} + + go-chi/cors + + . Configure allowed origins via the{" "} + + CORS_ORIGINS + {" "} + variable. +

+ + +

Development

+ +

Production (single domain)

+ +

Production (multiple domains)

+ +
+
+

+ Since the Go server is an internal API called server-side by Next.js, + CORS is mostly relevant for development. In production, only your + Next.js server IP/domain needs access. +

+
+ + + + {/* Rate Limiting */} +
+
+ +

+ Rate Limiting +

+
+

+ Rate limiting is provided by{" "} + + go-chi/httprate + {" "} + and is enabled by default. +

+ + + Current defaults + + +
    +
  • • Rate limiting is applied per IP address.
  • +
  • • Limits are enforced at the router middleware level.
  • +
  • + • Requests exceeding the limit receive{" "} + + 429 Too Many Requests + + . +
  • +
+
+
+ + +
+ + Per-API-key rate limiting + + + Planned + +
+ + When billing tiers are added, rate limits will be enforced per API + key to match the user`'`s plan (free vs. pro). + +
+ +
    +
  • • Free tier: limited requests per day
  • +
  • • Pro tier: higher limits, hourly checks
  • +
  • • Team tier: highest limits, shared across org
  • +
+
+
+
+ + + + {/* Redis Caching */} +
+
+ +

+ Redis Caching +

+ + Planned + +
+

+ DNS result caching via Redis is on the roadmap. When available, the + server will check Redis before querying upstream DNS, storing results + with TTL-based expiry. +

+ + + + Planned Redis configuration + + + + +

+ The cache layer will use{" "} + + go-redis + {" "} + and sit as middleware before DNS resolution. A{" "} + + docker-compose.yml + {" "} + bundling the Go server with Redis will be provided. +

+
+
+
+
+ ); +} diff --git a/app/docs/layout.tsx b/app/docs/layout.tsx new file mode 100644 index 0000000..33a2145 --- /dev/null +++ b/app/docs/layout.tsx @@ -0,0 +1,72 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { Globe, BookOpen, Menu } from "lucide-react"; +import { ThemeToggle } from "@/components/theme-toggle"; +import { DocsSidebar } from "@/components/docs/sidebar"; + +export const metadata: Metadata = { + title: { + default: "Documentation", + template: "%s | VectorDNS Docs", + }, +}; + +export default function DocsLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+ {/* Header */} +
+
+
+ + + + VectorDNS + + + / + + + Docs + +
+ +
+
+ +
+ {/* Mobile sidebar toggle */} + +
+
+ ); +} diff --git a/app/docs/page.tsx b/app/docs/page.tsx new file mode 100644 index 0000000..e819fa4 --- /dev/null +++ b/app/docs/page.tsx @@ -0,0 +1,73 @@ +import Link from "next/link"; +import { Layers, FileCode, Settings, Server } from "lucide-react"; +import { + Card, + CardHeader, + CardTitle, + CardDescription, +} from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; + +const docSections = [ + { + icon: Layers, + title: "Architecture", + description: + "Understand the system design, data flow between the Next.js frontend and Go DNS microservice, and database schema.", + href: "/docs/architecture", + }, + { + icon: FileCode, + title: "API Reference", + description: + "Explore all available API endpoints for DNS lookups, WHOIS queries, and domain monitoring.", + href: "/docs/api", + }, + { + icon: Settings, + title: "Configuration", + description: + "Learn how to configure the DNS server, set environment variables, and customize behavior.", + href: "/docs/configuration", + }, + { + icon: Server, + title: "Self-Hosting", + description: + "Deploy VectorDNS on your own infrastructure with Docker, systemd, or cloud providers.", + href: "/docs/self-hosting", + }, +]; + +export default function DocsPage() { + return ( +
+
+ + Documentation + +

+ VectorDNS Documentation +

+

+ Everything you need to understand, configure, and deploy VectorDNS — + the open-source DNS lookup, WHOIS, and domain monitoring toolkit. +

+
+ +
+ {docSections.map((section) => ( + + + + + {section.title} + {section.description} + + + + ))} +
+
+ ); +} diff --git a/app/docs/self-hosting/page.tsx b/app/docs/self-hosting/page.tsx new file mode 100644 index 0000000..3faedd4 --- /dev/null +++ b/app/docs/self-hosting/page.tsx @@ -0,0 +1,343 @@ +import type { Metadata } from "next"; +import { Server, ArrowRight, Terminal, Package, Cloud } from "lucide-react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Separator } from "@/components/ui/separator"; + +export const metadata: Metadata = { + title: "Self-Hosting", +}; + +function CodeBlock({ code }: { code: string }) { + return ( +
+      {code.trim()}
+    
+ ); +} + +function Step({ + n, + title, + children, +}: { + n: number; + title: string; + children: React.ReactNode; +}) { + return ( +
+
+ {n} +
+
+

{title}

+ {children} +
+
+ ); +} + +export default function SelfHostingPage() { + return ( +
+ {/* Header */} +
+
+ + Deployment + + Self-Hosting +
+

Self-Hosting

+

+ Run the VectorDNS Go server on your own VPS or infrastructure. The DNS + API is a single stateless binary — no database required. +

+
+ + {/* Prerequisites */} + + + Prerequisites + + +
    +
  • + + Docker + + Recommended — no Go toolchain required on the host. +
  • +
  • + + Go 1.22+ + + Required only if building from source. +
  • +
  • + + Port 8080 + + Default port (configurable via{" "} + PORT env var). +
  • +
+
+
+ + {/* Docker (recommended) */} +
+
+ +

+ Docker (Recommended) +

+ + Recommended + +
+ +
+ + + + + + +

+ At minimum, set{" "} + + API_KEY + {" "} + and{" "} + + CORS_ORIGINS + + . See{" "} + + Configuration + {" "} + for all options. +

+
+ + + + + + + +

+ The server will be available at{" "} + + http://localhost:8080 + + . +

+
+ + + +

+ Should return{" "} + {`{"status":"ok","version":"0.1.0"}`} + . +

+
+
+
+ + + + {/* From source */} +
+
+ +

From Source

+
+ +

+ If you prefer to build and run without Docker (requires Go 1.22+): +

+ +
+ + + + + + + + + + + + + + + +
+
+ + + + {/* VPS Deployment */} +
+
+ +

+ VPS Deployment +

+
+ +

+ Deploying to a VPS (e.g. DigitalOcean, Hetzner, Linode)? Here is a + recommended setup. +

+ + + + Reverse proxy with nginx + + Serve the Go server behind nginx so you can add TLS and a clean + domain path. + + + + + + + + + + + systemd service (non-Docker) + + + Keep the server running across reboots without Docker. + + + + +
+ +
+
+
+ + + + + docker-compose (with Redis — near-term) + + + Future: once Redis caching is added, a docker-compose setup will + be provided. + + + + +

+ Redis caching is on the roadmap. This configuration will be + provided when the caching layer ships. +

+
+
+
+ + {/* Security note */} + + + + Security Checklist + + + +
    +
  • + • Set a strong{" "} + + API_KEY + {" "} + — do not leave auth disabled in production. +
  • +
  • + • Set{" "} + + CORS_ORIGINS + {" "} + to your exact frontend domain, not{" "} + *. +
  • +
  • + • Always run behind TLS (use Let`'`s Encrypt via Certbot with + nginx). +
  • +
  • • Rate limiting is enabled by default — keep it on.
  • +
+
+
+
+ ); +} diff --git a/app/page.tsx b/app/page.tsx index b5a9899..870ceae 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,3 +1,4 @@ +import Link from "next/link"; import { Globe, Search, Shield, Activity } from "lucide-react"; import { Card, @@ -42,7 +43,15 @@ export default function Home() { VectorDNS
- +
+ + Docs + + +
diff --git a/bun.lock b/bun.lock index 077899a..31e0184 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "": { "name": "vectordns", "dependencies": { + "@xyflow/react": "^12.10.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.575.0", @@ -418,6 +419,18 @@ "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + + "@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="], + + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], + + "@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="], + + "@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="], + + "@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], @@ -492,6 +505,10 @@ "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="], + "@xyflow/react": ["@xyflow/react@12.10.1", "", { "dependencies": { "@xyflow/system": "0.0.75", "classcat": "^5.0.3", "zustand": "^4.4.0" }, "peerDependencies": { "react": ">=17", "react-dom": ">=17" } }, "sha512-5eSWtIK/+rkldOuFbOOz44CRgQRjtS9v5nufk77DV+XBnfCGL9HAQ8PG00o2ZYKqkEU/Ak6wrKC95Tu+2zuK3Q=="], + + "@xyflow/system": ["@xyflow/system@0.0.75", "", { "dependencies": { "@types/d3-drag": "^3.0.7", "@types/d3-interpolate": "^3.0.4", "@types/d3-selection": "^3.0.10", "@types/d3-transition": "^3.0.8", "@types/d3-zoom": "^3.0.8", "d3-drag": "^3.0.0", "d3-interpolate": "^3.0.1", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0" } }, "sha512-iXs+AGFLi8w/VlAoc/iSxk+CxfT6o64Uw/k0CKASOPqjqz6E0rb5jFZgJtXGZCpfQI6OQpu5EnumP5fGxQheaQ=="], + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], @@ -574,6 +591,8 @@ "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + "classcat": ["classcat@5.0.5", "", {}, "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w=="], + "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], @@ -616,6 +635,24 @@ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + + "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="], + + "d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="], + + "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + + "d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="], + + "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + + "d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="], + + "d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="], + "damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="], "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], @@ -1428,6 +1465,8 @@ "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], + "zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="], + "@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], "@dotenvx/dotenvx/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], diff --git a/components/docs/architecture-diagram.tsx b/components/docs/architecture-diagram.tsx new file mode 100644 index 0000000..f91f790 --- /dev/null +++ b/components/docs/architecture-diagram.tsx @@ -0,0 +1,193 @@ +"use client"; + +import { + ReactFlow, + Background, + type Node, + type Edge, + type NodeProps, + Handle, + Position, +} from "@xyflow/react"; +import "@xyflow/react/dist/style.css"; +import { Globe, Server, Database, Wifi } from "lucide-react"; + +type ServiceNodeData = { + label: string; + subtitle: string; + icon: React.ElementType; + color: string; + items?: string[]; +}; + +function ServiceNode({ data }: NodeProps>) { + const Icon = data.icon; + return ( +
+ + + + +
+
+ +
+
+
+ {data.label} +
+
{data.subtitle}
+
+
+ {data.items && ( +
    + {data.items.map((item) => ( +
  • + + {item} +
  • + ))} +
+ )} +
+ ); +} + +const nodeTypes = { service: ServiceNode }; + +const nodes: Node[] = [ + { + id: "nextjs", + type: "service", + position: { x: 0, y: 0 }, + data: { + label: "Next.js", + subtitle: "Vercel (Frontend)", + icon: Globe, + color: "#3b82f6", + items: ["UI / SSR", "Auth (Supabase)", "WHOIS lookups", "Static pages"], + }, + }, + { + id: "go", + type: "service", + position: { x: 350, y: 0 }, + data: { + label: "Go DNS API", + subtitle: "VPS (Microservice)", + icon: Server, + color: "#10b981", + items: [ + "DNS resolution (miekg/dns)", + "DNSSEC validation", + "Propagation checks", + "Monitoring cron", + ], + }, + }, + { + id: "supabase", + type: "service", + position: { x: 0, y: 230 }, + data: { + label: "Supabase", + subtitle: "Database & Auth", + icon: Database, + color: "#8b5cf6", + items: ["Postgres DB", "Auth (OAuth + email)", "Row Level Security"], + }, + }, + { + id: "dns", + type: "service", + position: { x: 350, y: 230 }, + data: { + label: "DNS Resolvers", + subtitle: "Authoritative NS", + icon: Wifi, + color: "#f59e0b", + items: ["Google (8.8.8.8)", "Cloudflare (1.1.1.1)", "Authoritative NS"], + }, + }, +]; + +const edges: Edge[] = [ + { + id: "nextjs-go", + source: "nextjs", + target: "go", + sourceHandle: "right", + targetHandle: "left", + label: "HTTPS", + animated: true, + style: { stroke: "#3b82f6" }, + labelStyle: { fontSize: 15, fill: "#94a3b8", fontWeight: "bold" }, + labelBgStyle: { fill: "transparent" }, + }, + { + id: "nextjs-supabase", + source: "nextjs", + target: "supabase", + label: "SDK", + style: { stroke: "#8b5cf6" }, + labelStyle: { fontSize: 15, fill: "#94a3b8", fontWeight: "bold" }, + labelBgStyle: { fill: "transparent" }, + }, + { + id: "go-dns", + source: "go", + target: "dns", + label: "UDP/TCP", + animated: true, + style: { stroke: "#10b981" }, + labelStyle: { fontSize: 15, fill: "#94a3b8", fontWeight: "bold" }, + labelBgStyle: { fill: "transparent" }, + }, +]; + +export function ArchitectureDiagram() { + return ( +
+ + + +
+ ); +} diff --git a/components/docs/sidebar.tsx b/components/docs/sidebar.tsx new file mode 100644 index 0000000..eb94a50 --- /dev/null +++ b/components/docs/sidebar.tsx @@ -0,0 +1,85 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { + BookOpen, + Layers, + ArrowRightLeft, + Database, + FileCode, + Settings, + Server, +} from "lucide-react"; + +const sections = [ + { + title: "Getting Started", + items: [{ title: "Overview", href: "/docs", icon: BookOpen }], + }, + { + title: "Architecture", + items: [ + { title: "System Overview", href: "/docs/architecture", icon: Layers }, + { + title: "Data Flow", + href: "/docs/architecture/data-flow", + icon: ArrowRightLeft, + }, + { + title: "Database Schema", + href: "/docs/architecture/database", + icon: Database, + }, + ], + }, + { + title: "API", + items: [ + { title: "API Reference", href: "/docs/api", icon: FileCode }, + { title: "Configuration", href: "/docs/configuration", icon: Settings }, + ], + }, + { + title: "Deployment", + items: [ + { title: "Self-Hosting", href: "/docs/self-hosting", icon: Server }, + ], + }, +]; + +export function DocsSidebar() { + const pathname = usePathname(); + + return ( + + ); +} diff --git a/docs/feature-plan.md b/docs/feature-plan.md new file mode 100644 index 0000000..f6cc69b --- /dev/null +++ b/docs/feature-plan.md @@ -0,0 +1,111 @@ +# VectorDNS Feature Plan + +Decisions captured for current and future development. + +--- + +## Auth & User System + +| Feature | Decision | Priority | +|---|---|---| +| OAuth providers | GitHub + Google | MVP | +| Email/password + magic link | Yes | MVP | +| Team/org accounts | Yes, eventually — shared watchlists, role-based access (admin/viewer) | Future | +| User API keys | No — Go server is internal, called only by the Next.js backend | N/A | + +### Schema implications (teams) +- Future `teams` table with membership + roles +- `saved_domains` will need an optional `team_id` foreign key +- RLS policies will need team-aware variants + +--- + +## DNS & Monitoring + +| Feature | Decision | Priority | +|---|---|---| +| Check intervals | Daily (MVP), then hourly/daily/weekly selectable | MVP → Near-term | +| Realtime monitoring | Not now — possible distant future | Deferred | +| Custom resolvers | Fixed resolvers (Google 8.8.8.8, Cloudflare 1.1.1.1) by default; user-specified resolvers as future feature | MVP → Future | +| Propagation checks | Query multiple resolvers and compare results | Near-term | +| DNSSEC validation | Basic AD flag check (current), deep chain validation later | MVP → Future | + +### Go server implications +- Scheduler needed for flexible intervals (hourly/daily/weekly per domain) +- Resolver list should be configurable, not hardcoded (prep for custom resolvers) +- Propagation endpoint: query N resolvers in parallel, return per-resolver results + +--- + +## Alert Channels + +| Channel | Priority | +|---|---| +| In-app notification feed | MVP | +| Email (Resend) | MVP | +| Webhooks (user-configured URL) | Near-term | +| Slack integration | Future | +| Discord integration | Future | + +### Schema implications +- `notification_channels` table: user_id, type (email/webhook/slack/discord), config (jsonb), enabled +- Webhook: store URL + optional secret for HMAC signing +- Slack/Discord: store webhook URL or OAuth token + +--- + +## Dashboard & UX + +| Feature | Decision | Priority | +|---|---|---| +| Domain organization | Tags (current) + folders/groups | Near-term | +| Public sharing | Shareable links for domain DNS snapshots | Near-term | +| Data export | CSV + JSON for DNS history, saved domains, notifications | Near-term | +| Bulk operations | Add/remove/re-check multiple domains at once | Future | +| Search & filter | Filter by tag, folder, record type, change status | Near-term | + +### Schema implications (folders) +- `domain_folders` table: id, user_id, name, created_at +- `saved_domains` gets an optional `folder_id` foreign key +- RLS policies for folder ownership + +### Schema implications (public sharing) +- `shared_snapshots` table: id, saved_domain_id, share_token (unique), dns_snapshot (jsonb), expires_at, created_at +- Public access via token — no auth required to view + +--- + +## Infrastructure + +| Feature | Decision | Priority | +|---|---|---| +| Billing/paid tiers | Freemium — free tier with limits, paid for more domains/features/intervals | Future | +| DNS caching | Redis — external cache for TTL-based DNS result caching | Near-term | +| Self-hosting | Go server only — provide Dockerfile + docker-compose for the DNS API | Near-term | +| Next.js hosting | Vercel only, not self-hostable | — | + +### Freemium tier structure (draft) +- **Free**: 5 monitored domains, daily checks, in-app + email alerts +- **Pro**: 50 domains, hourly checks, webhooks, folders, export +- **Team**: everything in Pro + team accounts, shared watchlists + +### Go server additions needed +- Redis client (go-redis) for caching layer +- Cache middleware: check Redis before querying DNS, store with TTL +- docker-compose.yml with Go server + Redis for self-hosting +- Rate limiting per API key / tier (when billing is added) + +--- + +## Summary of future schema additions + +These tables will be needed as features are built: + +| Table | For | +|---|---| +| `teams` | Team/org accounts | +| `team_members` | Membership + roles | +| `domain_folders` | Folder organization | +| `shared_snapshots` | Public shareable links | +| `notification_channels` | Webhook/Slack/Discord config | +| `subscriptions` | Billing tier tracking | diff --git a/package.json b/package.json index 84080db..f3ebda5 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "eslint" }, "dependencies": { + "@xyflow/react": "^12.10.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.575.0",