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

412
app/docs/api/page.tsx Normal file
View File

@@ -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<string, string> = {
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 (
<span
className={`inline-flex items-center rounded border px-2 py-0.5 font-mono text-xs font-semibold ${colors[method] ?? "bg-muted text-muted-foreground"}`}
>
{method}
</span>
);
}
function CodeBlock({ code }: { code: string }) {
return (
<pre className="overflow-x-auto rounded-md border bg-muted/50 p-4 font-mono text-sm leading-relaxed">
<code>{code.trim()}</code>
</pre>
);
}
function StatusBadge({ code }: { code: number }) {
const isOk = code < 400;
return (
<span
className={`inline-flex items-center gap-1 rounded px-1.5 py-0.5 font-mono text-xs font-medium ${isOk ? "bg-green-500/10 text-green-400" : "bg-red-500/10 text-red-400"}`}
>
{isOk ? (
<CheckCircle className="size-3" />
) : (
<XCircle className="size-3" />
)}
{code}
</span>
);
}
export default function ApiReferencePage() {
return (
<div className="space-y-10">
{/* Header */}
<div>
<div className="mb-3 flex items-center gap-2 text-sm text-muted-foreground">
<FileCode className="size-4" />
<span>Go Server</span>
<ArrowRight className="size-3" />
<span>API Reference</span>
</div>
<h1 className="text-3xl font-bold tracking-tight">API Reference</h1>
<p className="mt-2 text-muted-foreground">
Complete reference for the VectorDNS Go server REST API.
</p>
</div>
{/* Base URL & Auth */}
<Card>
<CardHeader>
<CardTitle className="text-base">
Base URL &amp; Authentication
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<p className="mb-2 text-sm text-muted-foreground">Base URL</p>
<CodeBlock code="https://<your-vps-host>/api/v1" />
</div>
<div>
<p className="mb-2 text-sm text-muted-foreground">
All requests (except{" "}
<span className="font-mono text-xs">/health</span>) require an API
key header:
</p>
<CodeBlock code="X-API-Key: <your-shared-secret>" />
</div>
<p className="text-sm text-muted-foreground">
Set{" "}
<span className="font-mono text-xs bg-muted px-1 py-0.5 rounded">
API_KEY
</span>{" "}
in your server environment to enable authentication. If left empty,
auth is disabled (not recommended for production).
</p>
</CardContent>
</Card>
{/* Endpoints */}
<div className="space-y-6">
<h2 className="text-xl font-semibold tracking-tight">Endpoints</h2>
{/* DNS Lookup */}
<Card>
<CardHeader>
<div className="flex items-center gap-3">
<Method method="POST" />
<code className="font-mono text-sm font-semibold">
/dns/lookup
</code>
</div>
<CardDescription>
Resolve DNS records for a domain. Query one or more record types
in a single request.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div>
<p className="mb-2 text-sm font-medium">Request</p>
<CodeBlock
code={`{
"domain": "example.com",
"types": ["A", "AAAA", "MX", "TXT", "NS", "CNAME", "SOA", "CAA", "SRV"],
"nameserver": "8.8.8.8"
}`}
/>
<ul className="mt-3 space-y-1 text-sm text-muted-foreground">
<li>
<span className="font-mono text-xs text-foreground">
domain
</span>{" "}
<Badge variant="secondary" className="text-xs">
required
</Badge>{" "}
The domain to query.
</li>
<li>
<span className="font-mono text-xs text-foreground">
types
</span>{" "}
<Badge variant="outline" className="text-xs">
optional
</Badge>{" "}
Record types to query. Defaults to all 9 supported types if
omitted.
</li>
<li>
<span className="font-mono text-xs text-foreground">
nameserver
</span>{" "}
<Badge variant="outline" className="text-xs">
optional
</Badge>{" "}
Specific nameserver to query. Defaults to system resolver.
</li>
</ul>
</div>
<Separator />
<div>
<div className="mb-2 flex items-center gap-2">
<p className="text-sm font-medium">Response</p>
<StatusBadge code={200} />
</div>
<CodeBlock
code={`{
"domain": "example.com",
"nameserver": "8.8.8.8",
"records": {
"A": [{ "value": "93.184.216.34", "ttl": 300 }],
"MX": [{ "value": "mail.example.com", "priority": 10, "ttl": 3600 }]
},
"query_time_ms": 12
}`}
/>
</div>
</CardContent>
</Card>
{/* DNS Propagation */}
<Card>
<CardHeader>
<div className="flex items-center gap-3">
<Method method="POST" />
<code className="font-mono text-sm font-semibold">
/dns/propagation
</code>
</div>
<CardDescription>
Check DNS propagation across multiple public resolvers. Queries
resolvers in parallel and compares results.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div>
<p className="mb-2 text-sm font-medium">Request</p>
<CodeBlock
code={`{
"domain": "example.com",
"type": "A",
"resolvers": ["8.8.8.8", "1.1.1.1", "9.9.9.9"]
}`}
/>
<ul className="mt-3 space-y-1 text-sm text-muted-foreground">
<li>
<span className="font-mono text-xs text-foreground">
domain
</span>{" "}
<Badge variant="secondary" className="text-xs">
required
</Badge>{" "}
The domain to check.
</li>
<li>
<span className="font-mono text-xs text-foreground">
type
</span>{" "}
<Badge variant="secondary" className="text-xs">
required
</Badge>{" "}
The record type to check (e.g.{" "}
<span className="font-mono text-xs">A</span>,{" "}
<span className="font-mono text-xs">MX</span>).
</li>
<li>
<span className="font-mono text-xs text-foreground">
resolvers
</span>{" "}
<Badge variant="outline" className="text-xs">
optional
</Badge>{" "}
List of resolver IPs. Defaults to a built-in list of public
resolvers.
</li>
</ul>
</div>
<Separator />
<div>
<div className="mb-2 flex items-center gap-2">
<p className="text-sm font-medium">Response</p>
<StatusBadge code={200} />
</div>
<CodeBlock
code={`{
"domain": "example.com",
"type": "A",
"results": [
{ "resolver": "8.8.8.8", "values": ["93.184.216.34"], "ttl": 300 },
{ "resolver": "1.1.1.1", "values": ["93.184.216.34"], "ttl": 280 },
{ "resolver": "9.9.9.9", "values": ["93.184.216.34"], "ttl": 295 }
],
"consistent": true
}`}
/>
</div>
</CardContent>
</Card>
{/* DNSSEC Validate */}
<Card>
<CardHeader>
<div className="flex items-center gap-3">
<Method method="POST" />
<code className="font-mono text-sm font-semibold">
/dns/validate
</code>
</div>
<CardDescription>
DNSSEC validation for a domain. Checks the AD flag and verifies
the signature chain.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div>
<p className="mb-2 text-sm font-medium">Request</p>
<CodeBlock
code={`{
"domain": "example.com"
}`}
/>
<ul className="mt-3 space-y-1 text-sm text-muted-foreground">
<li>
<span className="font-mono text-xs text-foreground">
domain
</span>{" "}
<Badge variant="secondary" className="text-xs">
required
</Badge>{" "}
The domain to validate.
</li>
</ul>
</div>
<Separator />
<div>
<div className="mb-2 flex items-center gap-2">
<p className="text-sm font-medium">Response</p>
<StatusBadge code={200} />
</div>
<CodeBlock
code={`{
"domain": "example.com",
"dnssec": true,
"chain_valid": true,
"details": "RRSIG verified for A record"
}`}
/>
</div>
</CardContent>
</Card>
{/* Health */}
<Card>
<CardHeader>
<div className="flex items-center gap-3">
<Method method="GET" />
<code className="font-mono text-sm font-semibold">/health</code>
</div>
<CardDescription>
Health check endpoint. No authentication required. Use this for
uptime monitoring.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div>
<div className="mb-2 flex items-center gap-2">
<p className="text-sm font-medium">Response</p>
<StatusBadge code={200} />
</div>
<CodeBlock
code={`{
"status": "ok",
"version": "0.1.0"
}`}
/>
</div>
</CardContent>
</Card>
</div>
{/* Errors */}
<div className="space-y-4">
<h2 className="text-xl font-semibold tracking-tight">
Error Responses
</h2>
<p className="text-sm text-muted-foreground">
All errors return a consistent JSON body:
</p>
<CodeBlock
code={`{
"error": "invalid domain",
"code": "INVALID_DOMAIN"
}`}
/>
<Card>
<CardContent className="pt-6">
<table className="w-full text-sm">
<thead>
<tr className="border-b text-left text-muted-foreground">
<th className="pb-2 pr-4 font-medium">Status</th>
<th className="pb-2 pr-4 font-medium">Code</th>
<th className="pb-2 font-medium">Description</th>
</tr>
</thead>
<tbody className="divide-y">
{[
{
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) => (
<tr key={row.code}>
<td className="py-2 pr-4">
<StatusBadge code={row.status} />
</td>
<td className="py-2 pr-4">
<code className="font-mono text-xs">{row.code}</code>
</td>
<td className="py-2 text-muted-foreground">{row.desc}</td>
</tr>
))}
</tbody>
</table>
</CardContent>
</Card>
</div>
</div>
);
}

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>
);
}

