mirror of
https://github.com/DevVoxel/VectorDNS.git
synced 2026-02-27 05:47:38 +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:
412
app/docs/api/page.tsx
Normal file
412
app/docs/api/page.tsx
Normal 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 & 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
398
app/docs/configuration/page.tsx
Normal file
398
app/docs/configuration/page.tsx
Normal 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`'`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
72
app/docs/layout.tsx
Normal 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
73
app/docs/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
343
app/docs/self-hosting/page.tsx
Normal file
343
app/docs/self-hosting/page.tsx
Normal 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`'`s Encrypt via Certbot with
|
||||||
|
nginx).
|
||||||
|
</li>
|
||||||
|
<li>• Rate limiting is enabled by default — keep it on.</li>
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
app/page.tsx
11
app/page.tsx
@@ -1,3 +1,4 @@
|
|||||||
|
import Link from "next/link";
|
||||||
import { Globe, Search, Shield, Activity } from "lucide-react";
|
import { Globe, Search, Shield, Activity } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -42,7 +43,15 @@ export default function Home() {
|
|||||||
VectorDNS
|
VectorDNS
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
39
bun.lock
39
bun.lock
@@ -5,6 +5,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "vectordns",
|
"name": "vectordns",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@xyflow/react": "^12.10.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.575.0",
|
"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=="],
|
"@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/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
|
|
||||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
"@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=="],
|
"@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=="],
|
"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=="],
|
"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=="],
|
"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-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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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/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=="],
|
"@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=="],
|
||||||
|
|||||||
193
components/docs/architecture-diagram.tsx
Normal file
193
components/docs/architecture-diagram.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
85
components/docs/sidebar.tsx
Normal file
85
components/docs/sidebar.tsx
Normal 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
111
docs/feature-plan.md
Normal 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 |
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@xyflow/react": "^12.10.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.575.0",
|
"lucide-react": "^0.575.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user