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
This commit is contained in:
Aiden Smith
2026-02-24 18:23:09 -05:00
parent f49bdb7099
commit 301402e2b1
14 changed files with 2577 additions and 1 deletions

View File

@@ -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 (
<div className="space-y-2">
{steps.map((s, i) => (
<div key={s.step}>
<div className="flex gap-4">
<div className="flex flex-col items-center">
<div className="flex size-7 shrink-0 items-center justify-center rounded-full bg-primary text-xs font-bold text-primary-foreground">
{s.step}
</div>
{i < steps.length - 1 && (
<div className="mt-1 flex flex-1 flex-col items-center">
<div className="w-px flex-1 bg-border" />
<ArrowDown className="size-3 text-muted-foreground" />
</div>
)}
</div>
<div className="pb-4">
<div className="flex flex-wrap items-center gap-2">
<Badge variant="outline" className="text-xs">
{s.actor}
</Badge>
<span className="text-sm font-medium">{s.action}</span>
</div>
<p className="mt-1 text-sm text-muted-foreground">{s.detail}</p>
</div>
</div>
</div>
))}
</div>
);
}
export default function DataFlowPage() {
return (
<div className="space-y-10">
{/* Page header */}
<div className="space-y-3">
<div className="flex items-center gap-2">
<ArrowRightLeft className="size-6 text-primary" />
<h1 className="text-3xl font-bold tracking-tight">Data Flow</h1>
</div>
<p className="text-lg text-muted-foreground">
How requests travel through VectorDNS from the browser through
Next.js and the Go DNS service to resolvers, and how results are
stored.
</p>
</div>
<Separator />
{/* DNS query flow */}
<div className="space-y-4">
<div className="space-y-1">
<h2 className="text-xl font-semibold tracking-tight">
DNS Record Lookup
</h2>
<p className="text-sm text-muted-foreground">
User-initiated DNS query flow.
</p>
</div>
<Card className="overflow-hidden">
<CardContent className="p-0">
<pre className="overflow-x-auto bg-muted/50 p-4 font-mono text-xs text-foreground">
User Next.js API Route Go DNS API DNS Resolvers
</pre>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<FlowSteps steps={dnsQuerySteps} />
</CardContent>
</Card>
</div>
<Separator />
{/* Monitoring flow */}
<div className="space-y-4">
<div className="space-y-1">
<h2 className="text-xl font-semibold tracking-tight">
Domain Monitoring
</h2>
<p className="text-sm text-muted-foreground">
Scheduled background job runs on the VPS, no user trigger
required.
</p>
</div>
<Card className="overflow-hidden">
<CardContent className="p-0">
<pre className="overflow-x-auto bg-muted/50 p-4 font-mono text-xs text-foreground">
Go Cron Supabase (read) DNS Resolvers Supabase (write)
Notifications
</pre>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<FlowSteps steps={monitoringSteps} />
</CardContent>
</Card>
</div>
<Separator />
{/* WHOIS flow */}
<div className="space-y-4">
<div className="space-y-1">
<h2 className="text-xl font-semibold tracking-tight">
WHOIS Lookups
</h2>
<p className="text-sm text-muted-foreground">
Handled entirely by Next.js the Go service is not involved.
</p>
</div>
<Card className="overflow-hidden">
<CardContent className="p-0">
<pre className="overflow-x-auto bg-muted/50 p-4 font-mono text-xs text-foreground">
User Next.js API Route (whoiser) WHOIS Servers
</pre>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<FlowSteps steps={whoIsSteps} />
</CardContent>
</Card>
</div>
<Separator />
{/* Key design notes */}
<div className="space-y-4">
<h2 className="text-xl font-semibold tracking-tight">
Key Design Notes
</h2>
<div className="grid gap-4 sm:grid-cols-2">
<Card>
<CardHeader>
<CardTitle className="text-sm">No direct client Go</CardTitle>
<CardDescription className="text-sm">
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.
</CardDescription>
</CardHeader>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm">
Persistent process on VPS
</CardTitle>
<CardDescription className="text-sm">
Unlike serverless functions, the Go service runs continuously
enabling native cron jobs and long-running monitoring without
timeout constraints.
</CardDescription>
</CardHeader>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm">UDP/TCP, not DoH</CardTitle>
<CardDescription className="text-sm">
DNS queries use direct UDP/TCP to resolvers or authoritative
nameservers via miekg/dns faster and more capable than
DNS-over-HTTPS.
</CardDescription>
</CardHeader>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm">
Supabase as source of truth
</CardTitle>
<CardDescription className="text-sm">
Both Next.js and the Go service read/write Supabase. Row Level
Security ensures users only access their own data.
</CardDescription>
</CardHeader>
</Card>
</div>
</div>
</div>
);
}

View File