View File

@@ -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 (
<pre className="overflow-x-auto rounded-md border bg-muted/50 p-4 font-mono text-sm leading-relaxed">
<code>{code.trim()}</code>
</pre>
);
}
function EnvVar({
name,
defaultVal,
required,
description,
}: {
name: string;
defaultVal: string;
required?: boolean;
description: string;
}) {
return (
<tr className="border-b last:border-b-0">
<td className="py-3 pr-4 align-top">
<code className="font-mono text-xs">{name}</code>
</td>
<td className="py-3 pr-4 align-top">
{defaultVal ? (
<code className="font-mono text-xs text-muted-foreground">
{defaultVal}
</code>
) : (
<span className="text-xs text-muted-foreground"></span>
)}
</td>
<td className="py-3 pr-4 align-top">
{required ? (
<Badge variant="secondary" className="text-xs">
required
</Badge>
) : (
<Badge variant="outline" className="text-xs">
optional
</Badge>
)}
</td>
<td className="py-3 align-top text-sm text-muted-foreground">
{description}
</td>
</tr>
);
}
export default function ConfigurationPage() {
return (
<div className="space-y-10">
{/* Header */}
<div>
<div className="mb-3 flex items-center gap-2 text-sm text-muted-foreground">
<Settings className="size-4" />
<span>Go Server</span>
<ArrowRight className="size-3" />
<span>Configuration</span>
</div>
<h1 className="text-3xl font-bold tracking-tight">Configuration</h1>
<p className="mt-2 text-muted-foreground">
All server configuration is done via environment variables or a{" "}
<span className="font-mono text-xs bg-muted px-1 rounded">.env</span>{" "}
file in the project root.
</p>
</div>
{/* .env example */}
<Card>
<CardHeader>
<CardTitle className="text-base">Example .env</CardTitle>
</CardHeader>
<CardContent>
<CodeBlock
code={`PORT=8080
API_KEY=your-secret-api-key
CORS_ORIGINS=https://yourdomain.com,https://app.yourdomain.com
LOG_FORMAT=json`}
/>
<p className="mt-3 text-sm text-muted-foreground">
Copy{" "}
<span className="font-mono text-xs bg-muted px-1 rounded">
.env.example
</span>{" "}
to{" "}
<span className="font-mono text-xs bg-muted px-1 rounded">
.env
</span>{" "}
to get started:{" "}
<code className="font-mono text-xs bg-muted px-1 rounded">
cp .env.example .env
</code>
</p>
</CardContent>
</Card>
{/* Environment Variables reference */}
<div className="space-y-4">
<h2 className="text-xl font-semibold tracking-tight">
Environment Variables
</h2>
<Card>
<CardContent className="pt-6">
<table className="w-full text-sm">
<thead>
<tr className="border-b text-left text-muted-foreground">
<th className="pb-2 pr-4 font-medium">Variable</th>
<th className="pb-2 pr-4 font-medium">Default</th>
<th className="pb-2 pr-4 font-medium"></th>
<th className="pb-2 font-medium">Description</th>
</tr>
</thead>
<tbody>
<EnvVar
name="PORT"
defaultVal="8080"
description="Port the HTTP server listens on."
/>
<EnvVar
name="API_KEY"
defaultVal=""
description="Shared secret for API key authentication. Auth is disabled when empty — do not leave empty in production."
/>
<EnvVar
name="CORS_ORIGINS"
defaultVal="http://localhost:3000"
description="Comma-separated list of allowed CORS origins. Set to your frontend domain in production."
/>
<EnvVar
name="LOG_FORMAT"
defaultVal="text"
description='Log output format. "text" for human-readable, "json" for structured logging (recommended for production).'
/>
</tbody>
</table>
</CardContent>
</Card>
</div>
<Separator />
{/* API Key Auth */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<Shield className="size-5 text-primary" />
<h2 className="text-xl font-semibold tracking-tight">
API Key Authentication
</h2>
</div>
<p className="text-sm text-muted-foreground">
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.
</p>
<Card>
<CardHeader>
<CardTitle className="text-sm">How it works</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<p className="text-sm text-muted-foreground">
When{" "}
<span className="font-mono text-xs bg-muted px-1 rounded">
API_KEY
</span>{" "}
is set, every request (except{" "}
<span className="font-mono text-xs bg-muted px-1 rounded">
GET /health
</span>
) must include the header:
</p>
<CodeBlock code="X-API-Key: your-secret-api-key" />
<p className="text-sm text-muted-foreground">
Requests missing or sending an incorrect key receive a{" "}
<span className="font-mono text-xs bg-muted px-1 rounded">
401 UNAUTHORIZED
</span>{" "}
response.
</p>
<CodeBlock
code={`{
"error": "missing or invalid API key",
"code": "UNAUTHORIZED"
}`}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm">Calling from Next.js</CardTitle>
</CardHeader>
<CardContent>
<CodeBlock
code={`// In your Next.js server action or API route
const res = await fetch(\`\${process.env.DNS_SERVER_URL}/api/v1/dns/lookup\`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": process.env.DNS_API_KEY!,
},
body: JSON.stringify({ domain, types }),
});`}
/>
<p className="mt-3 text-sm text-muted-foreground">
Store{" "}
<span className="font-mono text-xs bg-muted px-1 rounded">
DNS_API_KEY
</span>{" "}
as a secret environment variable in your Next.js deployment. Never
expose it client-side.
</p>
</CardContent>
</Card>
</div>
<Separator />
{/* CORS */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<Globe className="size-5 text-primary" />
<h2 className="text-xl font-semibold tracking-tight">CORS</h2>
</div>
<p className="text-sm text-muted-foreground">
CORS is handled by{" "}
<a
href="https://github.com/go-chi/cors"
className="text-primary underline-offset-4 hover:underline"
target="_blank"
rel="noopener noreferrer"
>
go-chi/cors
</a>
. Configure allowed origins via the{" "}
<span className="font-mono text-xs bg-muted px-1 rounded">
CORS_ORIGINS
</span>{" "}
variable.
</p>
<Card>
<CardContent className="space-y-3 pt-6">
<p className="text-sm font-medium">Development</p>
<CodeBlock code="CORS_ORIGINS=http://localhost:3000" />
<p className="text-sm font-medium">Production (single domain)</p>
<CodeBlock code="CORS_ORIGINS=https://app.yourdomain.com" />
<p className="text-sm font-medium">Production (multiple domains)</p>
<CodeBlock code="CORS_ORIGINS=https://app.yourdomain.com,https://yourdomain.com" />
</CardContent>
</Card>
<p className="text-sm text-muted-foreground">
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.
</p>
</div>
<Separator />
{/* Rate Limiting */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<Gauge className="size-5 text-primary" />
<h2 className="text-xl font-semibold tracking-tight">
Rate Limiting
</h2>
</div>
<p className="text-sm text-muted-foreground">
Rate limiting is provided by{" "}
<a
href="https://github.com/go-chi/httprate"
className="text-primary underline-offset-4 hover:underline"
target="_blank"
rel="noopener noreferrer"
>
go-chi/httprate
</a>{" "}
and is enabled by default.
</p>
<Card>
<CardHeader>
<CardTitle className="text-sm">Current defaults</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2 text-sm text-muted-foreground">
<li> Rate limiting is applied per IP address.</li>
<li> Limits are enforced at the router middleware level.</li>
<li>
Requests exceeding the limit receive{" "}
<span className="font-mono text-xs bg-muted px-1 rounded">
429 Too Many Requests
</span>
.
</li>
</ul>
</CardContent>
</Card>
<Card className="border-dashed">
<CardHeader>
<div className="flex items-center gap-2">
<CardTitle className="text-sm">
Per-API-key rate limiting
</CardTitle>
<Badge variant="outline" className="text-xs">
Planned
</Badge>
</div>
<CardDescription>
When billing tiers are added, rate limits will be enforced per API
key to match the user`&apos;`s plan (free vs. pro).
</CardDescription>
</CardHeader>
<CardContent>
<ul className="space-y-1.5 text-sm text-muted-foreground">
<li> Free tier: limited requests per day</li>
<li> Pro tier: higher limits, hourly checks</li>
<li> Team tier: highest limits, shared across org</li>
</ul>
</CardContent>
</Card>
</div>
<Separator />
{/* Redis Caching */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<Database className="size-5 text-primary" />
<h2 className="text-xl font-semibold tracking-tight">
Redis Caching
</h2>
<Badge variant="outline" className="text-xs">
Planned
</Badge>
</div>
<p className="text-sm text-muted-foreground">
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.
</p>
<Card className="border-dashed">
<CardHeader>
<CardTitle className="text-sm">
Planned Redis configuration
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<CodeBlock
code={`# Planned env vars (not yet available)
REDIS_URL=redis://localhost:6379
REDIS_TTL=300 # seconds`}
/>
<p className="text-sm text-muted-foreground">
The cache layer will use{" "}
<a
href="https://github.com/redis/go-redis"
className="text-primary underline-offset-4 hover:underline"
target="_blank"
rel="noopener noreferrer"
>
go-redis
</a>{" "}
and sit as middleware before DNS resolution. A{" "}
<span className="font-mono text-xs bg-muted px-1 rounded">
docker-compose.yml
</span>{" "}
bundling the Go server with Redis will be provided.
</p>
</CardContent>
</Card>
</div>
</div>
);
}

72
app/docs/layout.tsx Normal file
View File

@@ -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 (
<div className="flex min-h-screen flex-col">
{/* Header */}
<header className="sticky top-0 z-50 border-b bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60">
<div className="container mx-auto flex h-14 items-center justify-between px-4">
<div className="flex items-center gap-4">
<Link href="/" className="flex items-center gap-2">
<Globe className="size-5 text-primary" />
<span className="text-lg font-semibold tracking-tight">
VectorDNS
</span>
</Link>
<span className="text-muted-foreground">/</span>
<Link
href="/docs"
className="flex items-center gap-1.5 text-sm text-muted-foreground transition-colors hover:text-foreground"
>
<BookOpen className="size-4" />
Docs
</Link>
</div>
<ThemeToggle />
</div>
</header>
<div className="container mx-auto flex flex-1 px-4">
{/* Mobile sidebar toggle */}
<input type="checkbox" id="sidebar-toggle" className="peer hidden" />
<label
htmlFor="sidebar-toggle"
className="fixed bottom-4 right-4 z-40 flex size-12 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg md:hidden"
>
<Menu className="size-5" />
</label>
{/* Sidebar */}
<aside className="fixed inset-y-0 left-0 z-30 w-64 -translate-x-full border-r bg-background p-6 pt-20 transition-transform peer-checked:translate-x-0 md:sticky md:top-14 md:z-auto md:block md:h-[calc(100vh-3.5rem)] md:w-56 md:translate-x-0 md:border-r-0 md:p-0 md:py-8 md:pr-6 lg:w-64">
<DocsSidebar />
</aside>
{/* Overlay for mobile sidebar */}
<label
htmlFor="sidebar-toggle"
className="fixed inset-0 z-20 hidden bg-black/50 peer-checked:block md:hidden!"
/>
{/* Main content */}
<main className="min-w-0 flex-1 py-8 md:pl-6 md:border-l">
<div className="mx-auto max-w-3xl">{children}</div>
</main>
</div>
</div>
);
}

73
app/docs/page.tsx Normal file
View File

@@ -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 (
<div className="space-y-8">
<div className="space-y-4">
<Badge variant="secondary" className="text-sm">
Documentation
</Badge>
<h1 className="text-3xl font-bold tracking-tight sm:text-4xl">
VectorDNS Documentation
</h1>
<p className="text-lg text-muted-foreground">
Everything you need to understand, configure, and deploy VectorDNS
the open-source DNS lookup, WHOIS, and domain monitoring toolkit.
</p>
</div>
<div className="grid gap-4 sm:grid-cols-2">
{docSections.map((section) => (
<Link key={section.href} href={section.href} className="group">
<Card className="h-full transition-colors group-hover:border-primary/50">
<CardHeader>
<section.icon className="mb-2 size-8 text-primary" />
<CardTitle>{section.title}</CardTitle>
<CardDescription>{section.description}</CardDescription>
</CardHeader>
</Card>
</Link>
))}
</div>
</div>
);
}

View File

@@ -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 (
<pre className="overflow-x-auto rounded-md border bg-muted/50 p-4 font-mono text-sm leading-relaxed">
<code>{code.trim()}</code>
</pre>
);
}
function Step({
n,
title,
children,
}: {
n: number;
title: string;
children: React.ReactNode;
}) {
return (
<div className="flex gap-4">
<div className="flex size-7 shrink-0 items-center justify-center rounded-full border bg-muted text-sm font-semibold">
{n}
</div>
<div className="flex-1 space-y-3 pt-0.5">
<p className="font-medium">{title}</p>
{children}
</div>
</div>
);
}
export default function SelfHostingPage() {
return (
<div className="space-y-10">
{/* Header */}
<div>
<div className="mb-3 flex items-center gap-2 text-sm text-muted-foreground">
<Server className="size-4" />
<span>Deployment</span>
<ArrowRight className="size-3" />
<span>Self-Hosting</span>
</div>
<h1 className="text-3xl font-bold tracking-tight">Self-Hosting</h1>
<p className="mt-2 text-muted-foreground">
Run the VectorDNS Go server on your own VPS or infrastructure. The DNS
API is a single stateless binary no database required.
</p>
</div>
{/* Prerequisites */}
<Card>
<CardHeader>
<CardTitle className="text-base">Prerequisites</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2 text-sm text-muted-foreground">
<li className="flex items-center gap-2">
<Badge variant="outline" className="font-mono text-xs">
Docker
</Badge>
Recommended no Go toolchain required on the host.
</li>
<li className="flex items-center gap-2">
<Badge variant="outline" className="font-mono text-xs">
Go 1.22+
</Badge>
Required only if building from source.
</li>
<li className="flex items-center gap-2">
<Badge variant="outline" className="font-mono text-xs">
Port 8080
</Badge>
Default port (configurable via{" "}
<span className="font-mono text-xs">PORT</span> env var).
</li>
</ul>
</CardContent>
</Card>
{/* Docker (recommended) */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<Package className="size-5 text-primary" />
<h2 className="text-xl font-semibold tracking-tight">
Docker (Recommended)
</h2>
<Badge variant="secondary" className="text-xs">
Recommended
</Badge>
</div>
<div className="space-y-6">
<Step n={1} title="Clone the repository">
<CodeBlock code="git clone https://github.com/yourusername/vectordns-server.git && cd vectordns-server" />
</Step>
<Step n={2} title="Create your .env file">
<CodeBlock
code={`cp .env.example .env
# Edit .env with your values
nano .env`}
/>
<p className="text-sm text-muted-foreground">
At minimum, set{" "}
<span className="font-mono text-xs bg-muted px-1 rounded">
API_KEY
</span>{" "}
and{" "}
<span className="font-mono text-xs bg-muted px-1 rounded">
CORS_ORIGINS
</span>
. See{" "}
<a
href="/docs/configuration"
className="text-primary underline-offset-4 hover:underline"
>
Configuration
</a>{" "}
for all options.
</p>
</Step>
<Step n={3} title="Build the Docker image">
<CodeBlock code="docker build -t vectordns-server ." />
</Step>
<Step n={4} title="Run the container">
<CodeBlock code="docker run -d -p 8080:8080 --env-file .env --name vectordns-server vectordns-server" />
<p className="text-sm text-muted-foreground">
The server will be available at{" "}
<span className="font-mono text-xs bg-muted px-1 rounded">
http://localhost:8080
</span>
.
</p>
</Step>
<Step n={5} title="Verify it's running">
<CodeBlock code="curl http://localhost:8080/api/v1/health" />
<p className="text-sm text-muted-foreground">
Should return{" "}
<span className="font-mono text-xs bg-muted px-1 rounded">{`{"status":"ok","version":"0.1.0"}`}</span>
.
</p>
</Step>
</div>
</div>
<Separator />
{/* From source */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<Terminal className="size-5 text-primary" />
<h2 className="text-xl font-semibold tracking-tight">From Source</h2>
</div>
<p className="text-sm text-muted-foreground">
If you prefer to build and run without Docker (requires Go 1.22+):
</p>
<div className="space-y-6">
<Step n={1} title="Install dependencies">
<CodeBlock code="go mod tidy" />
</Step>
<Step n={2} title="Configure environment">
<CodeBlock code="cp .env.example .env && nano .env" />
</Step>
<Step n={3} title="Run the server">
<CodeBlock code="go run ./cmd/server" />
</Step>
<Step n={4} title="Build a binary (optional)">
<CodeBlock
code={`go build -o vectordns-server ./cmd/server
./vectordns-server`}
/>
</Step>
</div>
</div>
<Separator />
{/* VPS Deployment */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<Cloud className="size-5 text-primary" />
<h2 className="text-xl font-semibold tracking-tight">
VPS Deployment
</h2>
</div>
<p className="text-sm text-muted-foreground">
Deploying to a VPS (e.g. DigitalOcean, Hetzner, Linode)? Here is a
recommended setup.
</p>
<Card>
<CardHeader>
<CardTitle className="text-sm">Reverse proxy with nginx</CardTitle>
<CardDescription>
Serve the Go server behind nginx so you can add TLS and a clean
domain path.
</CardDescription>
</CardHeader>
<CardContent>
<CodeBlock
code={`# /etc/nginx/sites-available/vectordns
server {
listen 443 ssl;
server_name api.yourdomain.com;
location /api/ {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}`}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm">
systemd service (non-Docker)
</CardTitle>
<CardDescription>
Keep the server running across reboots without Docker.
</CardDescription>
</CardHeader>
<CardContent>
<CodeBlock
code={`# /etc/systemd/system/vectordns-server.service
[Unit]
Description=VectorDNS Go Server
After=network.target
[Service]
ExecStart=/opt/vectordns/vectordns-server
EnvironmentFile=/opt/vectordns/.env
Restart=on-failure
User=www-data
[Install]
WantedBy=multi-user.target`}
/>
<div className="mt-3">
<CodeBlock
code={`sudo systemctl enable vectordns-server
sudo systemctl start vectordns-server`}
/>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm">
docker-compose (with Redis near-term)
</CardTitle>
<CardDescription>
Future: once Redis caching is added, a docker-compose setup will
be provided.
</CardDescription>
</CardHeader>
<CardContent>
<CodeBlock
code={`# docker-compose.yml (planned)
services:
server:
build: .
ports:
- "8080:8080"
env_file: .env
depends_on:
- redis
redis:
image: redis:7-alpine
ports:
- "6379:6379"`}
/>
<p className="mt-3 text-sm text-muted-foreground">
Redis caching is on the roadmap. This configuration will be
provided when the caching layer ships.
</p>
</CardContent>
</Card>
</div>
{/* Security note */}
<Card className="border-yellow-500/20 bg-yellow-500/5">
<CardHeader>
<CardTitle className="text-sm text-yellow-500">
Security Checklist
</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-1.5 text-sm text-muted-foreground">
<li>
Set a strong{" "}
<span className="font-mono text-xs bg-muted px-1 rounded">
API_KEY
</span>{" "}
do not leave auth disabled in production.
</li>
<li>
Set{" "}
<span className="font-mono text-xs bg-muted px-1 rounded">
CORS_ORIGINS
</span>{" "}
to your exact frontend domain, not{" "}
<span className="font-mono text-xs">*</span>.
</li>
<li>
Always run behind TLS (use Let`&apos;`s Encrypt via Certbot with
nginx).
</li>
<li> Rate limiting is enabled by default keep it on.</li>
</ul>
</CardContent>
</Card>
</div>
);
}

View File

@@ -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
</span>
</div>
<ThemeToggle />
<div className="flex items-center gap-4">
<Link
href="/docs"
className="text-sm text-muted-foreground transition-colors hover:text-foreground"
>
Docs
</Link>
<ThemeToggle />
</div>
</div>
</header>

View File

@@ -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=="],

View File

@@ -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<Node<ServiceNodeData>>) {
const Icon = data.icon;
return (
<div
className="rounded-lg border bg-card px-5 py-4 shadow-sm"
style={{ minWidth: 200 }}
>
<Handle type="target" position={Position.Top} className="opacity-0" />
<Handle type="source" position={Position.Bottom} className="opacity-0" />
<Handle
type="source"
position={Position.Right}
id="right"
className="opacity-0"
/>
<Handle
type="target"
position={Position.Left}
id="left"
className="opacity-0"
/>
<div className="flex items-center gap-2.5">
<div
className="flex size-8 items-center justify-center rounded-md"
style={{ backgroundColor: data.color }}
>
<Icon className="size-4 text-white" />
</div>
<div>
<div className="text-sm font-semibold text-foreground">
{data.label}
</div>
<div className="text-xs text-muted-foreground">{data.subtitle}</div>
</div>
</div>
{data.items && (
<ul className="mt-3 space-y-1 border-t pt-3">
{data.items.map((item) => (
<li
key={item}
className="flex items-center gap-1.5 text-[11px] text-muted-foreground"
>
<span
className="size-1 shrink-0 rounded-full"
style={{ backgroundColor: data.color }}
/>
{item}
</li>
))}
</ul>
)}
</div>
);
}
const nodeTypes = { service: ServiceNode };
const nodes: Node<ServiceNodeData>[] = [
{
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 (
<div className="h-105 w-full rounded-lg border bg-background">
<ReactFlow
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
fitView
fitViewOptions={{ padding: 0.3 }}
proOptions={{ hideAttribution: true }}
nodesDraggable={false}
nodesConnectable={false}
elementsSelectable={false}
panOnDrag={false}
zoomOnScroll={false}
zoomOnPinch={false}
zoomOnDoubleClick={false}
preventScrolling={false}
>
<Background gap={20} size={1} className="opacity-30" />
</ReactFlow>
</div>
);
}

View File

@@ -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 (
<nav className="space-y-6">
{sections.map((section) => (
<div key={section.title}>
<h4 className="mb-2 text-sm font-semibold tracking-tight text-muted-foreground">
{section.title}
</h4>
<ul className="space-y-1">
{section.items.map((item) => {
const isActive = pathname === item.href;
return (
<li key={item.href}>
<Link
href={item.href}
className={`flex items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors hover:bg-muted ${
isActive
? "bg-muted font-medium text-foreground"
: "text-muted-foreground"
}`}
>
<item.icon className="size-4" />
{item.title}
</Link>
</li>
);
})}
</ul>
</div>
))}
</nav>
);
}

111
docs/feature-plan.md Normal file
View File

@@ -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 |

View File

@@ -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",