mirror of
https://github.com/DevVoxel/VectorDNS.git
synced 2026-02-27 09:57:39 +00:00
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:
311
app/docs/architecture/data-flow/page.tsx
Normal file
311
app/docs/architecture/data-flow/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
314
app/docs/architecture/database/page.tsx
Normal file
314
app/docs/architecture/database/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
215
app/docs/architecture/page.tsx
Normal file
215
app/docs/architecture/page.tsx
Normal 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'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user