@@ -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 (
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between gap-2">
<CardTitle className="font-mono text-base">{table.name}</CardTitle>
<Badge variant="secondary" className="text-xs">
current
</Badge>
</div>
<CardDescription>{table.description}</CardDescription>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b">
<th className="pb-2 pr-4 text-left font-medium text-muted-foreground">
Column
</th>
<th className="pb-2 pr-4 text-left font-medium text-muted-foreground">
Type
</th>
<th className="pb-2 text-left font-medium text-muted-foreground">
Notes
</th>
</tr>
</thead>
<tbody className="divide-y">
{table.columns.map((col) => (
<tr key={col.name}>
<td className="py-2 pr-4 font-mono text-xs text-foreground">
{col.name}
</td>
<td className="py-2 pr-4 font-mono text-xs text-primary">
{col.type}
</td>
<td className="py-2 text-xs text-muted-foreground">
{col.notes ?? "—"}
</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
);
}
export default function DatabaseSchemaPage() {
return (
<div className="space-y-10">
{/* Page header */}
<div className="space-y-3">
<div className="flex items-center gap-2">
<Database className="size-6 text-primary" />
<h1 className="text-3xl font-bold tracking-tight">Database Schema</h1>
</div>
<p className="text-lg text-muted-foreground">
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.
</p>
</div>
<Separator />
{/* Current tables */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold tracking-tight">
Current Tables
</h2>
<Badge>{currentTables.length} tables</Badge>
</div>
<div className="space-y-4">
{currentTables.map((table) => (
<TableCard key={table.name} table={table} />
))}
</div>
</div>
<Separator />
{/* Planned tables */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold tracking-tight">
Planned Tables
</h2>
<Badge variant="outline">{plannedTables.length} planned</Badge>
</div>
<p className="text-sm text-muted-foreground">
These tables will be added as the corresponding features are
implemented. Schema details may evolve.
</p>
<div className="grid gap-4 sm:grid-cols-2">
{plannedTables.map((table) => (
<Card key={table.name}>
<CardHeader className="pb-3">
<div className="flex items-start justify-between gap-2">
<CardTitle className="font-mono text-base">
{table.name}
</CardTitle>
<Badge variant="outline" className="shrink-0 text-xs">
planned
</Badge>
</div>
<Badge
variant="secondary"
className="w-fit text-xs font-normal"
>
{table.for}
</Badge>
<CardDescription className="pt-1">
{table.description}
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-1.5">
{table.keyColumns.map((col) => (
<code
key={col}
className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs text-muted-foreground"
>
{col}
</code>
))}
</div>
</CardContent>
</Card>
))}
</div>
</div>
<Separator />
{/* RLS note */}
<div className="space-y-3">
<h2 className="text-xl font-semibold tracking-tight">
Row Level Security
</h2>
<Card className="border-primary/20 bg-primary/5">
<CardContent className="pt-6 text-sm text-muted-foreground">
<p>
All tables have RLS enabled. Policies enforce that users can only
read and write their own rows (
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">
user_id = auth.uid()
</code>
). Future team-aware policies will extend this to allow team
members access to shared resources based on their role.
</p>
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -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 (
<div className="space-y-10">
{/* Page header */}
<div className="space-y-3">
<div className="flex items-center gap-2">
<Layers className="size-6 text-primary" />
<h1 className="text-3xl font-bold tracking-tight">System Overview</h1>
</div>
<p className="text-lg text-muted-foreground">
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.
</p>
</div>
<Separator />
{/* Architecture diagram */}
<div className="space-y-4">
<h2 className="text-xl font-semibold tracking-tight">
Architecture Diagram
</h2>
<ArchitectureDiagram />
</div>
{/* Services */}
<div className="space-y-4">
<h2 className="text-xl font-semibold tracking-tight">
What Each Service Handles
</h2>
<div className="grid gap-4 md:grid-cols-3">
{services.map((service) => (
<Card key={service.title}>
<CardHeader className="pb-3">
<div className="flex items-start justify-between gap-2">
<service.icon className="mt-0.5 size-5 text-primary" />
<Badge variant={service.badgeVariant}>{service.badge}</Badge>
</div>
<CardTitle className="text-base">{service.title}</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-1.5 text-sm text-muted-foreground">
{service.items.map((item) => (
<li key={item} className="flex gap-2">
<span className="mt-1.5 size-1 shrink-0 rounded-full bg-primary" />
{item}
</li>
))}
</ul>
</CardContent>
</Card>
))}
</div>
</div>
<Separator />
{/* Why hybrid */}
<div className="space-y-4">
<h2 className="text-xl font-semibold tracking-tight">Why Hybrid?</h2>
<Card>
<CardContent className="pt-6">
<div className="divide-y">
{tradeoffs.map(({ concern, solution }) => (
<div
key={concern}
className="flex flex-col gap-1 py-3 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:gap-4"
>
<span className="min-w-55 text-sm font-medium">
{concern}
</span>
<span className="text-sm text-muted-foreground">
{solution}
</span>
</div>
))}
</div>
</CardContent>
</Card>
</div>
{/* Communication */}
<div className="space-y-4">
<h2 className="text-xl font-semibold tracking-tight">Communication</h2>
<p className="text-sm text-muted-foreground">
Next.js API routes call the Go service over HTTPS. The Go service URL
is configured via{" "}
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">
GO_DNS_API_URL
</code>{" "}
and requests are authenticated with a shared API key via{" "}
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">
GO_DNS_API_KEY
</code>
.
</p>
<Card>
<CardContent className="p-0">
<pre className="overflow-x-auto bg-muted/50 p-4 font-mono text-sm text-foreground">
Next.js API route HTTPS Go DNS API UDP/TCP DNS resolvers
</pre>
</CardContent>
</Card>
</div>
{/* Why Go over DoH */}
<div className="space-y-4">
<h2 className="text-xl font-semibold tracking-tight">
Why Go Over DoH?
</h2>
<Card>
<CardHeader className="pb-3">
<CardDescription>
VectorDNS uses a custom Go microservice for DNS resolution instead
of DNS-over-HTTPS providers like Cloudflare&apos;s Tangerine.
</CardDescription>
</CardHeader>
<CardContent>
<ul className="space-y-2 text-sm text-muted-foreground">
{goAdvantages.map((item) => (
<li key={item} className="flex gap-2">
<span className="mt-1.5 size-1 shrink-0 rounded-full bg-primary" />
{item}
</li>
))}
</ul>
</CardContent>
</Card>
</div>
</div>
);
}