This commit is contained in:
David 2026-02-27 23:33:07 +01:00
parent 4880d1dd87
commit 410ae18d2d
43 changed files with 4390 additions and 12 deletions

View File

@ -56,6 +56,8 @@ LLM Provider Adapters (OpenAI, Anthropic, Azure, Mistral, Ollama)
**Frontend:** React 18 + TypeScript + Vite, shadcn/ui, recharts. Routes protected via OIDC (Keycloak); `web/src/auth/` manages the auth flow. API clients live in `web/src/api/`. **Frontend:** React 18 + TypeScript + Vite, shadcn/ui, recharts. Routes protected via OIDC (Keycloak); `web/src/auth/` manages the auth flow. API clients live in `web/src/api/`.
**Documentation site** (`http://localhost:3000/docs`): public, no auth required. Root: `web/src/pages/docs/` — sections: getting-started, installation, api-reference (8 endpoints), guides (6), deployment (3), security (2), changelog. Layout components: `DocLayout.tsx` (sidebar + content + TOC), `DocSidebar.tsx` (with search), `DocBreadcrumbs.tsx`, `DocPagination.tsx`. Shared components: `components/CodeBlock.tsx`, `Callout.tsx`, `ApiEndpoint.tsx`, `ParamTable.tsx`, `TableOfContents.tsx`. Nav structure: `web/src/pages/docs/nav.ts`. Uses `@tailwindcss/typography` (added as devDependency) for prose rendering.
## Repository Structure ## Repository Structure
``` ```
@ -69,6 +71,7 @@ proto/pii/v1/ # gRPC .proto definitions
migrations/ # golang-migrate SQL files (up/down pairs) migrations/ # golang-migrate SQL files (up/down pairs)
clickhouse/ # ClickHouse DDL applied at startup via ApplyDDL() clickhouse/ # ClickHouse DDL applied at startup via ApplyDDL()
web/ # React frontend (Vite, src/pages, src/components, src/api) web/ # React frontend (Vite, src/pages, src/components, src/api)
src/pages/docs/ # Public documentation site (no auth); nav.ts defines sidebar structure
test/ # Integration tests (test/integration/, //go:build integration) + k6 load tests (test/k6/) test/ # Integration tests (test/integration/, //go:build integration) + k6 load tests (test/k6/)
deploy/ # Helm, Kubernetes manifests, Terraform (EKS), Prometheus/Grafana, alertmanager deploy/ # Helm, Kubernetes manifests, Terraform (EKS), Prometheus/Grafana, alertmanager
clickhouse/ # ClickHouse config overrides for Docker (e.g. listen-ipv4.xml — forces IPv4) clickhouse/ # ClickHouse config overrides for Docker (e.g. listen-ipv4.xml — forces IPv4)

28
web/package-lock.json generated
View File

@ -32,6 +32,7 @@
"zod": "^3.23.8" "zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/typography": "^0.5.19",
"@types/node": "^22.5.5", "@types/node": "^22.5.5",
"@types/react": "^18.3.5", "@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
@ -2034,6 +2035,33 @@
"win32" "win32"
] ]
}, },
"node_modules/@tailwindcss/typography": {
"version": "0.5.19",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
"integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"postcss-selector-parser": "6.0.10"
},
"peerDependencies": {
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
}
},
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@tanstack/query-core": { "node_modules/@tanstack/query-core": {
"version": "5.90.20", "version": "5.90.20",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz",

View File

@ -34,6 +34,7 @@
"zod": "^3.23.8" "zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/typography": "^0.5.19",
"@types/node": "^22.5.5", "@types/node": "^22.5.5",
"@types/react": "^18.3.5", "@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",

View File

@ -257,7 +257,7 @@ export function LandingPage() {
<a href="#securite" style={{ color: muted, textDecoration: "none", fontSize: ".875rem", fontWeight: 500 }}>Sécurité</a> <a href="#securite" style={{ color: muted, textDecoration: "none", fontSize: ".875rem", fontWeight: 500 }}>Sécurité</a>
<a href="#personas" style={{ color: muted, textDecoration: "none", fontSize: ".875rem", fontWeight: 500 }}>Pour qui</a> <a href="#personas" style={{ color: muted, textDecoration: "none", fontSize: ".875rem", fontWeight: 500 }}>Pour qui</a>
<a href="http://localhost:8090/playground" style={{ color: muted, textDecoration: "none", fontSize: ".875rem", fontWeight: 500 }}>Playground</a> <a href="http://localhost:8090/playground" style={{ color: muted, textDecoration: "none", fontSize: ".875rem", fontWeight: 500 }}>Playground</a>
<a href="http://localhost:8090/docs" style={{ color: muted, textDecoration: "none", fontSize: ".875rem", fontWeight: 500 }}>Docs</a> <Link to="/docs" style={{ color: muted, textDecoration: "none", fontSize: ".875rem", fontWeight: 500 }}>Docs</Link>
<Link <Link
to="/login" to="/login"
style={{ style={{
@ -716,7 +716,7 @@ export function LandingPage() {
<BtnOutline to="/login"> <BtnOutline to="/login">
Se connecter au dashboard Se connecter au dashboard
</BtnOutline> </BtnOutline>
<BtnOutline href="http://localhost:8090/docs"> <BtnOutline to="/docs">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg> <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
Documentation Documentation
</BtnOutline> </BtnOutline>
@ -741,15 +741,9 @@ export function LandingPage() {
Veylant<span style={{ color: brandLight }}> IA</span> Veylant<span style={{ color: brandLight }}> IA</span>
</Link> </Link>
<div style={{ display: "flex", gap: "1.5rem" }}> <div style={{ display: "flex", gap: "1.5rem" }}>
{[ <Link to="/docs" style={{ fontSize: ".84rem", color: dim, textDecoration: "none" }}>Documentation</Link>
{ label: "Documentation", href: "http://localhost:8090/docs" }, <a href="http://localhost:8090/playground" style={{ fontSize: ".84rem", color: dim, textDecoration: "none" }}>Playground</a>
{ label: "Playground", href: "http://localhost:8090/playground" }, <a href="mailto:demo@veylant.io" style={{ fontSize: ".84rem", color: dim, textDecoration: "none" }}>Contact</a>
{ label: "Contact", href: "mailto:demo@veylant.io" },
].map((l) => (
<a key={l.label} href={l.href} style={{ fontSize: ".84rem", color: dim, textDecoration: "none" }}>
{l.label}
</a>
))}
</div> </div>
<span style={{ fontSize: ".8rem", color: dim }}>© 2026 Veylant. Conçu pour l'entreprise européenne.</span> <span style={{ fontSize: ".8rem", color: dim }}>© 2026 Veylant. Conçu pour l'entreprise européenne.</span>
</div> </div>

View File

@ -0,0 +1,151 @@
import { Callout } from "./components/Callout";
import { Link } from "react-router-dom";
const v100Changes = {
features: [
"AI proxy with OpenAI-compatible API (/v1/chat/completions)",
"3-layer PII detection: regex + spaCy NER + LLM validation",
"PII pseudonymization with AES-256-GCM Redis mappings",
"Intelligent routing engine (PostgreSQL JSONB rules, in-memory cache, priority ASC)",
"RBAC: admin, manager, user, auditor roles with model restrictions",
"Streaming SSE support (PII anonymization on request, not response)",
"Circuit breaker per provider (threshold=5, TTL=60s) with fallback chain",
"Token-bucket rate limiting (per-tenant + per-user, DB overrides)",
"ClickHouse append-only audit logs with async batch writer",
"GDPR Article 30 processing registry with PDF/JSON export",
"EU AI Act risk classification questionnaire (forbidden/high/limited/minimal)",
"DPIA template generation",
"GDPR Art. 15 (Right of Access) and Art. 17 (Right to Erasure) endpoints",
"Feature flags (PostgreSQL + in-memory fallback)",
"React 18 dashboard with 11 pages (French UI)",
"Public AI playground at /playground (IP rate-limited 20/min)",
"Swagger UI at /docs",
"Provider adapters: OpenAI, Anthropic, Azure, Mistral, Ollama",
"Prometheus metrics + Grafana dashboards (overview + SLO)",
"7 Prometheus alerts with Alertmanager (PagerDuty + Slack)",
"HPA autoscaling 3→15 replicas (CPU 70% + memory 80%)",
"Blue/green deployment with Istio VirtualService (<5s rollback)",
"Terraform EKS v1.31, 3-AZ, eu-west-3",
"PostgreSQL backup CronJob (daily 02:00 UTC, S3, 7-day retention)",
"k6 load tests: smoke, load, stress, soak scenarios",
"HashiCorp Vault integration for API key rotation (90-day cycle)",
"AES-256-GCM prompt encryption",
"Production SLO dashboard (99.5% availability)",
"6 operational runbooks",
"Admin guide + integration guide + onboarding scripts",
"Pentest scope + remediation report (0 Critical, 0 High)",
"Commercial materials: one-pager, pitch deck, battle card",
"CHANGELOG.md with full v1.0.0 history",
"GitHub Actions release pipeline (multi-arch Docker + Helm OCI + GitHub Release)",
],
bugfixes: [
"Rate limit 429 responses now include Retry-After: 1 header (RFC 6585)",
"CSP header updated to allow dashboard iframe embeds",
"CORS allowed origins now configurable (server.allowed_origins) instead of wildcard",
"Routing rule cache race condition on concurrent updates fixed",
],
};
const v110Roadmap = [
{ priority: "Must-have", item: "Layer 3 LLM validation for PII (ambiguous cases)" },
{ priority: "Must-have", item: "Per-routing-rule PII control (enable/disable per rule)" },
{ priority: "Must-have", item: "Native OpenAI SDK (Go) for seamless migration" },
{ priority: "Should-have", item: "ML anomaly detection on prompt patterns" },
{ priority: "Should-have", item: "Shadow AI discovery (network traffic analysis)" },
{ priority: "Should-have", item: "Physical multi-tenant isolation (separate DB schemas)" },
{ priority: "Should-have", item: "SIEM integrations (Splunk, Elastic)" },
{ priority: "Could-have", item: "Custom model adapters via plugin API" },
{ priority: "Could-have", item: "Prompt caching (provider-level + in-proxy)" },
{ priority: "Could-have", item: "Cost budget alerts via email/Slack (direct)" },
];
export function ChangelogPage() {
return (
<div>
<h1 id="changelog">Changelog</h1>
<p>
All notable changes to Veylant IA are documented here. Versioning follows{" "}
<a
href="https://semver.org"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Semantic Versioning
</a>
.
</p>
<h2 id="v100">v1.0.0 February 25, 2026</h2>
<Callout type="tip" title="General Availability">
First production release. Pentest passed (0 Critical, 0 High). 2 pilot clients migrated.
</Callout>
<h3 id="features">Features</h3>
<ul className="space-y-1">
{v100Changes.features.map((f) => (
<li key={f} className="text-sm text-muted-foreground leading-relaxed">
{f}
</li>
))}
</ul>
<h3 id="bug-fixes">Bug Fixes</h3>
<ul className="space-y-1">
{v100Changes.bugfixes.map((b) => (
<li key={b} className="text-sm text-muted-foreground leading-relaxed">
{b}
</li>
))}
</ul>
<h2 id="v110-roadmap">v1.1.0 Planned (Q2 2026)</h2>
<p className="text-muted-foreground text-sm">
Priorities sourced from 2 pilot client sessions (MoSCoW method). See{" "}
<code>docs/feedback-backlog.md</code> for the full backlog.
</p>
<div className="overflow-x-auto my-4">
<table className="w-full text-sm border rounded-lg overflow-hidden">
<thead>
<tr className="bg-muted/50 border-b">
<th className="text-left px-4 py-2.5 font-semibold">Priority</th>
<th className="text-left px-4 py-2.5 font-semibold">Feature</th>
</tr>
</thead>
<tbody>
{v110Roadmap.map((item) => (
<tr key={item.item} className="border-b last:border-0">
<td className="px-4 py-2.5">
<span
className={`text-xs px-1.5 py-0.5 rounded font-medium ${
item.priority === "Must-have"
? "bg-red-100 dark:bg-red-900/40 text-red-700 dark:text-red-300"
: item.priority === "Should-have"
? "bg-amber-100 dark:bg-amber-900/40 text-amber-700 dark:text-amber-300"
: "bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300"
}`}
>
{item.priority}
</span>
</td>
<td className="px-4 py-2.5 text-sm text-muted-foreground">{item.item}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 id="migration">Migration & Compatibility</h2>
<p>
V1.0.0 has no breaking changes from the beta releases used by pilot clients. The{" "}
<code>/v1/chat/completions</code> API is fully backward-compatible with the OpenAI API
format. Any client using the OpenAI SDK will continue to work without changes.
</p>
<p>
See the <Link to="/docs/installation/docker">Installation guide</Link> for upgrade
instructions from beta.
</p>
</div>
);
}

View File

@ -0,0 +1,28 @@
import { Link, useLocation } from "react-router-dom";
import { ChevronRight } from "lucide-react";
import { NAV_SECTIONS } from "./nav";
export function DocBreadcrumbs() {
const { pathname } = useLocation();
const section = NAV_SECTIONS.find((s) => s.items.some((i) => i.path === pathname));
const item = section?.items.find((i) => i.path === pathname);
if (!section || !item) return null;
return (
<nav className="flex items-center gap-1.5 text-sm text-muted-foreground mb-6">
<Link to="/docs" className="hover:text-foreground transition-colors">
Docs
</Link>
<ChevronRight className="h-3.5 w-3.5" />
<span>{section.title}</span>
{item.path !== "/docs" && (
<>
<ChevronRight className="h-3.5 w-3.5" />
<span className="text-foreground">{item.title}</span>
</>
)}
</nav>
);
}

View File

@ -0,0 +1,51 @@
import { Outlet, useLocation } from "react-router-dom";
import { DocSidebar } from "./DocSidebar";
import { DocBreadcrumbs } from "./DocBreadcrumbs";
import { DocPagination } from "./DocPagination";
import { TableOfContents } from "./components/TableOfContents";
export function DocLayout() {
const { pathname } = useLocation();
const isHome = pathname === "/docs";
return (
<div className="flex min-h-screen bg-background">
{/* Left sidebar */}
<DocSidebar />
{/* Main content */}
<div className="flex flex-1 min-w-0">
{/* Article */}
<main className="flex-1 min-w-0 px-8 py-10 overflow-y-auto max-h-screen">
<div className="max-w-3xl mx-auto">
{!isHome && <DocBreadcrumbs />}
<div
data-doc-content
className="prose prose-sm sm:prose-base max-w-none
prose-headings:font-bold prose-headings:tracking-tight
prose-h1:text-3xl prose-h1:mb-2
prose-h2:text-xl prose-h2:mt-8 prose-h2:mb-3
prose-h3:text-base prose-h3:mt-6 prose-h3:mb-2
prose-p:text-muted-foreground prose-p:leading-7
prose-li:text-muted-foreground prose-li:leading-7
prose-strong:text-foreground
prose-code:text-sm prose-code:bg-muted prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:before:content-none prose-code:after:content-none
prose-a:text-primary prose-a:no-underline hover:prose-a:underline
"
>
<Outlet />
</div>
{!isHome && <DocPagination />}
</div>
</main>
{/* Right TOC (hidden on home page and small screens) */}
{!isHome && (
<div className="hidden xl:block w-56 shrink-0 px-6 py-10 overflow-y-auto max-h-screen">
<TableOfContents />
</div>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,43 @@
import { Link, useLocation } from "react-router-dom";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { getPrevNext } from "./nav";
export function DocPagination() {
const { pathname } = useLocation();
const { prev, next } = getPrevNext(pathname);
if (!prev && !next) return null;
return (
<div className="flex items-center justify-between mt-12 pt-6 border-t">
{prev ? (
<Link
to={prev.path}
className="group flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<ChevronLeft className="h-4 w-4 group-hover:-translate-x-0.5 transition-transform" />
<div className="text-left">
<div className="text-xs uppercase tracking-wide mb-0.5">Previous</div>
<div className="font-medium">{prev.title}</div>
</div>
</Link>
) : (
<div />
)}
{next ? (
<Link
to={next.path}
className="group flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors text-right"
>
<div>
<div className="text-xs uppercase tracking-wide mb-0.5">Next</div>
<div className="font-medium">{next.title}</div>
</div>
<ChevronRight className="h-4 w-4 group-hover:translate-x-0.5 transition-transform" />
</Link>
) : (
<div />
)}
</div>
);
}

View File

@ -0,0 +1,139 @@
import { useState } from "react";
import { NavLink, Link } from "react-router-dom";
import { Search, Shield, X } from "lucide-react";
import { cn } from "@/lib/utils";
import { NAV_SECTIONS, ALL_NAV_ITEMS } from "./nav";
export function DocSidebar() {
const [query, setQuery] = useState("");
const filtered =
query.trim().length > 0
? ALL_NAV_ITEMS.filter(
(i) =>
i.title.toLowerCase().includes(query.toLowerCase()) ||
i.section.toLowerCase().includes(query.toLowerCase())
)
: null;
return (
<aside className="w-72 shrink-0 hidden lg:flex flex-col h-full border-r bg-background">
{/* Logo */}
<div className="flex items-center gap-2 px-5 py-4 border-b">
<div className="h-7 w-7 rounded-md bg-primary flex items-center justify-center">
<Shield className="h-4 w-4 text-primary-foreground" />
</div>
<div>
<Link to="/docs" className="font-bold text-sm hover:text-primary transition-colors">
Veylant IA Docs
</Link>
<span className="ml-2 text-xs bg-muted text-muted-foreground px-1.5 py-0.5 rounded font-mono">
v1.0.0
</span>
</div>
</div>
{/* Search */}
<div className="px-4 py-3 border-b">
<div className="relative">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
<input
type="text"
placeholder="Search docs..."
value={query}
onChange={(e) => setQuery(e.target.value)}
className="w-full pl-8 pr-7 py-1.5 text-sm bg-muted rounded-md border-0 outline-none focus:ring-1 focus:ring-primary placeholder:text-muted-foreground"
/>
{query && (
<button
onClick={() => setQuery("")}
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
<X className="h-3.5 w-3.5" />
</button>
)}
</div>
</div>
{/* Nav */}
<nav className="flex-1 overflow-y-auto py-4 px-3">
{filtered ? (
/* Search results */
<div>
<p className="text-xs text-muted-foreground px-2 mb-2">
{filtered.length} result{filtered.length !== 1 ? "s" : ""}
</p>
{filtered.length === 0 ? (
<p className="text-sm text-muted-foreground px-2">No pages found.</p>
) : (
<ul className="space-y-0.5">
{filtered.map((item) => (
<li key={item.path}>
<NavLink
to={item.path}
end={item.path === "/docs"}
onClick={() => setQuery("")}
className={({ isActive }) =>
cn(
"flex flex-col px-3 py-2 rounded-md text-sm transition-colors",
isActive
? "bg-primary/10 text-primary font-semibold border-l-2 border-primary"
: "text-muted-foreground hover:text-foreground hover:bg-muted"
)
}
>
<span>{item.title}</span>
<span className="text-xs text-muted-foreground mt-0.5">{item.section}</span>
</NavLink>
</li>
))}
</ul>
)}
</div>
) : (
/* Full nav tree */
<div className="space-y-6">
{NAV_SECTIONS.map((section) => (
<div key={section.title}>
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider px-3 mb-1.5">
{section.title}
</p>
<ul className="space-y-0.5">
{section.items.map((item) => (
<li key={item.path}>
<NavLink
to={item.path}
end={item.path === "/docs"}
className={({ isActive }) =>
cn(
"block px-3 py-1.5 rounded-md text-sm transition-colors",
isActive
? "bg-primary/10 text-primary font-semibold border-l-2 border-primary pl-[10px]"
: "text-muted-foreground hover:text-foreground hover:bg-muted"
)
}
>
{item.title}
</NavLink>
</li>
))}
</ul>
</div>
))}
</div>
)}
</nav>
{/* Footer */}
<div className="px-4 py-3 border-t">
<Link
to="/dashboard"
className="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
<Shield className="h-3.5 w-3.5" />
Back to Dashboard
</Link>
</div>
</aside>
);
}

View File

@ -0,0 +1,203 @@
import { Link } from "react-router-dom";
import {
Rocket,
Package,
Plug,
Shield,
GitBranch,
Scale,
Ship,
Lock,
ArrowRight,
Sparkles,
} from "lucide-react";
import { Callout } from "./components/Callout";
const cards = [
{
icon: Rocket,
title: "Quick Start",
description: "Get Veylant IA running in 5 minutes with Docker Compose.",
href: "/docs/getting-started/quick-start",
color: "bg-blue-50 dark:bg-blue-950/30 text-blue-600 dark:text-blue-400",
},
{
icon: Package,
title: "Installation",
description: "Configure services, providers, and environment variables.",
href: "/docs/installation/docker",
color: "bg-purple-50 dark:bg-purple-950/30 text-purple-600 dark:text-purple-400",
},
{
icon: Plug,
title: "API Reference",
description: "Full reference for all REST endpoints, parameters, and responses.",
href: "/docs/api/chat-completions",
color: "bg-green-50 dark:bg-green-950/30 text-green-600 dark:text-green-400",
},
{
icon: Shield,
title: "PII & Privacy",
description: "3-layer PII detection, pseudonymization, and Redis-backed mappings.",
href: "/docs/guides/pii",
color: "bg-red-50 dark:bg-red-950/30 text-red-600 dark:text-red-400",
},
{
icon: GitBranch,
title: "Routing Rules",
description: "Route AI requests by role, department, model, or sensitivity.",
href: "/docs/guides/routing",
color: "bg-amber-50 dark:bg-amber-950/30 text-amber-600 dark:text-amber-400",
},
{
icon: Scale,
title: "Compliance",
description: "GDPR Article 30 registry, AI Act risk classification, and DPIA.",
href: "/docs/guides/compliance",
color: "bg-teal-50 dark:bg-teal-950/30 text-teal-600 dark:text-teal-400",
},
{
icon: Ship,
title: "Deployment",
description: "Docker, Kubernetes Helm chart, and blue/green with Istio.",
href: "/docs/deployment/kubernetes",
color: "bg-indigo-50 dark:bg-indigo-950/30 text-indigo-600 dark:text-indigo-400",
},
{
icon: Lock,
title: "Security",
description: "Zero Trust, mTLS, AES-256-GCM encryption, and audit trails.",
href: "/docs/security/model",
color: "bg-slate-50 dark:bg-slate-950/30 text-slate-600 dark:text-slate-400",
},
];
export function DocsHomePage() {
return (
<div className="py-4">
{/* Hero */}
<div className="mb-10">
<div className="flex items-center gap-2 mb-4">
<span className="text-xs font-semibold bg-primary/10 text-primary px-2 py-0.5 rounded-full">
v1.0.0 Generally Available
</span>
</div>
<h1 className="text-4xl font-extrabold tracking-tight text-foreground mb-4">
Veylant IA Documentation
</h1>
<p className="text-lg text-muted-foreground max-w-2xl leading-relaxed">
Everything you need to build, configure, and operate your enterprise AI governance
platform. Prevent Shadow AI, enforce PII anonymization, ensure GDPR compliance, and
control costs across all LLM usage.
</p>
</div>
{/* What's new */}
<Callout type="tip" title="What's new in v1.0.0">
Pentest passed (0 Critical, 0 High), 2 pilot clients migrated, blue/green deployment with
Istio, HPA autoscaling (315 replicas), 7 Prometheus alerts, SLO dashboard (99.5%), and 6
operational runbooks.{" "}
<Link to="/docs/changelog" className="text-primary hover:underline font-medium">
Read the full changelog
</Link>
</Callout>
{/* Cards grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-8">
{cards.map((card) => (
<Link
key={card.href}
to={card.href}
className="group flex items-start gap-4 rounded-xl border bg-card p-5 hover:shadow-md transition-all hover:border-primary/30"
>
<div className={`p-2.5 rounded-lg shrink-0 ${card.color}`}>
<card.icon className="h-5 w-5" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<h3 className="font-semibold text-foreground">{card.title}</h3>
<ArrowRight className="h-4 w-4 text-muted-foreground group-hover:text-primary group-hover:translate-x-0.5 transition-all" />
</div>
<p className="text-sm text-muted-foreground mt-1 leading-relaxed">
{card.description}
</p>
</div>
</Link>
))}
</div>
{/* Quick links */}
<div className="mt-10 grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="rounded-xl border bg-card p-4">
<div className="flex items-center gap-2 mb-3">
<Sparkles className="h-4 w-4 text-primary" />
<span className="text-sm font-semibold">Popular pages</span>
</div>
<ul className="space-y-1.5">
{[
{ title: "Chat Completions API", href: "/docs/api/chat-completions" },
{ title: "Routing Rules Engine", href: "/docs/guides/routing" },
{ title: "RBAC & Permissions", href: "/docs/guides/rbac" },
{ title: "GDPR Compliance", href: "/docs/guides/compliance" },
].map((l) => (
<li key={l.href}>
<Link
to={l.href}
className="text-sm text-muted-foreground hover:text-primary transition-colors flex items-center gap-1.5"
>
<ArrowRight className="h-3 w-3" />
{l.title}
</Link>
</li>
))}
</ul>
</div>
<div className="rounded-xl border bg-card p-4">
<div className="flex items-center gap-2 mb-3">
<Package className="h-4 w-4 text-primary" />
<span className="text-sm font-semibold">AI Providers</span>
</div>
<ul className="space-y-1.5">
{[
"OpenAI (gpt-4o, gpt-4-turbo)",
"Anthropic (claude-3-5-sonnet)",
"Azure OpenAI",
"Mistral AI",
"Ollama (self-hosted)",
].map((p) => (
<li key={p} className="text-sm text-muted-foreground flex items-center gap-1.5">
<div className="h-1.5 w-1.5 rounded-full bg-green-500" />
{p}
</li>
))}
</ul>
</div>
<div className="rounded-xl border bg-card p-4">
<div className="flex items-center gap-2 mb-3">
<Plug className="h-4 w-4 text-primary" />
<span className="text-sm font-semibold">Compatibility</span>
</div>
<p className="text-sm text-muted-foreground mb-2">
OpenAI-compatible API drop-in replacement:
</p>
<ul className="space-y-1.5">
{[
"openai Python SDK",
"openai Node.js SDK",
"LangChain",
"LlamaIndex",
"Any HTTP client",
].map((c) => (
<li key={c} className="text-sm text-muted-foreground flex items-center gap-1.5">
<div className="h-1.5 w-1.5 rounded-full bg-primary" />
{c}
</li>
))}
</ul>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,143 @@
import { CodeBlock } from "../components/CodeBlock";
import { Callout } from "../components/Callout";
import { ApiEndpoint } from "../components/ApiEndpoint";
import { ParamTable } from "../components/ParamTable";
export function AdminCompliancePage() {
return (
<div>
<h1 id="admin-compliance">Admin Compliance</h1>
<p>
GDPR Article 30 processing registry, EU AI Act risk classification, and data subject
rights (access and erasure).
</p>
<Callout type="info" title="Required role">
Compliance endpoints require <code>admin</code> role. Auditors can read but not modify.
</Callout>
<h2 id="processing-registry">GDPR Article 30 Processing Registry</h2>
<ApiEndpoint method="GET" path="/v1/admin/compliance/entries" description="List all processing activities in the GDPR Art. 30 registry." />
<ApiEndpoint method="POST" path="/v1/admin/compliance/entries" description="Create a new processing activity entry." />
<ApiEndpoint method="GET" path="/v1/admin/compliance/entries/{id}" description="Get a specific processing entry." />
<ApiEndpoint method="PUT" path="/v1/admin/compliance/entries/{id}" description="Update a processing activity entry." />
<ParamTable
title="Processing Entry Fields"
params={[
{ name: "use_case_name", type: "string", required: true, description: "Name of the AI use case (e.g. 'Legal contract analysis')." },
{ name: "purpose", type: "string", required: true, description: "Processing purpose as required by GDPR Art. 5." },
{ name: "legal_basis", type: "string", required: true, description: "Legal basis: legitimate_interest | contract | legal_obligation | consent | vital_interests | public_task" },
{ name: "data_categories", type: "[]string", required: true, description: "Categories of personal data: name, email, phone, id_number, financial, health, etc." },
{ name: "retention_period", type: "string", required: true, description: "Data retention period (e.g. '3 years', '90 days')." },
{ name: "security_measures", type: "string", required: true, description: "Technical and organizational security measures." },
{ name: "controller_name", type: "string", required: true, description: "Data controller name and contact." },
{ name: "recipients", type: "[]string", required: false, description: "Third parties receiving the data." },
{ name: "processors", type: "[]string", required: false, description: "Data processors (LLM providers, cloud services)." },
{ name: "dpia_required", type: "boolean", required: false, default: "false", description: "Whether a DPIA (Art. 35) is required." },
]}
/>
<CodeBlock
language="bash"
code={`curl -X POST http://localhost:8090/v1/admin/compliance/entries \\
-H "Authorization: Bearer $TOKEN" \\
-H "Content-Type: application/json" \\
-d '{
"use_case_name": "Legal contract analysis",
"purpose": "Automated review of supplier contracts for risk identification",
"legal_basis": "legitimate_interest",
"data_categories": ["name", "financial", "company_data"],
"retention_period": "3 years",
"security_measures": "AES-256-GCM encryption, PII anonymization, audit logs",
"controller_name": "Acme Corp — dpo@acme.com",
"processors": ["Anthropic (Claude via Veylant IA proxy)"]
}'`}
/>
<h2 id="ai-act">EU AI Act Classification</h2>
<ApiEndpoint method="POST" path="/v1/admin/compliance/entries/{id}/classify" description="Run the AI Act risk questionnaire for a processing entry and classify the risk level." />
<CodeBlock
language="bash"
code={`curl -X POST http://localhost:8090/v1/admin/compliance/entries/entry-uuid/classify \\
-H "Authorization: Bearer $TOKEN" \\
-H "Content-Type: application/json" \\
-d '{
"autonomous_decisions": false,
"biometric_data": false,
"critical_infrastructure": false,
"sensitive_data": true,
"transparency_required": true
}'
# Response:
{
"risk_level": "limited",
"score": 2,
"description": "Limited risk — transparency obligations apply. Users must be informed they are interacting with an AI system.",
"actions_required": [
"Display AI disclosure in the user interface",
"Document in GDPR Art. 30 registry",
"Annual review recommended"
]
}`}
/>
<p>Risk level mapping:</p>
<div className="overflow-x-auto my-4">
<table className="w-full text-sm border rounded-lg overflow-hidden">
<thead>
<tr className="bg-muted/50 border-b">
<th className="text-left px-4 py-2.5 font-semibold">Score</th>
<th className="text-left px-4 py-2.5 font-semibold">Level</th>
<th className="text-left px-4 py-2.5 font-semibold">Description</th>
</tr>
</thead>
<tbody>
{[
{ score: "5", level: "Forbidden", desc: "System must not be deployed. Example: social scoring, real-time biometric surveillance in public spaces.", color: "text-red-600" },
{ score: "34", level: "High", desc: "Strict conformity assessment required before deployment. DPIA mandatory.", color: "text-orange-600" },
{ score: "12", level: "Limited", desc: "Transparency obligations: users must be informed they interact with AI.", color: "text-amber-600" },
{ score: "0", level: "Minimal", desc: "Minimal risk. Voluntary code of conduct recommended.", color: "text-green-600" },
].map((row) => (
<tr key={row.level} className="border-b last:border-0">
<td className="px-4 py-2.5 font-mono text-xs">{row.score}</td>
<td className={`px-4 py-2.5 font-semibold text-xs ${row.color}`}>{row.level}</td>
<td className="px-4 py-2.5 text-muted-foreground text-xs">{row.desc}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 id="gdpr-rights">GDPR Subject Rights</h2>
<h3 id="access">Article 15 Right of Access</h3>
<ApiEndpoint method="GET" path="/v1/admin/compliance/gdpr/access/{user_id}" description="Return all personal data stored for a specific user (prompt history, audit entries, pseudonymization mappings)." />
<CodeBlock
language="bash"
code={`curl http://localhost:8090/v1/admin/compliance/gdpr/access/user-uuid \\
-H "Authorization: Bearer $TOKEN"
# Returns: audit entries, PII mapping references, user profile data`}
/>
<h3 id="erasure">Article 17 Right to Erasure</h3>
<ApiEndpoint method="POST" path="/v1/admin/compliance/gdpr/erase/{user_id}" description="Pseudonymize or delete all personal data for a user. Audit log metadata is retained but PII is scrubbed." />
<CodeBlock
language="bash"
code={`curl -X POST http://localhost:8090/v1/admin/compliance/gdpr/erase/user-uuid \\
-H "Authorization: Bearer $TOKEN" \\
-H "Content-Type: application/json" \\
-d '{"reason": "User request under GDPR Art. 17"}'`}
/>
<Callout type="warning" title="ClickHouse is append-only">
ClickHouse audit logs cannot be deleted. The erasure endpoint scrubs PII from prompt
content and pseudonymizes user identifiers, but the request metadata (token counts, cost,
timestamps) is retained for compliance reporting.
</Callout>
</div>
);
}

View File

@ -0,0 +1,98 @@
import { CodeBlock } from "../components/CodeBlock";
import { Callout } from "../components/Callout";
import { ApiEndpoint } from "../components/ApiEndpoint";
export function AdminFlagsPage() {
return (
<div>
<h1 id="admin-flags">Admin Feature Flags</h1>
<p>
Feature flags control behavior at runtime without redeployment. Flags are stored in
PostgreSQL with an in-memory cache (updated every 30 seconds). If PostgreSQL is
unavailable, the in-memory defaults are used.
</p>
<Callout type="info" title="Required role">
Feature flag management requires the <code>admin</code> role.
</Callout>
<h2 id="list">List All Flags</h2>
<ApiEndpoint method="GET" path="/v1/admin/flags" description="Return all feature flags and their current values." />
<CodeBlock
language="bash"
code={`curl http://localhost:8090/v1/admin/flags \\
-H "Authorization: Bearer $TOKEN"
# Response:
{
"data": [
{"name": "pii_detection", "enabled": true, "description": "Enable PII detection pipeline"},
{"name": "pii_pseudonymization","enabled": true, "description": "Store PII mappings in Redis"},
{"name": "audit_logging", "enabled": true, "description": "Write to ClickHouse audit log"},
{"name": "playground", "enabled": true, "description": "Public playground endpoint"},
{"name": "streaming", "enabled": true, "description": "SSE streaming support"},
{"name": "cost_tracking", "enabled": true, "description": "Track token costs per request"},
{"name": "circuit_breaker", "enabled": true, "description": "Provider circuit breaker"}
]
}`}
/>
<h2 id="get">Get a Flag</h2>
<ApiEndpoint method="GET" path="/v1/admin/flags/{name}" description="Get the current value of a named flag." />
<h2 id="set">Set a Flag</h2>
<ApiEndpoint method="PUT" path="/v1/admin/flags/{name}" description="Enable or disable a feature flag." />
<CodeBlock
language="bash"
code={`# Disable PII detection (e.g. for debugging)
curl -X PUT http://localhost:8090/v1/admin/flags/pii_detection \\
-H "Authorization: Bearer $TOKEN" \\
-H "Content-Type: application/json" \\
-d '{"enabled": false}'`}
/>
<h2 id="delete">Delete a Flag</h2>
<ApiEndpoint method="DELETE" path="/v1/admin/flags/{name}" description="Delete a feature flag (it reverts to the compiled-in default)." />
<h2 id="defaults">Default Flag Values</h2>
<div className="overflow-x-auto my-4">
<table className="w-full text-sm border rounded-lg overflow-hidden">
<thead>
<tr className="bg-muted/50 border-b">
<th className="text-left px-4 py-2.5 font-semibold">Flag</th>
<th className="text-left px-4 py-2.5 font-semibold">Default</th>
<th className="text-left px-4 py-2.5 font-semibold">Effect when disabled</th>
</tr>
</thead>
<tbody>
{[
{ flag: "pii_detection", def: "true", effect: "Prompts forwarded without PII scanning" },
{ flag: "pii_pseudonymization", def: "true", effect: "PII detected but not stored in Redis" },
{ flag: "audit_logging", def: "true", effect: "Requests not written to ClickHouse" },
{ flag: "playground", def: "true", effect: "POST /playground/analyze returns 404" },
{ flag: "streaming", def: "true", effect: "SSE requests return 400" },
{ flag: "cost_tracking", def: "true", effect: "Token cost not computed or stored" },
{ flag: "circuit_breaker", def: "true", effect: "Failures not counted, no fallback" },
].map((row) => (
<tr key={row.flag} className="border-b last:border-0">
<td className="px-4 py-2.5 font-mono text-xs">{row.flag}</td>
<td className="px-4 py-2.5 text-xs">
<span className="bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300 px-1.5 py-0.5 rounded font-medium">
{row.def}
</span>
</td>
<td className="px-4 py-2.5 text-muted-foreground text-xs">{row.effect}</td>
</tr>
))}
</tbody>
</table>
</div>
<Callout type="warning" title="Production safety">
Disabling <code>audit_logging</code> or <code>pii_detection</code> in production may
violate GDPR obligations and internal SLA requirements. Always coordinate with your DPO
before making changes.
</Callout>
</div>
);
}

View File

@ -0,0 +1,133 @@
import { CodeBlock } from "../components/CodeBlock";
import { Callout } from "../components/Callout";
import { ApiEndpoint } from "../components/ApiEndpoint";
import { ParamTable } from "../components/ParamTable";
export function AdminLogsPage() {
return (
<div>
<h1 id="admin-logs">Admin Audit Logs & Costs</h1>
<p>
Query the immutable audit trail and cost breakdown for AI requests. All data is stored in
ClickHouse (append-only no DELETE operations).
</p>
<h2 id="audit-logs">Audit Logs</h2>
<ApiEndpoint method="GET" path="/v1/admin/logs" description="Query audit log entries with optional filters." />
<ParamTable
title="Query Parameters"
params={[
{ name: "user_id", type: "string", required: false, description: "Filter by user UUID." },
{ name: "provider", type: "string", required: false, description: "Filter by provider: openai | anthropic | azure | mistral | ollama" },
{ name: "model", type: "string", required: false, description: "Filter by model used." },
{ name: "from", type: "string (ISO 8601)", required: false, description: "Start of time range, e.g. 2026-01-01T00:00:00Z" },
{ name: "to", type: "string (ISO 8601)", required: false, description: "End of time range." },
{ name: "has_pii", type: "boolean", required: false, description: "Filter entries where PII was detected." },
{ name: "limit", type: "integer", required: false, default: "50", description: "Max entries to return (max 1000)." },
{ name: "offset", type: "integer", required: false, default: "0", description: "Pagination offset." },
]}
/>
<CodeBlock
language="bash"
code={`curl "http://localhost:8090/v1/admin/logs?from=2026-01-01T00:00:00Z&has_pii=true&limit=10" \\
-H "Authorization: Bearer $TOKEN"
# Response:
{
"data": [
{
"id": "log-uuid",
"tenant_id": "tenant-uuid",
"user_id": "user-uuid",
"user_email": "alice@acme.com",
"provider": "anthropic",
"model_requested": "gpt-4o",
"model_used": "claude-3-5-sonnet-20241022",
"prompt_tokens": 128,
"completion_tokens": 345,
"total_tokens": 473,
"cost_usd": 0.003412,
"latency_ms": 1423,
"pii_detected": true,
"pii_entities": ["PERSON", "EMAIL_ADDRESS"],
"policy_matched": "Legal → Anthropic",
"status_code": 200,
"timestamp": "2026-01-15T14:32:11Z"
}
],
"total": 142,
"limit": 10,
"offset": 0
}`}
/>
<Callout type="info" title="Audit-of-the-audit">
All accesses to audit logs are themselves logged. This satisfies the "audit-of-the-audit"
requirement for sensitive compliance use cases.
</Callout>
<h2 id="costs">Cost Breakdown</h2>
<ApiEndpoint method="GET" path="/v1/admin/costs" description="Aggregate cost breakdown by provider, model, or department." />
<ParamTable
title="Query Parameters"
params={[
{ name: "group_by", type: "string", required: false, default: "provider", description: "Grouping dimension: provider | model | department | user" },
{ name: "from", type: "string (ISO 8601)", required: false, description: "Start of billing period." },
{ name: "to", type: "string (ISO 8601)", required: false, description: "End of billing period." },
]}
/>
<CodeBlock
language="bash"
code={`curl "http://localhost:8090/v1/admin/costs?group_by=provider&from=2026-01-01T00:00:00Z" \\
-H "Authorization: Bearer $TOKEN"
# Response:
{
"data": [
{
"key": "openai",
"request_count": 1423,
"total_tokens": 2840000,
"prompt_tokens": 1200000,
"completion_tokens": 1640000,
"total_cost_usd": 28.40
},
{
"key": "anthropic",
"request_count": 231,
"total_tokens": 462000,
"prompt_tokens": 230000,
"completion_tokens": 232000,
"total_cost_usd": 6.93
}
],
"period_from": "2026-01-01T00:00:00Z",
"period_to": "2026-01-31T23:59:59Z"
}`}
/>
<h2 id="rate-limits">Rate Limit Overrides</h2>
<ApiEndpoint method="GET" path="/v1/admin/rate-limits" description="List per-tenant rate limit overrides." />
<ApiEndpoint method="GET" path="/v1/admin/rate-limits/{tenant_id}" description="Get rate limit configuration for a specific tenant." />
<CodeBlock
language="bash"
code={`# Get rate limit for a tenant
curl http://localhost:8090/v1/admin/rate-limits/tenant-uuid \\
-H "Authorization: Bearer $TOKEN"
# Response:
{
"tenant_id": "tenant-uuid",
"rpm": 500, # requests per minute
"tpm": 50000, # tokens per minute
"daily_token_limit": 1000000
}`}
/>
</div>
);
}

View File

@ -0,0 +1,124 @@
import { CodeBlock } from "../components/CodeBlock";
import { Callout } from "../components/Callout";
import { ApiEndpoint } from "../components/ApiEndpoint";
import { ParamTable } from "../components/ParamTable";
export function AdminPoliciesPage() {
return (
<div>
<h1 id="admin-policies">Admin Routing Policies</h1>
<p>
Routing policies (rules) define how AI requests are dispatched to providers. Rules are
evaluated in ascending priority order; the first matching rule wins.
</p>
<Callout type="info" title="Required role">
All <code>/v1/admin/policies</code> endpoints require the <code>admin</code> or{" "}
<code>manager</code> role.
</Callout>
<h2 id="list">List Policies</h2>
<ApiEndpoint method="GET" path="/v1/admin/policies" description="Return all routing rules for the current tenant, sorted by priority." />
<CodeBlock
language="bash"
code={`curl http://localhost:8090/v1/admin/policies \\
-H "Authorization: Bearer $TOKEN"
# Response:
{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Legal → Anthropic",
"priority": 10,
"provider": "anthropic",
"target_model": "claude-3-5-sonnet-20241022",
"fallback_providers": ["openai"],
"conditions": [
{"field": "user.department", "operator": "eq", "value": "legal"}
],
"enabled": true,
"created_at": "2026-01-15T10:00:00Z"
}
]
}`}
/>
<h2 id="create">Create Policy</h2>
<ApiEndpoint method="POST" path="/v1/admin/policies" description="Create a new routing rule." />
<ParamTable
params={[
{ name: "name", type: "string", required: true, description: "Human-readable rule name." },
{ name: "priority", type: "integer", required: true, description: "Evaluation order (lower = first). Must be unique per tenant." },
{ name: "provider", type: "string", required: true, description: "Target provider: openai | anthropic | azure | mistral | ollama" },
{ name: "target_model", type: "string", required: false, description: "Override the model requested by the client." },
{ name: "fallback_providers", type: "[]string", required: false, description: "Ordered list of fallback providers if the primary fails or circuit is open." },
{ name: "conditions", type: "[]Condition", required: false, description: "Array of conditions (all must match, AND-joined). Empty = catch-all." },
{ name: "enabled", type: "boolean", required: false, default: "true", description: "Whether the rule is active." },
]}
/>
<h3 id="conditions">Condition Fields</h3>
<div className="overflow-x-auto my-4">
<table className="w-full text-sm border rounded-lg overflow-hidden">
<thead>
<tr className="bg-muted/50 border-b">
<th className="text-left px-4 py-2.5 font-semibold">Field</th>
<th className="text-left px-4 py-2.5 font-semibold">Operators</th>
<th className="text-left px-4 py-2.5 font-semibold">Value example</th>
</tr>
</thead>
<tbody>
{[
{ field: "user.role", ops: "eq, neq, in, nin", ex: '"admin"' },
{ field: "user.department", ops: "eq, neq, in, nin, contains", ex: '"legal"' },
{ field: "request.model", ops: "eq, neq, in, nin", ex: '"gpt-4o"' },
{ field: "request.sensitivity", ops: "eq, neq, gte, lte", ex: '"high"' },
{ field: "request.use_case", ops: "eq, neq, contains, matches", ex: '"code_review"' },
{ field: "request.token_estimate", ops: "gte, lte", ex: '"4000"' },
].map((row) => (
<tr key={row.field} className="border-b last:border-0">
<td className="px-4 py-2.5 font-mono text-xs">{row.field}</td>
<td className="px-4 py-2.5 text-muted-foreground text-xs">{row.ops}</td>
<td className="px-4 py-2.5 font-mono text-xs text-blue-600 dark:text-blue-400">{row.ex}</td>
</tr>
))}
</tbody>
</table>
</div>
<CodeBlock
language="bash"
code={`curl -X POST http://localhost:8090/v1/admin/policies \\
-H "Authorization: Bearer $TOKEN" \\
-H "Content-Type: application/json" \\
-d '{
"name": "Legal → Anthropic",
"priority": 10,
"provider": "anthropic",
"target_model": "claude-3-5-sonnet-20241022",
"fallback_providers": ["openai"],
"conditions": [
{"field": "user.department", "operator": "eq", "value": "legal"}
]
}'`}
/>
<h2 id="update">Update Policy</h2>
<ApiEndpoint method="PUT" path="/v1/admin/policies/{id}" description="Update an existing routing rule (full replacement)." />
<h2 id="delete">Delete Policy</h2>
<ApiEndpoint method="DELETE" path="/v1/admin/policies/{id}" description="Permanently delete a routing rule." />
<h2 id="seed">Seed from Template</h2>
<ApiEndpoint method="POST" path="/v1/admin/policies/seed/{template}" description="Create a pre-built set of routing rules from a template." />
<p>Available templates:</p>
<ul>
<li><code>default</code> Single catch-all rule routing to OpenAI</li>
<li><code>rbac</code> Rules per RBAC role with model restrictions</li>
<li><code>department</code> Rules per department (legal, hr, engineering, finance)</li>
<li><code>cost-optimized</code> Routes to cheaper models for simple queries</li>
</ul>
</div>
);
}

View File

@ -0,0 +1,91 @@
import { CodeBlock } from "../components/CodeBlock";
import { Callout } from "../components/Callout";
import { ApiEndpoint } from "../components/ApiEndpoint";
import { ParamTable } from "../components/ParamTable";
export function AdminUsersPage() {
return (
<div>
<h1 id="admin-users">Admin Users</h1>
<p>
Manage users within a tenant. Users are synchronized from Keycloak but can have additional
metadata (department, cost overrides) managed through the admin API.
</p>
<Callout type="info" title="Required role">
Requires <code>admin</code> role. Managers can read users but cannot create or delete.
</Callout>
<h2 id="list">List Users</h2>
<ApiEndpoint method="GET" path="/v1/admin/users" description="Return all users for the current tenant." />
<CodeBlock
language="bash"
code={`curl http://localhost:8090/v1/admin/users \\
-H "Authorization: Bearer $TOKEN"
# Response:
{
"data": [
{
"id": "user-uuid",
"email": "alice@acme.com",
"name": "Alice Martin",
"role": "user",
"department": "legal",
"tenant_id": "tenant-uuid",
"created_at": "2026-01-01T00:00:00Z"
}
]
}`}
/>
<h2 id="create">Create User</h2>
<ApiEndpoint method="POST" path="/v1/admin/users" description="Create or sync a user in the tenant database." />
<ParamTable
params={[
{ name: "email", type: "string", required: true, description: "User email (must match Keycloak account)." },
{ name: "name", type: "string", required: true, description: "Display name." },
{ name: "role", type: "string", required: true, description: "RBAC role: admin | manager | user | auditor" },
{ name: "department", type: "string", required: false, description: "Department name — used in routing rule conditions." },
]}
/>
<h2 id="get">Get User</h2>
<ApiEndpoint method="GET" path="/v1/admin/users/{id}" description="Return a single user by ID." />
<h2 id="update">Update User</h2>
<ApiEndpoint method="PUT" path="/v1/admin/users/{id}" description="Update user metadata (role, department)." />
<h2 id="roles">RBAC Role Reference</h2>
<div className="overflow-x-auto my-4">
<table className="w-full text-sm border rounded-lg overflow-hidden">
<thead>
<tr className="bg-muted/50 border-b">
<th className="text-left px-4 py-2.5 font-semibold">Role</th>
<th className="text-left px-4 py-2.5 font-semibold">Inference</th>
<th className="text-left px-4 py-2.5 font-semibold">Models</th>
<th className="text-left px-4 py-2.5 font-semibold">Admin API</th>
<th className="text-left px-4 py-2.5 font-semibold">Audit/Compliance</th>
</tr>
</thead>
<tbody>
{[
{ role: "admin", infer: "Yes", models: "All", admin: "Full", audit: "Read" },
{ role: "manager", infer: "Yes", models: "All", admin: "Read-Write policies/users", audit: "Read" },
{ role: "user", infer: "Yes", models: "rbac.user_allowed_models only", admin: "None", audit: "None" },
{ role: "auditor", infer: "No", models: "—", admin: "None", audit: "Full read + export" },
].map((row) => (
<tr key={row.role} className="border-b last:border-0">
<td className="px-4 py-2.5 font-mono text-xs font-semibold">{row.role}</td>
<td className="px-4 py-2.5 text-xs">{row.infer}</td>
<td className="px-4 py-2.5 text-xs text-muted-foreground">{row.models}</td>
<td className="px-4 py-2.5 text-xs text-muted-foreground">{row.admin}</td>
<td className="px-4 py-2.5 text-xs text-muted-foreground">{row.audit}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}

View File

@ -0,0 +1,133 @@
import { CodeBlock } from "../components/CodeBlock";
import { Callout } from "../components/Callout";
export function AuthenticationPage() {
return (
<div>
<h1 id="authentication">Authentication</h1>
<p>
All <code>/v1/*</code> endpoints require a Bearer JWT in the{" "}
<code>Authorization</code> header. Veylant IA validates the token against Keycloak (OIDC)
or uses a mock verifier in development mode.
</p>
<h2 id="bearer-token">Bearer Token</h2>
<CodeBlock
language="bash"
code={`curl http://localhost:8090/v1/chat/completions \\
-H "Authorization: Bearer <your-access-token>" \\
-H "Content-Type: application/json" \\
-d '{"model": "gpt-4o", "messages": [{"role": "user", "content": "Hi"}]}'`}
/>
<h2 id="dev-mode">Development Mode</h2>
<Callout type="info" title="Development mode auth bypass">
When <code>server.env=development</code> and Keycloak is unreachable, the proxy uses a{" "}
<code>MockVerifier</code>. Any non-empty Bearer token is accepted. The authenticated user
is injected as <code>admin@veylant.dev</code> with <code>admin</code> role and tenant ID{" "}
<code>dev-tenant</code>.
</Callout>
<CodeBlock
language="bash"
code={`# Any string works as the token in dev mode
curl http://localhost:8090/v1/chat/completions \\
-H "Authorization: Bearer dev-token" \\
...`}
/>
<h2 id="keycloak-flow">Production: Keycloak OIDC Flow</h2>
<p>In production, clients obtain a token via the standard OIDC Authorization Code flow:</p>
<ol>
<li>Redirect user to Keycloak login page</li>
<li>User authenticates; Keycloak redirects back with an authorization code</li>
<li>Exchange code for tokens at the token endpoint</li>
<li>Use the <code>access_token</code> as the Bearer token</li>
</ol>
<CodeBlock
language="bash"
code={`# Token endpoint (replace values)
curl -X POST \\
http://localhost:8080/realms/veylant/protocol/openid-connect/token \\
-d "grant_type=password" \\
-d "client_id=veylant-proxy" \\
-d "username=admin@veylant.dev" \\
-d "password=admin123"
# Response includes:
{
"access_token": "eyJhbGci...",
"expires_in": 300,
"refresh_token": "eyJhbGci...",
"token_type": "Bearer"
}`}
/>
<h2 id="jwt-claims">JWT Claims</h2>
<p>The proxy extracts the following claims from the JWT:</p>
<div className="overflow-x-auto my-4">
<table className="w-full text-sm border rounded-lg overflow-hidden">
<thead>
<tr className="bg-muted/50 border-b">
<th className="text-left px-4 py-2.5 font-semibold">Claim</th>
<th className="text-left px-4 py-2.5 font-semibold">Source</th>
<th className="text-left px-4 py-2.5 font-semibold">Description</th>
</tr>
</thead>
<tbody>
{[
{ claim: "sub", source: "Standard JWT", desc: "User ID (UUID)" },
{ claim: "email", source: "Standard JWT", desc: "User email" },
{ claim: "realm_access.roles", source: "Keycloak extension", desc: "RBAC roles: admin, manager, user, auditor" },
{ claim: "veylant_tenant_id", source: "Keycloak mapper", desc: "Tenant UUID" },
{ claim: "department", source: "Keycloak user attribute", desc: "Department name for routing rules" },
].map((row) => (
<tr key={row.claim} className="border-b last:border-0">
<td className="px-4 py-2.5 font-mono text-xs">{row.claim}</td>
<td className="px-4 py-2.5 text-muted-foreground text-xs">{row.source}</td>
<td className="px-4 py-2.5 text-muted-foreground text-xs">{row.desc}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 id="test-users">Pre-configured Test Users</h2>
<p>The Keycloak realm export includes these users for testing:</p>
<CodeBlock
language="bash"
code={`# Admin user (full access)
username: admin@veylant.dev
password: admin123
roles: admin
# Regular user (restricted to allowed models)
username: user@veylant.dev
password: user123
roles: user`}
/>
<h2 id="error-responses">Auth Error Responses</h2>
<p>Authentication errors always return OpenAI-format JSON:</p>
<CodeBlock
language="json"
code={`// 401 — Missing or invalid token
{
"error": {
"type": "authentication_error",
"message": "invalid or missing authorization token",
"code": "invalid_api_key"
}
}
// 403 — Valid token, insufficient role
{
"error": {
"type": "permission_error",
"message": "role 'user' does not have access to model 'gpt-4o'. Allowed: gpt-4o-mini",
"code": "permission_denied"
}
}`}
/>
</div>
);
}

View File

@ -0,0 +1,194 @@
import { CodeBlock } from "../components/CodeBlock";
import { Callout } from "../components/Callout";
import { ApiEndpoint } from "../components/ApiEndpoint";
import { ParamTable } from "../components/ParamTable";
export function ChatCompletionsPage() {
return (
<div>
<h1 id="chat-completions">Chat Completions</h1>
<p>
The primary inference endpoint. Fully compatible with the OpenAI Chat Completions API
switch your <code>base_url</code> to Veylant IA and all existing SDK calls work unchanged.
</p>
<ApiEndpoint
method="POST"
path="/v1/chat/completions"
description="Create an AI chat completion with automatic PII anonymization, routing, and audit logging."
/>
<Callout type="info" title="OpenAI-compatible">
This endpoint is a superset of the OpenAI Chat Completions API. All standard OpenAI
parameters are supported and forwarded to the upstream provider. Veylant IA adds governance
on top without changing the request/response schema.
</Callout>
<h2 id="request">Request Body</h2>
<ParamTable
params={[
{ name: "model", type: "string", required: true, description: "Model ID to use (e.g. gpt-4o, claude-3-5-sonnet-20241022). The routing engine may override this." },
{ name: "messages", type: "array", required: true, description: "List of messages in the conversation. Each message has role (system|user|assistant) and content." },
{ name: "stream", type: "boolean", required: false, default: "false", description: "If true, responses are streamed as Server-Sent Events (SSE). Compatible with EventSource and the OpenAI SDK streaming API." },
{ name: "temperature", type: "float", required: false, default: "1.0", description: "Sampling temperature (02). Forwarded to the provider." },
{ name: "max_tokens", type: "integer", required: false, description: "Maximum tokens to generate. Forwarded to the provider." },
{ name: "top_p", type: "float", required: false, description: "Nucleus sampling. Forwarded to the provider." },
{ name: "n", type: "integer", required: false, default: "1", description: "Number of completions. Note: PII anonymization applies to each choice." },
]}
/>
<h2 id="example-request">Example Request</h2>
<CodeBlock
language="bash"
code={`curl http://localhost:8090/v1/chat/completions \\
-H "Authorization: Bearer $TOKEN" \\
-H "Content-Type: application/json" \\
-d '{
"model": "gpt-4o",
"messages": [
{
"role": "system",
"content": "You are a helpful assistant."
},
{
"role": "user",
"content": "Summarize the contract for John Smith (john@acme.com)."
}
],
"temperature": 0.7
}'`}
/>
<h2 id="pii-anonymization">PII Anonymization in Action</h2>
<p>
Before the request is forwarded to the LLM, the PII service scans all message content.
Detected entities are anonymized in the prompt. The audit log records what was found.
</p>
<CodeBlock
language="json"
code={`// Original user message:
"Summarize the contract for John Smith (john@acme.com)."
// After PII anonymization (what the LLM sees):
"Summarize the contract for [PERSON] ([EMAIL_ADDRESS])."
// Audit log records:
{
"pii_entities": [
{"type": "PERSON", "original": "John Smith", "start": 31, "end": 41},
{"type": "EMAIL_ADDRESS", "original": "john@acme.com", "start": 43, "end": 56}
]
}`}
/>
<h2 id="response">Response</h2>
<p>Responses are identical to the OpenAI API format:</p>
<CodeBlock
language="json"
code={`{
"id": "chatcmpl-veylant-8f3k2j",
"object": "chat.completion",
"created": 1735000000,
"model": "gpt-4o",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Here is a summary of the contract..."
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 28,
"completion_tokens": 142,
"total_tokens": 170
}
}`}
/>
<h2 id="streaming">Streaming (SSE)</h2>
<p>
Set <code>"stream": true</code> to receive chunks via Server-Sent Events. PII
anonymization applies to the <strong>request</strong> before it's sent upstream not to
the streamed response. This keeps streaming latency minimal.
</p>
<CodeBlock
language="bash"
code={`curl http://localhost:8090/v1/chat/completions \\
-H "Authorization: Bearer $TOKEN" \\
-H "Content-Type: application/json" \\
-d '{
"model": "gpt-4o",
"messages": [{"role": "user", "content": "Tell me a story"}],
"stream": true
}'
# Response (streaming chunks):
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","choices":[{"delta":{"content":"Once"},"index":0}]}
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","choices":[{"delta":{"content":" upon"},"index":0}]}
data: [DONE]`}
/>
<CodeBlock
language="python"
code={`from openai import OpenAI
client = OpenAI(base_url="http://localhost:8090/v1", api_key="dev-token")
stream = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Tell me a story"}],
stream=True,
)
for chunk in stream:
print(chunk.choices[0].delta.content or "", end="", flush=True)`}
/>
<h2 id="errors">Error Responses</h2>
<CodeBlock
language="json"
code={`// 429 — Rate limit exceeded
{
"error": {
"type": "rate_limit_error",
"message": "rate limit exceeded, retry after 1 second",
"code": "rate_limit_exceeded"
}
}
// Headers:
// Retry-After: 1
// 503 — Provider unavailable (circuit breaker open)
{
"error": {
"type": "provider_error",
"message": "all providers unavailable",
"code": "service_unavailable"
}
}
// 400 — Bad request
{
"error": {
"type": "invalid_request_error",
"message": "messages field is required",
"code": "invalid_request"
}
}`}
/>
<h2 id="routing-override">How Routing Affects the Model</h2>
<p>
The routing engine evaluates rules against the request context. A matched rule can override
the requested model or select a different provider. The <code>model</code> in the response
reflects the model actually used (which may differ from what was requested).
</p>
<Callout type="tip">
Use the <code>GET /v1/admin/logs</code> endpoint to see <code>model_requested</code> vs{" "}
<code>model_used</code> in the audit trail.
</Callout>
</div>
);
}

View File

@ -0,0 +1,136 @@
import { CodeBlock } from "../components/CodeBlock";
import { Callout } from "../components/Callout";
import { ApiEndpoint } from "../components/ApiEndpoint";
import { ParamTable } from "../components/ParamTable";
export function PiiAnalysisPage() {
return (
<div>
<h1 id="pii-analysis">PII Analysis</h1>
<p>
Analyze text for PII on demand. This endpoint exposes the same 3-layer detection pipeline
used internally for prompt anonymization useful for testing policies, building pre-flight
checks, or auditing content.
</p>
<ApiEndpoint
method="POST"
path="/v1/pii/analyze"
description="Detect and optionally anonymize PII entities in a text string."
/>
<h2 id="request">Request Body</h2>
<ParamTable
params={[
{ name: "text", type: "string", required: true, description: "The text to analyze for PII entities." },
{ name: "anonymize", type: "boolean", required: false, default: "false", description: "If true, return an anonymized version of the text with PII replaced." },
{ name: "store_mapping", type: "boolean", required: false, default: "false", description: "If true and anonymize=true, store the original→synthetic mapping in Redis (AES-256-GCM encrypted, TTL-based). Enables de-pseudonymization." },
{ name: "language", type: "string", required: false, default: "fr", description: "Language hint for NER (fr | en). Affects which spaCy model is used." },
]}
/>
<h2 id="example">Example</h2>
<CodeBlock
language="bash"
code={`curl http://localhost:8090/v1/pii/analyze \\
-H "Authorization: Bearer $TOKEN" \\
-H "Content-Type: application/json" \\
-d '{
"text": "Call John Smith at +33 6 12 34 56 78 or john.smith@acme.com. His IBAN is FR76 3000 6000 0112 3456 7890 189.",
"anonymize": true
}'`}
/>
<h2 id="response">Response</h2>
<CodeBlock
language="json"
code={`{
"entities": [
{
"type": "PERSON",
"text": "John Smith",
"start": 5,
"end": 15,
"score": 0.98,
"layer": "ner"
},
{
"type": "PHONE_NUMBER",
"text": "+33 6 12 34 56 78",
"start": 19,
"end": 37,
"score": 1.0,
"layer": "regex"
},
{
"type": "EMAIL_ADDRESS",
"text": "john.smith@acme.com",
"start": 41,
"end": 60,
"score": 1.0,
"layer": "regex"
},
{
"type": "IBAN_CODE",
"text": "FR76 3000 6000 0112 3456 7890 189",
"start": 73,
"end": 106,
"score": 1.0,
"layer": "regex"
}
],
"anonymized_text": "Call [PERSON] at [PHONE_NUMBER] or [EMAIL_ADDRESS]. His IBAN is [IBAN_CODE].",
"entity_count": 4,
"latency_ms": 23
}`}
/>
<h2 id="entity-types">Supported Entity Types</h2>
<div className="overflow-x-auto my-4">
<table className="w-full text-sm border rounded-lg overflow-hidden">
<thead>
<tr className="bg-muted/50 border-b">
<th className="text-left px-4 py-2.5 font-semibold">Type</th>
<th className="text-left px-4 py-2.5 font-semibold">Layer</th>
<th className="text-left px-4 py-2.5 font-semibold">Examples</th>
</tr>
</thead>
<tbody>
{[
{ type: "EMAIL_ADDRESS", layer: "regex", ex: "user@example.com" },
{ type: "PHONE_NUMBER", layer: "regex", ex: "+33 6 12 34 56 78, 06.12.34.56.78" },
{ type: "IBAN_CODE", layer: "regex", ex: "FR76 3000 6000 0112 3456 7890 189" },
{ type: "CREDIT_CARD", layer: "regex", ex: "4532 1234 5678 9012" },
{ type: "SSN", layer: "regex", ex: "1 85 12 75 108 112 48" },
{ type: "PERSON", layer: "ner", ex: "John Smith, Marie Dupont" },
{ type: "LOCATION", layer: "ner", ex: "Paris, 75001, rue de Rivoli" },
{ type: "ORGANIZATION", layer: "ner", ex: "Acme Corp, Banque de France" },
{ type: "DATE_TIME", layer: "ner", ex: "12/03/1985, le 3 mars" },
{ type: "NRP", layer: "ner", ex: "Nationality, religion, political party" },
].map((row) => (
<tr key={row.type} className="border-b last:border-0">
<td className="px-4 py-2.5 font-mono text-xs">{row.type}</td>
<td className="px-4 py-2.5">
<span className={`text-xs px-1.5 py-0.5 rounded font-medium ${row.layer === "regex" ? "bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300" : "bg-purple-100 dark:bg-purple-900/40 text-purple-700 dark:text-purple-300"}`}>
{row.layer}
</span>
</td>
<td className="px-4 py-2.5 text-muted-foreground text-xs">{row.ex}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 id="playground">Interactive Playground</h2>
<Callout type="tip" title="Public playground">
Try PII detection without authentication at{" "}
<a href="http://localhost:8090/playground" target="_blank" rel="noopener noreferrer">
http://localhost:8090/playground
</a>
. The playground endpoint (<code>POST /playground/analyze</code>) is IP-rate-limited to
20 requests/minute and requires no Bearer token.
</Callout>
</div>
);
}

View File

@ -0,0 +1,38 @@
import { cn } from "@/lib/utils";
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
interface ApiEndpointProps {
method: HttpMethod;
path: string;
description?: string;
}
const methodColors: Record<HttpMethod, string> = {
GET: "bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300",
POST: "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300",
PUT: "bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300",
PATCH: "bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300",
DELETE: "bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300",
};
export function ApiEndpoint({ method, path, description }: ApiEndpointProps) {
return (
<div className="my-4 rounded-lg border bg-card overflow-hidden">
<div className="flex items-center gap-3 px-4 py-3">
<span
className={cn(
"text-xs font-bold px-2 py-0.5 rounded font-mono uppercase tracking-wide",
methodColors[method]
)}
>
{method}
</span>
<code className="text-sm font-mono text-foreground">{path}</code>
</div>
{description && (
<div className="px-4 pb-3 text-sm text-muted-foreground border-t">{description}</div>
)}
</div>
);
}

View File

@ -0,0 +1,64 @@
import { Info, AlertTriangle, XCircle, Lightbulb } from "lucide-react";
import { cn } from "@/lib/utils";
type CalloutType = "info" | "warning" | "danger" | "tip";
interface CalloutProps {
type?: CalloutType;
title?: string;
children: React.ReactNode;
}
const configs: Record<
CalloutType,
{ icon: React.ElementType; borderColor: string; bgColor: string; titleColor: string; iconColor: string }
> = {
info: {
icon: Info,
borderColor: "border-blue-400",
bgColor: "bg-blue-50 dark:bg-blue-950/30",
titleColor: "text-blue-800 dark:text-blue-200",
iconColor: "text-blue-500",
},
warning: {
icon: AlertTriangle,
borderColor: "border-amber-400",
bgColor: "bg-amber-50 dark:bg-amber-950/30",
titleColor: "text-amber-800 dark:text-amber-200",
iconColor: "text-amber-500",
},
danger: {
icon: XCircle,
borderColor: "border-red-400",
bgColor: "bg-red-50 dark:bg-red-950/30",
titleColor: "text-red-800 dark:text-red-200",
iconColor: "text-red-500",
},
tip: {
icon: Lightbulb,
borderColor: "border-green-400",
bgColor: "bg-green-50 dark:bg-green-950/30",
titleColor: "text-green-800 dark:text-green-200",
iconColor: "text-green-500",
},
};
export function Callout({ type = "info", title, children }: CalloutProps) {
const { icon: Icon, borderColor, bgColor, titleColor, iconColor } = configs[type];
const defaultTitles: Record<CalloutType, string> = {
info: "Note",
warning: "Warning",
danger: "Danger",
tip: "Tip",
};
return (
<div className={cn("flex gap-3 rounded-lg border-l-4 p-4 my-4", borderColor, bgColor)}>
<Icon className={cn("h-5 w-5 mt-0.5 shrink-0", iconColor)} />
<div className="text-sm leading-relaxed">
<p className={cn("font-semibold mb-1", titleColor)}>{title ?? defaultTitles[type]}</p>
<div className="text-foreground/80">{children}</div>
</div>
</div>
);
}

View File

@ -0,0 +1,48 @@
import { useState } from "react";
import { Copy, Check } from "lucide-react";
import { cn } from "@/lib/utils";
interface CodeBlockProps {
code: string;
language?: string;
className?: string;
}
export function CodeBlock({ code, language = "bash", className }: CodeBlockProps) {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
await navigator.clipboard.writeText(code.trim());
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className={cn("relative rounded-lg overflow-hidden my-4 group not-prose", className)}>
{/* Header bar */}
<div className="flex items-center justify-between bg-zinc-800 px-4 py-2">
<span className="text-xs text-zinc-400 font-mono uppercase tracking-wide">{language}</span>
<button
onClick={handleCopy}
className="flex items-center gap-1.5 text-xs text-zinc-400 hover:text-zinc-200 transition-colors"
>
{copied ? (
<>
<Check className="h-3.5 w-3.5 text-green-400" />
<span className="text-green-400">Copied!</span>
</>
) : (
<>
<Copy className="h-3.5 w-3.5" />
<span>Copy</span>
</>
)}
</button>
</div>
{/* Code body */}
<pre className="bg-zinc-900 text-zinc-100 overflow-x-auto p-4 text-sm leading-relaxed">
<code className="font-mono">{code.trim()}</code>
</pre>
</div>
);
}

View File

@ -0,0 +1,56 @@
import { cn } from "@/lib/utils";
export interface Param {
name: string;
type: string;
required?: boolean;
description: string;
default?: string;
}
interface ParamTableProps {
params: Param[];
title?: string;
}
export function ParamTable({ params, title = "Parameters" }: ParamTableProps) {
return (
<div className="my-4">
{title && <h4 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">{title}</h4>}
<div className="rounded-lg border overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="bg-muted/50 border-b">
<th className="text-left px-4 py-2.5 font-semibold">Name</th>
<th className="text-left px-4 py-2.5 font-semibold">Type</th>
<th className="text-left px-4 py-2.5 font-semibold">Required</th>
<th className="text-left px-4 py-2.5 font-semibold">Description</th>
</tr>
</thead>
<tbody>
{params.map((p, i) => (
<tr key={p.name} className={cn("border-b last:border-0", i % 2 === 0 ? "" : "bg-muted/20")}>
<td className="px-4 py-2.5">
<code className="text-xs font-mono bg-muted px-1.5 py-0.5 rounded">{p.name}</code>
</td>
<td className="px-4 py-2.5">
<span className="text-xs font-mono text-blue-600 dark:text-blue-400">{p.type}</span>
</td>
<td className="px-4 py-2.5">
{p.required ? (
<span className="text-xs text-red-600 dark:text-red-400 font-medium">required</span>
) : (
<span className="text-xs text-muted-foreground">
optional{p.default ? ` (${p.default})` : ""}
</span>
)}
</td>
<td className="px-4 py-2.5 text-muted-foreground">{p.description}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}

View File

@ -0,0 +1,94 @@
import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";
import { useLocation } from "react-router-dom";
interface TocItem {
id: string;
title: string;
level: number;
}
function useTocItems(): TocItem[] {
const [items, setItems] = useState<TocItem[]>([]);
const location = useLocation();
useEffect(() => {
// Small delay to allow page to render
const timer = setTimeout(() => {
const headings = Array.from(document.querySelectorAll("[data-doc-content] h2, [data-doc-content] h3"));
const tocItems: TocItem[] = headings.map((el) => ({
id: el.id,
title: el.textContent ?? "",
level: el.tagName === "H2" ? 2 : 3,
}));
setItems(tocItems);
}, 50);
return () => clearTimeout(timer);
}, [location.pathname]);
return items;
}
function useActiveId(ids: string[]): string {
const [activeId, setActiveId] = useState("");
useEffect(() => {
if (ids.length === 0) return;
const observer = new IntersectionObserver(
(entries) => {
const visible = entries.filter((e) => e.isIntersecting);
if (visible.length > 0) {
setActiveId(visible[0].target.id);
}
},
{ rootMargin: "0% 0% -70% 0%", threshold: 0 }
);
ids.forEach((id) => {
const el = document.getElementById(id);
if (el) observer.observe(el);
});
return () => observer.disconnect();
}, [ids]);
return activeId;
}
export function TableOfContents() {
const items = useTocItems();
const activeId = useActiveId(items.map((i) => i.id));
if (items.length === 0) return null;
return (
<nav className="sticky top-20">
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">
On this page
</p>
<ul className="space-y-1">
{items.map((item) => (
<li key={item.id}>
<a
href={`#${item.id}`}
className={cn(
"block text-sm leading-5 py-0.5 transition-colors",
item.level === 3 && "pl-4",
activeId === item.id
? "text-primary font-medium"
: "text-muted-foreground hover:text-foreground"
)}
onClick={(e) => {
e.preventDefault();
document.getElementById(item.id)?.scrollIntoView({ behavior: "smooth" });
}}
>
{item.title}
</a>
</li>
))}
</ul>
</nav>
);
}

View File

@ -0,0 +1,107 @@
import { CodeBlock } from "../components/CodeBlock";
import { Callout } from "../components/Callout";
export function BlueGreenPage() {
return (
<div>
<h1 id="blue-green">Blue/Green Deployment</h1>
<p>
Veylant IA supports zero-downtime deployments via blue/green traffic switching with Istio.
Two Helm releases (<code>veylant-proxy-blue</code> and <code>veylant-proxy-green</code>)
run simultaneously; traffic is switched via an Istio VirtualService patch.
</p>
<Callout type="tip" title="Rollback time">
Traffic rollback is under <strong>5 seconds</strong> just patch the VirtualService weight.
No pod restarts required.
</Callout>
<h2 id="architecture">Architecture</h2>
<div className="my-4 rounded-lg border bg-zinc-950 text-zinc-100 p-4 font-mono text-xs leading-relaxed overflow-x-auto">
<pre>{`Internet
Istio Gateway
VirtualService (veylant-proxy)
weight: 100 Service: veylant-proxy-blue (ACTIVE)
weight: 0 Service: veylant-proxy-green (STANDBY)
Helm releases:
veylant-proxy-blue: 3 replicas, image: 1.0.0 current production
veylant-proxy-green: 3 replicas, image: 1.1.0 new version (deployed but no traffic)`}</pre>
</div>
<h2 id="deploy-new-version">Deploying a New Version</h2>
<CodeBlock
language="bash"
code={`# Step 1: Deploy to the inactive slot (green)
make deploy-green IMAGE_TAG=1.1.0
# Step 2: Verify green is healthy
kubectl rollout status deployment/veylant-proxy-green -n veylant
curl http://green.internal.veylant.local/healthz
# Step 3: Switch traffic (blue green)
kubectl patch virtualservice veylant-proxy -n veylant --type=json -p '[
{"op":"replace","path":"/spec/http/0/route/0/weight","value":0},
{"op":"replace","path":"/spec/http/0/route/1/weight","value":100}
]'
# Step 4: Verify production traffic on green
kubectl logs -l app=veylant-proxy,slot=green -n veylant --tail=20`}
/>
<h2 id="rollback">Rollback</h2>
<CodeBlock
language="bash"
code={`# Instant rollback — switch traffic back to blue
make deploy-rollback ACTIVE_SLOT=blue
# Or manually:
kubectl patch virtualservice veylant-proxy -n veylant --type=json -p '[
{"op":"replace","path":"/spec/http/0/route/0/weight","value":100},
{"op":"replace","path":"/spec/http/0/route/1/weight","value":0}
]'`}
/>
<h2 id="make-commands">Make Commands</h2>
<div className="overflow-x-auto my-4">
<table className="w-full text-sm border rounded-lg overflow-hidden">
<thead>
<tr className="bg-muted/50 border-b">
<th className="text-left px-4 py-2.5 font-semibold">Command</th>
<th className="text-left px-4 py-2.5 font-semibold">Action</th>
</tr>
</thead>
<tbody>
{[
{ cmd: "make deploy-blue IMAGE_TAG=X.Y.Z", action: "Deploy IMAGE_TAG to blue slot (Helm upgrade)" },
{ cmd: "make deploy-green IMAGE_TAG=X.Y.Z", action: "Deploy IMAGE_TAG to green slot (Helm upgrade)" },
{ cmd: "make deploy-rollback ACTIVE_SLOT=blue", action: "Switch 100% traffic to blue slot" },
{ cmd: "make deploy-rollback ACTIVE_SLOT=green", action: "Switch 100% traffic to green slot" },
{ cmd: "make helm-dry-run", action: "Render Helm templates without deploying" },
{ cmd: "make helm-deploy IMAGE_TAG=X.Y.Z", action: "Deploy to staging (requires KUBECONFIG)" },
].map((row) => (
<tr key={row.cmd} className="border-b last:border-0">
<td className="px-4 py-2.5 font-mono text-xs">{row.cmd}</td>
<td className="px-4 py-2.5 text-xs text-muted-foreground">{row.action}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 id="release-pipeline">Release Pipeline</h2>
<p>
Tagging a version (<code>v*</code>) triggers the GitHub Actions release pipeline:
</p>
<ol>
<li>Multi-arch Docker build (amd64/arm64) pushed to GHCR</li>
<li>Helm chart packaged as OCI artifact pushed to GHCR</li>
<li>GitHub Release created with CHANGELOG.md notes extracted automatically</li>
<li>Trivy image scan (CRITICAL/HIGH blocking)</li>
<li>gitleaks secret detection</li>
</ol>
</div>
);
}

View File

@ -0,0 +1,71 @@
import { CodeBlock } from "../components/CodeBlock";
import { Callout } from "../components/Callout";
export function DockerPage() {
return (
<div>
<h1 id="docker-deployment">Docker Compose Deployment</h1>
<p>
For small to medium deployments (single server, staging), Docker Compose is the recommended
approach. The production configuration uses the same services as local development with
hardened settings.
</p>
<h2 id="production-config">Production Configuration</h2>
<Callout type="warning" title="Before production deployment">
Ensure you have set: <code>server.env=production</code>, a strong <code>crypto.key</code>,
TLS certificates for all services, PostgreSQL with TLS, and proper secrets management
(HashiCorp Vault recommended).
</Callout>
<CodeBlock
language="bash"
code={`# Production environment variables (set via secrets manager, not .env)
VEYLANT_SERVER_ENV=production
VEYLANT_SERVER_PORT=8090
VEYLANT_CRYPTO_KEY=$(openssl rand -base64 32)
VEYLANT_DATABASE_URL=postgres://veylant_app:STRONG_PASSWORD@postgres:5432/veylant?sslmode=require
VEYLANT_REDIS_URL=redis://:REDIS_PASSWORD@redis:6379
VEYLANT_CLICKHOUSE_DSN=clickhouse://clickhouse:9000/veylant?dial_timeout=5s
VEYLANT_KEYCLOAK_BASE_URL=https://keycloak.yourdomain.com
VEYLANT_PROVIDERS_OPENAI_API_KEY=sk-...
VEYLANT_PII_FAIL_OPEN=false`}
/>
<h2 id="build">Building the Production Image</h2>
<CodeBlock
language="bash"
code={`# Build multi-arch image (amd64 + arm64)
docker buildx build \\
--platform linux/amd64,linux/arm64 \\
--tag ghcr.io/veylant/ia-gateway:1.0.0 \\
--push .
# Run with production config
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d`}
/>
<h2 id="health-check">Health Checks</h2>
<CodeBlock
language="bash"
code={`# Check all services
make health # curl localhost:8090/healthz
# Check individual services
curl http://localhost:8090/healthz
curl http://localhost:8091/healthz # PII service`}
/>
<h2 id="backup">Database Backup</h2>
<CodeBlock
language="bash"
code={`# PostgreSQL backup (runs daily at 02:00 UTC via CronJob in Kubernetes)
pg_dump -h postgres -U veylant -d veylant \\
| gzip > backup-$(date +%Y%m%d).sql.gz
# Restore
gunzip -c backup-20260115.sql.gz | psql -h postgres -U veylant -d veylant`}
/>
</div>
);
}

View File

@ -0,0 +1,117 @@
import { CodeBlock } from "../components/CodeBlock";
import { Callout } from "../components/Callout";
export function KubernetesPage() {
return (
<div>
<h1 id="kubernetes">Kubernetes Deployment (Helm)</h1>
<p>
Veylant IA ships a production-grade Helm chart at{" "}
<code>deploy/helm/veylant-proxy/</code>. The chart includes Deployment, Service, HPA,
PodDisruptionBudget, and ServiceMonitor resources.
</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>Kubernetes 1.28+ (EKS v1.31 tested)</li>
<li>Helm 3.12+</li>
<li>Metrics Server (for HPA)</li>
<li>Prometheus Operator (optional, for ServiceMonitor)</li>
<li>Istio 1.20+ (for blue/green deployment)</li>
</ul>
<h2 id="install">Install the Chart</h2>
<CodeBlock
language="bash"
code={`# Add the Helm repository (GHCR OCI)
helm pull oci://ghcr.io/veylant/charts/veylant-proxy --version 1.0.0
# Install with production values
helm upgrade --install veylant-proxy \\
oci://ghcr.io/veylant/charts/veylant-proxy \\
--version 1.0.0 \\
--namespace veylant \\
--create-namespace \\
-f deploy/helm/veylant-proxy/values-production.yaml \\
--set image.tag=1.0.0 \\
--set config.crypto.key=$(openssl rand -base64 32) \\
--set config.providers.openai.apiKey=$OPENAI_API_KEY`}
/>
<h2 id="values">Production Values</h2>
<CodeBlock
language="yaml"
code={`# values-production.yaml
replicaCount: 3
image:
repository: ghcr.io/veylant/ia-gateway
tag: "1.0.0"
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 15
targetCPUUtilizationPercentage: 70
targetMemoryUtilizationPercentage: 80
resources:
requests:
cpu: "500m"
memory: "256Mi"
limits:
cpu: "2000m"
memory: "1Gi"
config:
server:
env: production
pii:
fail_open: false # return 503 on PII failure in production
podDisruptionBudget:
enabled: true
minAvailable: 1`}
/>
<h2 id="hpa">Horizontal Pod Autoscaler</h2>
<p>
The HPA scales from 3 to 15 replicas based on CPU (70%) and memory (80%) utilization.
Scale-up is fast (30s stabilization); scale-down is conservative (5 minutes) to avoid
thrashing.
</p>
<CodeBlock
language="bash"
code={`# Check HPA status
kubectl get hpa -n veylant
# Watch scaling events
kubectl describe hpa veylant-proxy -n veylant`}
/>
<h2 id="terraform">Terraform (AWS EKS)</h2>
<p>
Infrastructure as Code for a production EKS cluster is in{" "}
<code>deploy/terraform/</code>:
</p>
<ul>
<li>EKS v1.31, 3-AZ node groups (t3.large)</li>
<li>S3 bucket for PostgreSQL backups (7-day retention)</li>
<li>IRSA for pod-level AWS permissions</li>
<li>VPC, subnets, security groups</li>
</ul>
<CodeBlock
language="bash"
code={`cd deploy/terraform
terraform init
terraform plan -var="region=eu-west-3" -var="cluster_name=veylant-prod"
terraform apply`}
/>
<Callout type="warning" title="Terraform state">
Configure remote state (S3 + DynamoDB locking) before running in production. The default
local state is not suitable for team use.
</Callout>
</div>
);
}

View File

@ -0,0 +1,127 @@
import { Callout } from "../components/Callout";
const concepts = [
{
term: "Tenant",
definition:
"A logical unit of isolation — typically a company or business unit. All data in PostgreSQL is isolated by tenant_id via Row-Level Security. A tenant can have multiple users, multiple routing rules, and separate cost quotas.",
},
{
term: "Routing Rule",
definition:
"A policy that matches incoming AI requests based on conditions (user role, department, model, token estimate, sensitivity) and routes them to a specific provider with optional fallback. Rules are sorted by priority (lower = evaluated first). First match wins.",
},
{
term: "PII (Personally Identifiable Information)",
definition:
"Data that can identify a person: names, email addresses, phone numbers, IBANs, SSNs, credit card numbers, etc. Veylant IA detects and anonymizes PII in prompts before they leave your network.",
},
{
term: "Pseudonymization",
definition:
"A reversible PII replacement technique. Detected PII tokens are replaced with synthetic identifiers (e.g., PERSON_001) and the original→synthetic mapping is stored in Redis (AES-256-GCM encrypted, TTL-based). The LLM works with the synthetic data; the response can optionally be de-pseudonymized.",
},
{
term: "Audit Log",
definition:
"An immutable record of every AI request: tenant, user, model, provider, token counts, cost, PII entities detected, policy matched, latency, and response status. Stored in ClickHouse (append-only). Retention via TTL policies — no DELETE operations.",
},
{
term: "Provider Adapter",
definition:
"A Go interface (Send, Stream, Validate, HealthCheck) implemented for each LLM provider. The routing engine selects the adapter; all adapters return OpenAI-format responses regardless of the upstream API.",
},
{
term: "Circuit Breaker",
definition:
"A per-provider failure counter. When failures exceed a threshold (default: 5), the breaker opens and the provider is bypassed for a TTL period (default: 60s). The fallback chain in the routing rule is used instead.",
},
{
term: "RBAC",
definition:
"Role-Based Access Control. Four roles: admin (full access), manager (read-write policies and users), user (inference only, restricted models), auditor (read-only logs and compliance, no inference). Roles are embedded in the Keycloak JWT.",
},
{
term: "Feature Flag",
definition:
"A boolean or string flag stored in PostgreSQL with an in-memory cache. Used to gate features without redeployment. Falls back to in-memory defaults if the database is unavailable.",
},
{
term: "GDPR Article 30",
definition:
"The GDPR requirement to maintain a Record of Processing Activities (ROPA). Veylant IA provides a built-in registry with fields for use case, legal basis, data categories, retention period, recipients, and processors.",
},
{
term: "EU AI Act",
definition:
"EU regulation classifying AI systems by risk level: forbidden, high, limited, or minimal. Veylant IA's compliance module helps you classify each use case through a structured questionnaire and generates PDF reports.",
},
{
term: "SLO (Service Level Objective)",
definition:
"Veylant IA targets 99.5% availability and p95 latency < 500ms. These are tracked in the production Grafana dashboard with an error budget that updates in real time.",
},
];
export function KeyConceptsPage() {
return (
<div>
<h1 id="key-concepts">Key Concepts</h1>
<p>
This glossary explains the core abstractions you'll encounter when working with Veylant IA.
</p>
<Callout type="tip" title="Reading order">
If you're new to Veylant IA, read{" "}
<a href="/docs">What is Veylant IA?</a> first, then come back here before
diving into the API reference or guides.
</Callout>
<h2 id="glossary">Glossary</h2>
<div className="space-y-4 my-4">
{concepts.map((c) => (
<div key={c.term} className="rounded-lg border bg-card p-4">
<dt className="font-semibold text-foreground mb-1.5">{c.term}</dt>
<dd className="text-sm text-muted-foreground leading-relaxed">{c.definition}</dd>
</div>
))}
</div>
<h2 id="request-lifecycle">Request Lifecycle</h2>
<p>What happens when a client sends a request to <code>POST /v1/chat/completions</code>:</p>
<div className="my-4 rounded-lg border bg-zinc-950 text-zinc-100 p-4 font-mono text-xs leading-loose overflow-x-auto">
<pre>{`1. Request arrives at Go proxy (:8090)
2. RequestID middleware generate X-Request-ID
3. SecurityHeaders middleware set CSP, HSTS, COOP headers
4. CORS middleware validate Origin header
5. Auth middleware validate Bearer JWT (Keycloak or mock)
extract tenant_id, user_id, role, department from claims
6. RateLimit middleware check per-tenant token bucket (Redis)
if exceeded: 429 with Retry-After header
7. RBAC check validate role has access to requested model
8. Routing engine evaluate rules (priority ASC, first match)
select provider + fallback chain
9. PII detection gRPC call to PII service (<50ms budget)
anonymize/pseudonymize prompt
10. Circuit breaker check skip if provider is open
11. Provider adapter forward to LLM (stream or batch)
12. Audit logger async ClickHouse write (non-blocking)
13. Response returned to client`}</pre>
</div>
<h2 id="multi-tenancy">Multi-tenancy Model</h2>
<p>
Veylant IA uses <strong>logical isolation</strong> via PostgreSQL Row-Level Security (RLS).
The application connects as role <code>veylant_app</code> and sets{" "}
<code>app.tenant_id</code> per session using a middleware. All queries automatically filter
by tenant without requiring explicit <code>WHERE</code> clauses in application code.
</p>
<p>
Physical isolation (separate database instances per tenant) is a V2 feature. See the
feedback backlog.
</p>
</div>
);
}

View File

@ -0,0 +1,167 @@
import { CodeBlock } from "../components/CodeBlock";
import { Callout } from "../components/Callout";
export function QuickStartPage() {
return (
<div>
<h1 id="quick-start">Quick Start</h1>
<p>
This guide gets you from zero to a running Veylant IA instance in under 5 minutes using
Docker Compose.
</p>
<Callout type="info" title="Prerequisites">
You need <strong>Docker 24+</strong> and <strong>Docker Compose v2</strong> installed.
Clone the repository and ensure ports 8090, 8080, 5432, 6379, 8123, 3000, and 3001 are
free.
</Callout>
<h2 id="step-1-clone">Step 1 Clone the repository</h2>
<CodeBlock
language="bash"
code={`git clone https://github.com/veylant/ia-gateway.git
cd ia-gateway`}
/>
<h2 id="step-2-configure">Step 2 Configure environment</h2>
<p>
Copy the sample environment file and add at least one LLM provider API key. For a minimal
setup, OpenAI is enough.
</p>
<CodeBlock
language="bash"
code={`cp .env.example .env
# Edit .env and set:
OPENAI_API_KEY=sk-...
# Optional:
ANTHROPIC_API_KEY=sk-ant-...`}
/>
<Callout type="tip" title="Development mode">
In <code>server.env=development</code> (the default), all external services degrade
gracefully. Keycloak is bypassed (mock JWT), PostgreSQL failures disable routing, ClickHouse
failures disable audit logs. This means you can start the proxy even if some services
haven't fully initialized yet.
</Callout>
<h2 id="step-3-start">Step 3 Start the stack</h2>
<CodeBlock
language="bash"
code={`make dev
# Or directly:
docker compose up --build`}
/>
<p>
This starts 9 services: PostgreSQL, Redis, ClickHouse, Keycloak, the Go proxy, PII
detection service, Prometheus, Grafana, and the React dashboard.
</p>
<p>
Wait for the proxy to print <code>server listening on :8090</code>. First startup takes
~2 minutes while Keycloak initializes and database migrations run.
</p>
<h2 id="step-4-verify">Step 4 Verify the stack</h2>
<CodeBlock
language="bash"
code={`# Health check
curl http://localhost:8090/healthz
# {"status":"ok","version":"1.0.0"}`}
/>
<div className="overflow-x-auto my-4">
<table className="w-full text-sm border rounded-lg overflow-hidden">
<thead>
<tr className="bg-muted/50 border-b">
<th className="text-left px-4 py-2.5 font-semibold">Service</th>
<th className="text-left px-4 py-2.5 font-semibold">URL</th>
<th className="text-left px-4 py-2.5 font-semibold">Credentials</th>
</tr>
</thead>
<tbody>
{[
{ service: "AI Proxy", url: "http://localhost:8090", creds: "—" },
{ service: "React Dashboard", url: "http://localhost:3000", creds: "dev mode (no auth)" },
{ service: "Keycloak Admin", url: "http://localhost:8080", creds: "admin / admin" },
{ service: "Grafana", url: "http://localhost:3001", creds: "admin / admin" },
{ service: "Prometheus", url: "http://localhost:9090", creds: "—" },
{ service: "API Docs", url: "http://localhost:8090/docs", creds: "—" },
{ service: "Playground", url: "http://localhost:8090/playground", creds: "—" },
].map((row) => (
<tr key={row.service} className="border-b last:border-0">
<td className="px-4 py-2.5 font-medium">{row.service}</td>
<td className="px-4 py-2.5">
<code className="text-xs">{row.url}</code>
</td>
<td className="px-4 py-2.5 text-muted-foreground text-xs">{row.creds}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 id="step-5-first-call">Step 5 Make your first AI call</h2>
<p>
In development mode, the proxy uses a mock JWT verifier. Pass any Bearer token and the
request will be authenticated as <code>admin@veylant.dev</code>.
</p>
<CodeBlock
language="bash"
code={`curl http://localhost:8090/v1/chat/completions \\
-H "Authorization: Bearer dev-token" \\
-H "Content-Type: application/json" \\
-d '{
"model": "gpt-4o",
"messages": [{"role": "user", "content": "Hello, Veylant!"}]
}'`}
/>
<p>Or use the OpenAI Python SDK with a changed base URL:</p>
<CodeBlock
language="python"
code={`from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8090/v1",
api_key="dev-token", # any string in dev mode
)
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Hello, Veylant!"}],
)
print(response.choices[0].message.content)`}
/>
<h2 id="step-6-dashboard">Step 6 Explore the dashboard</h2>
<p>
Open <code>http://localhost:3000</code> to see the React dashboard. In development mode,
you're automatically logged in as <code>Dev Admin</code>. You'll see:
</p>
<ul>
<li>
<strong>Overview</strong> request counts, costs, and tokens consumed
</li>
<li>
<strong>Playground IA</strong> test prompts with live PII detection visualization
</li>
<li>
<strong>Policies</strong> create and manage routing rules
</li>
<li>
<strong>Compliance</strong> GDPR Article 30 registry and AI Act questionnaire
</li>
</ul>
<Callout type="tip" title="Next: Configure a routing rule">
Try creating a routing rule in the dashboard that sends all requests from the{" "}
<code>legal</code> department to Anthropic instead of OpenAI. See{" "}
<a href="/docs/guides/routing">Routing Rules Engine</a> for the full guide.
</Callout>
<h2 id="stop-the-stack">Stop the stack</h2>
<CodeBlock language="bash" code={`make dev-down\n# Stops all containers and removes volumes`} />
</div>
);
}

View File

@ -0,0 +1,172 @@
import { Callout } from "../components/Callout";
import { Link } from "react-router-dom";
export function WhatIsVeylantPage() {
return (
<div>
<h1 id="what-is-veylant">What is Veylant IA?</h1>
<p>
<strong>Veylant IA</strong> is a B2B SaaS platform that acts as an intelligent proxy and
gateway for enterprise AI consumption. It sits between your organization's applications and
LLM providers (OpenAI, Anthropic, Azure, Mistral, Ollama), enforcing governance policies on
every request.
</p>
<Callout type="info" title="OpenAI-compatible API">
Veylant IA implements the OpenAI API format specifically{" "}
<code>/v1/chat/completions</code>. Your existing OpenAI SDK clients work without
modification; just change the base URL.
</Callout>
<h2 id="core-value-proposition">Core Value Proposition</h2>
<p>Four problems Veylant IA solves for enterprise IT and compliance teams:</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 my-4">
{[
{
title: "Shadow AI Prevention",
desc: "Every AI call flows through the proxy — no direct provider access possible. Full audit trail of who sent what to which model.",
},
{
title: "PII Anonymization",
desc: "3-layer detection (regex + spaCy NER + LLM validation) anonymizes sensitive data before it leaves your perimeter. <50ms latency.",
},
{
title: "GDPR & EU AI Act Compliance",
desc: "Built-in Article 30 processing registry, AI Act risk classification, DPIA templates, and subject access/erasure rights.",
},
{
title: "Cost Control",
desc: "Per-tenant and per-user token budgets, circuit breakers per provider, cost breakdown by department and model.",
},
].map((card) => (
<div key={card.title} className="rounded-lg border bg-card p-4">
<h3 className="font-semibold text-sm mb-1">{card.title}</h3>
<p className="text-sm text-muted-foreground leading-relaxed">{card.desc}</p>
</div>
))}
</div>
<h2 id="architecture">Architecture Overview</h2>
<p>
Veylant IA is a <strong>modular monolith</strong> (not microservices) with two distinct
runtimes:
</p>
<div className="my-4 rounded-lg border bg-zinc-950 text-zinc-100 p-4 font-mono text-xs leading-relaxed overflow-x-auto">
<pre>{`API Gateway (Traefik / Nginx)
Go Proxy [cmd/proxy] :8090
Auth middleware (OIDC/Keycloak JWT validation)
Rate limiter (token-bucket, per-tenant + per-user)
PII gRPC client PII Service :50051
Routing engine (PostgreSQL JSONB rules, in-memory cache)
Provider adapters (OpenAI / Anthropic / Azure / Mistral / Ollama)
Audit logger ClickHouse (async batch)
Admin REST API (/v1/admin/*)
gRPC <2ms
PII Detection Service [FastAPI + grpc.aio] :8091 / :50051
Layer 1: Regex (IBAN, email, phone, SSN, credit cards)
Layer 2: Presidio + spaCy NER (names, addresses, orgs)
Layer 3: LLM validation (ambiguous cases)
LLM Provider (OpenAI / Anthropic / Azure / Mistral / Ollama)
Data Layer:
PostgreSQL 16 config, users, policies, registry (RLS multi-tenancy)
ClickHouse append-only audit logs and analytics
Redis 7 sessions, rate limiting, PII pseudonymization mappings
Keycloak IAM, SSO, SAML 2.0 / OIDC federation`}</pre>
</div>
<h2 id="key-components">Key Components</h2>
<h3 id="go-proxy">Go Proxy</h3>
<p>
The core of Veylant IA. Written in Go 1.24, it handles all incoming AI requests, applies
governance policies, and routes them to the appropriate LLM provider. It exposes:
</p>
<ul>
<li>
<code>/v1/chat/completions</code> OpenAI-compatible inference endpoint
</li>
<li>
<code>/v1/pii/analyze</code> on-demand PII detection
</li>
<li>
<code>/v1/admin/*</code> governance API (policies, users, audit logs, compliance)
</li>
<li>
<code>/healthz</code>, <code>/docs</code>, <code>/playground</code>
</li>
</ul>
<h3 id="pii-service">PII Detection Service</h3>
<p>
A Python FastAPI + gRPC service running 3 detection layers in under 50ms. Anonymizes or
pseudonymizes PII in prompts before they reach the upstream LLM. Pseudonymized mappings are
stored in Redis (AES-256-GCM encrypted, TTL-based).
</p>
<h3 id="routing-engine">Routing Engine</h3>
<p>
Rules stored in PostgreSQL (JSONB conditions), cached in memory. Routes requests to
providers based on user role, department, model requested, sensitivity score, token
estimate, and more. First-match wins; lower priority number = evaluated first.
</p>
<h2 id="supported-providers">Supported LLM Providers</h2>
<div className="overflow-x-auto my-4">
<table className="w-full text-sm border rounded-lg overflow-hidden">
<thead>
<tr className="bg-muted/50 border-b">
<th className="text-left px-4 py-2.5 font-semibold">Provider</th>
<th className="text-left px-4 py-2.5 font-semibold">Models</th>
<th className="text-left px-4 py-2.5 font-semibold">Streaming</th>
<th className="text-left px-4 py-2.5 font-semibold">Status</th>
</tr>
</thead>
<tbody>
{[
{ provider: "OpenAI", models: "gpt-4o, gpt-4-turbo, gpt-3.5-turbo", stream: "Yes", status: "GA" },
{ provider: "Anthropic", models: "claude-3-5-sonnet, claude-3-opus", stream: "Yes", status: "GA" },
{ provider: "Azure OpenAI", models: "GPT-4 deployments", stream: "Yes", status: "GA" },
{ provider: "Mistral AI", models: "mistral-large, mistral-medium", stream: "Yes", status: "GA" },
{ provider: "Ollama", models: "llama3, mistral, codellama, ...", stream: "Yes", status: "GA" },
].map((row) => (
<tr key={row.provider} className="border-b last:border-0">
<td className="px-4 py-2.5 font-medium">{row.provider}</td>
<td className="px-4 py-2.5 text-muted-foreground font-mono text-xs">{row.models}</td>
<td className="px-4 py-2.5 text-green-600 dark:text-green-400">{row.stream}</td>
<td className="px-4 py-2.5">
<span className="text-xs bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300 px-2 py-0.5 rounded font-medium">
{row.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 id="next-steps">Next Steps</h2>
<ul>
<li>
<Link to="/docs/getting-started/quick-start">Quick Start</Link> run the full stack in 5
minutes
</li>
<li>
<Link to="/docs/getting-started/concepts">Key Concepts</Link> learn the core
abstractions
</li>
<li>
<Link to="/docs/api/chat-completions">API Reference</Link> integrate your first
application
</li>
</ul>
</div>
);
}

View File

@ -0,0 +1,118 @@
import { Callout } from "../components/Callout";
import { CodeBlock } from "../components/CodeBlock";
export function CircuitBreakerGuide() {
return (
<div>
<h1 id="circuit-breaker">Circuit Breaker & Failover</h1>
<p>
Veylant IA includes a per-provider circuit breaker that prevents cascading failures when an
LLM provider is degraded or unreachable.
</p>
<h2 id="states">Circuit Breaker States</h2>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 my-4">
{[
{
state: "Closed",
color: "border-green-400",
bg: "bg-green-50 dark:bg-green-950/30",
desc: "Normal operation. Requests are forwarded to the provider. Failures are counted.",
},
{
state: "Open",
color: "border-red-400",
bg: "bg-red-50 dark:bg-red-950/30",
desc: "Provider bypassed. All requests use the fallback chain. Stays open for open_ttl seconds.",
},
{
state: "Half-Open",
color: "border-amber-400",
bg: "bg-amber-50 dark:bg-amber-950/30",
desc: "Testing if provider has recovered. One probe request sent. Success → Closed; Failure → Open.",
},
].map((item) => (
<div key={item.state} className={`rounded-lg border-l-4 p-4 ${item.color} ${item.bg}`}>
<h3 className="font-semibold text-sm mb-2">{item.state}</h3>
<p className="text-sm text-muted-foreground leading-relaxed">{item.desc}</p>
</div>
))}
</div>
<h2 id="configuration">Configuration</h2>
<CodeBlock
language="yaml"
code={`circuit_breaker:
threshold: 5 # consecutive failures to open the breaker
open_ttl: 60s # how long to stay open before half-open probe`}
/>
<Callout type="tip" title="Per-provider isolation">
Each provider has an independent circuit breaker. A failing Azure deployment does not affect
OpenAI or Anthropic calls.
</Callout>
<h2 id="fallback">Fallback Chain</h2>
<p>
When the primary provider's circuit is open, the routing engine uses the{" "}
<code>fallback_providers</code> array from the matched routing rule:
</p>
<CodeBlock
language="json"
code={`{
"provider": "azure",
"fallback_providers": ["anthropic", "openai"],
"conditions": [{"field": "user.department", "operator": "eq", "value": "legal"}]
}
// If azure is open → try anthropic → if anthropic is open → try openai → if all fail → 503`}
/>
<h2 id="check-status">Checking Status</h2>
<CodeBlock
language="bash"
code={`curl http://localhost:8090/v1/admin/providers/status \\
-H "Authorization: Bearer $TOKEN"
# Response:
{
"data": [
{"provider": "openai", "state": "closed", "failures": 0, "last_failure": null},
{"provider": "anthropic", "state": "closed", "failures": 0, "last_failure": null},
{"provider": "azure", "state": "open", "failures": 5, "last_failure": "2026-01-15T14:30:00Z"},
{"provider": "mistral", "state": "half-open", "failures": 3, "last_failure": "2026-01-15T14:28:00Z"},
{"provider": "ollama", "state": "closed", "failures": 0, "last_failure": null}
]
}`}
/>
<h2 id="prometheus">Prometheus Alert</h2>
<p>
The <code>CircuitBreakerOpen</code> alert fires when any provider is in the open state:
</p>
<CodeBlock
language="yaml"
code={`- alert: CircuitBreakerOpen
expr: veylant_circuit_breaker_state > 0
for: 0m
labels:
severity: warning
annotations:
summary: "Provider {{ $labels.provider }} circuit breaker is open"
description: "The circuit breaker for {{ $labels.provider }} has opened after repeated failures."`}
/>
<h2 id="graceful-degradation">Development Mode Degradation</h2>
<Callout type="info">
In <code>server.env=development</code>, Veylant IA degrades gracefully if services are
unreachable:
<ul className="mt-2 space-y-1">
<li><strong>Keycloak unreachable</strong> MockVerifier (auth bypassed)</li>
<li><strong>PostgreSQL unreachable</strong> routing disabled, feature flags use in-memory defaults</li>
<li><strong>ClickHouse unreachable</strong> audit logging disabled</li>
<li><strong>PII service unreachable</strong> PII skipped if <code>fail_open=true</code></li>
</ul>
In <code>production</code> mode, any of the above causes a fatal startup error.
</Callout>
</div>
);
}

View File

@ -0,0 +1,154 @@
import { Callout } from "../components/Callout";
import { CodeBlock } from "../components/CodeBlock";
import { Link } from "react-router-dom";
export function ComplianceGuide() {
return (
<div>
<h1 id="compliance">GDPR & EU AI Act Compliance</h1>
<p>
Veylant IA includes a built-in compliance module for GDPR Article 30 record-keeping and EU
AI Act risk classification. It is designed to serve as your primary compliance tool for AI
deployments.
</p>
<h2 id="gdpr-art30">GDPR Article 30 Record of Processing Activities</h2>
<p>
Article 30 requires organizations to maintain a written record of all data processing
activities. For AI systems, this means documenting each use case where personal data may be
processed.
</p>
<h3 id="ropa-fields">Required ROPA Fields</h3>
<div className="overflow-x-auto my-4">
<table className="w-full text-sm border rounded-lg overflow-hidden">
<thead>
<tr className="bg-muted/50 border-b">
<th className="text-left px-4 py-2.5 font-semibold">Field</th>
<th className="text-left px-4 py-2.5 font-semibold">GDPR Requirement</th>
<th className="text-left px-4 py-2.5 font-semibold">Example</th>
</tr>
</thead>
<tbody>
{[
{ field: "use_case_name", req: "Name of the processing activity", ex: "Legal contract analysis" },
{ field: "purpose", req: "Art. 5(1)(b) — purpose limitation", ex: "Automated risk identification in supplier contracts" },
{ field: "legal_basis", req: "Art. 6 — lawfulness of processing", ex: "legitimate_interest" },
{ field: "data_categories", req: "Art. 30(1)(c) — categories of data subjects and data", ex: "name, email, financial" },
{ field: "retention_period", req: "Art. 5(1)(e) — storage limitation", ex: "3 years" },
{ field: "security_measures", req: "Art. 32 — security of processing", ex: "AES-256-GCM, PII anonymization" },
{ field: "controller_name", req: "Art. 30(1)(a) — controller identity", ex: "Acme Corp — dpo@acme.com" },
{ field: "processors", req: "Art. 30(1)(d) — recipients of data", ex: "Anthropic (via Veylant IA proxy)" },
].map((row) => (
<tr key={row.field} className="border-b last:border-0">
<td className="px-4 py-2.5 font-mono text-xs">{row.field}</td>
<td className="px-4 py-2.5 text-xs text-muted-foreground">{row.req}</td>
<td className="px-4 py-2.5 text-xs text-muted-foreground italic">{row.ex}</td>
</tr>
))}
</tbody>
</table>
</div>
<h3 id="legal-bases">Legal Bases</h3>
<ul>
<li><code>consent</code> User has given explicit consent (Art. 6(1)(a))</li>
<li><code>contract</code> Processing necessary for a contract (Art. 6(1)(b))</li>
<li><code>legal_obligation</code> Required by law (Art. 6(1)(c))</li>
<li><code>vital_interests</code> Protecting someone's life (Art. 6(1)(d))</li>
<li><code>public_task</code> Public interest or official authority (Art. 6(1)(e))</li>
<li><code>legitimate_interest</code> Legitimate interests of the controller (Art. 6(1)(f))</li>
</ul>
<h2 id="ai-act">EU AI Act Risk Classification</h2>
<p>
The EU AI Act (effective August 2024, full enforcement from August 2026) classifies AI
systems into four risk categories.
</p>
<div className="space-y-3 my-4">
{[
{
level: "Forbidden",
color: "border-red-400 bg-red-50 dark:bg-red-950/30",
badge: "bg-red-100 dark:bg-red-900/40 text-red-700 dark:text-red-300",
score: "Score 5",
desc: "Cannot be deployed. Examples: social scoring systems, real-time biometric surveillance in public spaces, AI that exploits vulnerable groups.",
},
{
level: "High Risk",
color: "border-orange-400 bg-orange-50 dark:bg-orange-950/30",
badge: "bg-orange-100 dark:bg-orange-900/40 text-orange-700 dark:text-orange-300",
score: "Score 34",
desc: "Requires conformity assessment before deployment. DPIA mandatory. Examples: AI in hiring, credit scoring, education grading, critical infrastructure.",
},
{
level: "Limited Risk",
color: "border-amber-400 bg-amber-50 dark:bg-amber-950/30",
badge: "bg-amber-100 dark:bg-amber-900/40 text-amber-700 dark:text-amber-300",
score: "Score 12",
desc: "Transparency obligations apply. Users must be informed they interact with AI. Examples: chatbots, recommendation systems, customer service AI.",
},
{
level: "Minimal Risk",
color: "border-green-400 bg-green-50 dark:bg-green-950/30",
badge: "bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300",
score: "Score 0",
desc: "Minimal or no risk. Voluntary code of conduct recommended. Examples: spam filters, AI-powered search, content recommendation.",
},
].map((item) => (
<div key={item.level} className={`flex items-start gap-3 rounded-lg border-l-4 p-4 ${item.color}`}>
<div className="min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className={`text-xs font-bold px-2 py-0.5 rounded ${item.badge}`}>
{item.level}
</span>
<span className="text-xs text-muted-foreground">{item.score}</span>
</div>
<p className="text-sm text-muted-foreground leading-relaxed">{item.desc}</p>
</div>
</div>
))}
</div>
<h2 id="dpia">Data Protection Impact Assessment (DPIA)</h2>
<p>
A DPIA is mandatory under GDPR Art. 35 for high-risk processing activities. High-risk AI
systems under the AI Act also trigger DPIA requirements. Veylant IA generates DPIA template
documents from the Admin Compliance Reports tab.
</p>
<h2 id="reports">Compliance Reports</h2>
<p>Available report formats via the API:</p>
<CodeBlock
language="bash"
code={`# GDPR Article 30 registry — PDF
GET /v1/admin/compliance/reports/art30.pdf
# GDPR Article 30 registry JSON export
GET /v1/admin/compliance/reports/art30.json
# AI Act risk classification report PDF
GET /v1/admin/compliance/reports/ai-act.pdf
# DPIA template PDF
GET /v1/admin/compliance/reports/dpia/{entry_id}.pdf
# Audit log export CSV
GET /v1/admin/logs/export?from=2026-01-01T00:00:00Z&to=2026-01-31T23:59:59Z`}
/>
<Callout type="tip" title="Audit-of-the-audit">
All accesses to compliance reports and audit logs are themselves logged. This satisfies
data protection authority requirements for meta-logging of sensitive data access.
</Callout>
<h2 id="next-steps">Working with Compliance</h2>
<p>
See the <Link to="/docs/api/admin/compliance">Admin Compliance API</Link> for full
endpoint documentation, or navigate to{" "}
<strong>Dashboard Compliance</strong> to use the visual interface.
</p>
</div>
);
}

View File

@ -0,0 +1,162 @@
import { Callout } from "../components/Callout";
import { CodeBlock } from "../components/CodeBlock";
export function MonitoringGuide() {
return (
<div>
<h1 id="monitoring">Monitoring & Alerting</h1>
<p>
Veylant IA exposes Prometheus metrics and ships pre-built Grafana dashboards and
Alertmanager rules for production operations.
</p>
<h2 id="prometheus">Prometheus Metrics</h2>
<p>
The proxy exposes metrics at <code>GET /metrics</code> (Prometheus text format, scraped
every 15 seconds).
</p>
<div className="overflow-x-auto my-4">
<table className="w-full text-sm border rounded-lg overflow-hidden">
<thead>
<tr className="bg-muted/50 border-b">
<th className="text-left px-4 py-2.5 font-semibold">Metric</th>
<th className="text-left px-4 py-2.5 font-semibold">Type</th>
<th className="text-left px-4 py-2.5 font-semibold">Description</th>
</tr>
</thead>
<tbody>
{[
{ metric: "veylant_requests_total", type: "Counter", desc: "Total requests by provider, model, status_code, tenant_id" },
{ metric: "veylant_request_duration_seconds", type: "Histogram", desc: "End-to-end request latency (p50, p95, p99)" },
{ metric: "veylant_pii_entities_total", type: "Counter", desc: "PII entities detected by type" },
{ metric: "veylant_tokens_total", type: "Counter", desc: "Tokens consumed by provider and model" },
{ metric: "veylant_cost_usd_total", type: "Counter", desc: "USD cost by provider and model" },
{ metric: "veylant_circuit_breaker_state", type: "Gauge", desc: "Circuit breaker state (0=closed, 1=open, 2=half-open) by provider" },
{ metric: "veylant_rate_limit_rejections_total", type: "Counter", desc: "Rate limit rejections by tenant_id" },
{ metric: "veylant_pii_pipeline_duration_seconds", type: "Histogram", desc: "PII detection pipeline latency" },
{ metric: "veylant_db_connections", type: "Gauge", desc: "PostgreSQL connection pool (open, idle, in-use)" },
].map((row) => (
<tr key={row.metric} className="border-b last:border-0">
<td className="px-4 py-2.5 font-mono text-xs">{row.metric}</td>
<td className="px-4 py-2.5 text-xs">
<span className={`px-1.5 py-0.5 rounded text-xs font-medium ${row.type === "Counter" ? "bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300" : row.type === "Gauge" ? "bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300" : "bg-purple-100 dark:bg-purple-900/40 text-purple-700 dark:text-purple-300"}`}>
{row.type}
</span>
</td>
<td className="px-4 py-2.5 text-xs text-muted-foreground">{row.desc}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 id="grafana-dashboards">Grafana Dashboards</h2>
<p>
Two dashboards are pre-provisioned in Grafana (<code>http://localhost:3001</code>,
admin/admin):
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 my-4">
<div className="rounded-lg border bg-card p-4">
<h3 className="font-semibold text-sm mb-2">Proxy Overview</h3>
<ul className="text-sm text-muted-foreground space-y-1">
<li>Request rate (req/min)</li>
<li>Error rate (%)</li>
<li>Latency p50/p95/p99</li>
<li>Token consumption over time</li>
<li>PII entities by type</li>
<li>Provider distribution (pie chart)</li>
<li>DB connection pool</li>
</ul>
</div>
<div className="rounded-lg border bg-card p-4">
<h3 className="font-semibold text-sm mb-2">Production SLO</h3>
<ul className="text-sm text-muted-foreground space-y-1">
<li>SLO: 99.5% availability</li>
<li>Error budget (burn rate)</li>
<li>p95 latency SLO (&lt;500ms)</li>
<li>Error budget remaining (%)</li>
<li>Active incidents timeline</li>
</ul>
</div>
</div>
<h2 id="alerts">Prometheus Alerts</h2>
<p>
7 alert rules are configured in <code>deploy/prometheus/rules.yml</code> and integrated
with Alertmanager:
</p>
<div className="space-y-3 my-4">
{[
{ name: "HighLatency", severity: "warning", desc: "p95 latency > 500ms for 2 minutes", expr: "histogram_quantile(0.95, veylant_request_duration_seconds_bucket) > 0.5" },
{ name: "HighErrorRate", severity: "critical", desc: "Error rate > 5% for 1 minute", expr: "rate(veylant_requests_total{status_code=~'5..'}[1m]) / rate(veylant_requests_total[1m]) > 0.05" },
{ name: "CircuitBreakerOpen", severity: "warning", desc: "Any provider circuit breaker is open", expr: "veylant_circuit_breaker_state > 0" },
{ name: "ProxyDown", severity: "critical", desc: "Proxy health check failing", expr: "up{job='veylant-proxy'} == 0" },
{ name: "CertExpiry", severity: "warning", desc: "TLS certificate expires in < 7 days", expr: "probe_ssl_earliest_cert_expiry - time() < 7 * 86400" },
{ name: "DBConnPoolExhausted", severity: "warning", desc: "DB connection pool > 90% utilized", expr: "veylant_db_connections{state='in_use'} / (veylant_db_connections{state='in_use'} + veylant_db_connections{state='idle'}) > 0.9" },
{ name: "PIIAnomaly", severity: "warning", desc: "PII detection rate spiked > 3x baseline", expr: "rate(veylant_pii_entities_total[5m]) > 3 * avg_over_time(rate(veylant_pii_entities_total[5m])[1h:])" },
].map((alert) => (
<div key={alert.name} className="rounded-lg border bg-card p-4">
<div className="flex items-center gap-2 mb-1">
<span className="font-mono text-sm font-semibold">{alert.name}</span>
<span className={`text-xs px-1.5 py-0.5 rounded font-medium ${alert.severity === "critical" ? "bg-red-100 dark:bg-red-900/40 text-red-700 dark:text-red-300" : "bg-amber-100 dark:bg-amber-900/40 text-amber-700 dark:text-amber-300"}`}>
{alert.severity}
</span>
</div>
<p className="text-sm text-muted-foreground mb-2">{alert.desc}</p>
<code className="text-xs text-muted-foreground bg-muted px-2 py-1 rounded block overflow-x-auto">
{alert.expr}
</code>
</div>
))}
</div>
<h2 id="alertmanager">Alertmanager Routing</h2>
<p>Alerts are routed based on severity:</p>
<ul>
<li>
<strong>Critical</strong> PagerDuty (immediate on-call notification)
</li>
<li>
<strong>Warning</strong> Slack (#veylant-alerts channel)
</li>
</ul>
<CodeBlock
language="yaml"
code={`# deploy/alertmanager/alertmanager.yml
route:
group_by: ['alertname', 'severity']
group_wait: 30s
group_interval: 5m
repeat_interval: 12h
receiver: slack-warnings
routes:
- match:
severity: critical
receiver: pagerduty-critical
receivers:
- name: pagerduty-critical
pagerduty_configs:
- routing_key: PAGERDUTY_ROUTING_KEY
- name: slack-warnings
slack_configs:
- channel: '#veylant-alerts'`}
/>
<h2 id="slo">SLO Definition</h2>
<Callout type="info" title="Production SLO — v1.0.0">
<strong>Availability:</strong> 99.5% (allows ~3.6 hours downtime/month)
<br />
<strong>Latency:</strong> p95 &lt; 500ms for chat completion requests
<br />
<strong>PII pipeline:</strong> p99 &lt; 50ms
<br />
<strong>Error rate:</strong> &lt; 0.5% (5xx responses)
</Callout>
</div>
);
}

View File

@ -0,0 +1,147 @@
import { CodeBlock } from "../components/CodeBlock";
import { Callout } from "../components/Callout";
export function PiiGuide() {
return (
<div>
<h1 id="pii-detection">PII Detection & Anonymization</h1>
<p>
Veylant IA intercepts all AI prompts and runs a 3-layer PII detection pipeline before
forwarding to the LLM. The entire pipeline must complete in <strong>under 50ms</strong>.
</p>
<h2 id="pipeline">Detection Pipeline</h2>
<div className="my-4 rounded-lg border bg-zinc-950 text-zinc-100 p-4 font-mono text-xs leading-relaxed overflow-x-auto">
<pre>{`Incoming prompt text
Layer 1: Regex (< 1ms)
IBAN: /[A-Z]{2}\\d{2}[A-Z0-9]{11,30}/
Email: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/
Phone: /(?:\\+33|0)[1-9](?:[\\s.-]?\\d{2}){4}/
SSN: /\\d{1}\\s?\\d{2}\\s?\\d{2}\\s?\\d{2}\\s?\\d{3}\\s?\\d{3}\\s?\\d{2}/
Credit card: /\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}/
Layer 2: Presidio + spaCy NER (1540ms)
PERSON: "John Smith", "Marie Dupont"
LOCATION: "Paris", "75001", "rue de Rivoli"
ORGANIZATION: "Acme Corp", "Banque de France"
DATE_TIME: "12/03/1985"
NRP: nationality, religion, political group
Layer 3: LLM validation (optional, V1.1)
For ambiguous cases scored 0.50.8
Sends short validation prompts to a fast LLM
Corrects false positives and false negatives
Anonymized prompt forwarded to LLM provider`}</pre>
</div>
<h2 id="anonymization">Anonymization vs Pseudonymization</h2>
<p>Veylant IA supports two modes:</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 my-4">
<div className="rounded-lg border bg-card p-4">
<h3 className="font-semibold text-sm mb-2">Anonymization (default)</h3>
<p className="text-sm text-muted-foreground">
PII is replaced with a type label. No mapping stored. Irreversible.
</p>
<CodeBlock
language="text"
code={`Input: "Call John at +33 6 12 34 56 78"
Output: "Call [PERSON] at [PHONE_NUMBER]"`}
/>
</div>
<div className="rounded-lg border bg-card p-4">
<h3 className="font-semibold text-sm mb-2">Pseudonymization</h3>
<p className="text-sm text-muted-foreground">
PII is replaced with a synthetic token. Mapping stored in Redis (encrypted, TTL-based).
Reversible.
</p>
<CodeBlock
language="text"
code={`Input: "Call John at +33 6 12 34 56 78"
Output: "Call PERSON_001 at PHONE_001"
Mapping: PERSON_001 "John" (Redis, AES-256-GCM, TTL=3600s)`}
/>
</div>
</div>
<h2 id="redis-mapping">Redis Pseudonymization Mapping</h2>
<p>
When pseudonymization is enabled, the PII service stores mappings in Redis using the
following key structure:
</p>
<CodeBlock
language="text"
code={`Key: pii:{tenant_id}:{user_id}:{session_id}:{token}
Value: AES-256-GCM encrypted JSON {"original": "John Smith", "type": "PERSON"}
TTL: 3600 seconds (configurable)`}
/>
<h2 id="latency">Latency Budget</h2>
<div className="overflow-x-auto my-4">
<table className="w-full text-sm border rounded-lg overflow-hidden">
<thead>
<tr className="bg-muted/50 border-b">
<th className="text-left px-4 py-2.5 font-semibold">Layer</th>
<th className="text-left px-4 py-2.5 font-semibold">Typical latency</th>
<th className="text-left px-4 py-2.5 font-semibold">P99 latency</th>
</tr>
</thead>
<tbody>
{[
{ layer: "Layer 1: Regex", typ: "< 1ms", p99: "< 2ms" },
{ layer: "Layer 2: NER (spaCy)", typ: "1530ms", p99: "45ms" },
{ layer: "Redis write (pseudonymization)", typ: "< 2ms", p99: "5ms" },
{ layer: "Total pipeline (gRPC)", typ: "2035ms", p99: "50ms" },
].map((row) => (
<tr key={row.layer} className="border-b last:border-0">
<td className="px-4 py-2.5 text-sm">{row.layer}</td>
<td className="px-4 py-2.5 text-sm text-muted-foreground">{row.typ}</td>
<td className="px-4 py-2.5 text-sm text-muted-foreground">{row.p99}</td>
</tr>
))}
</tbody>
</table>
</div>
<Callout type="warning" title="Fail-open behavior">
If the PII service is unreachable and <code>pii.fail_open=true</code> (default in
development), requests are forwarded without anonymization and a warning is logged. In
production, set <code>pii.fail_open=false</code> to return 503 instead.
</Callout>
<h2 id="disabling">Disabling PII per Request</h2>
<p>
There is no per-request PII bypass. PII is controlled globally via the{" "}
<code>pii_detection</code> feature flag or the <code>pii.enabled</code> config key.
Fine-grained per-rule PII control is planned for V1.1.
</p>
<h2 id="testing">Testing PII Detection</h2>
<CodeBlock
language="bash"
code={`# Use the analyze endpoint directly
curl http://localhost:8090/v1/pii/analyze \\
-H "Authorization: Bearer $TOKEN" \\
-H "Content-Type: application/json" \\
-d '{
"text": "Contact Marie Dupont at marie.dupont@orange.fr or +33 1 23 45 67 89",
"anonymize": true
}'`}
/>
<CodeBlock
language="bash"
code={`# Run Python unit tests for the PII service
pytest services/pii/tests/test_regex.py
pytest services/pii/tests/test_pipeline.py
pytest services/pii/tests/test_pseudo.py`}
/>
</div>
);
}

View File

@ -0,0 +1,126 @@
import { Callout } from "../components/Callout";
import { CodeBlock } from "../components/CodeBlock";
export function RbacGuide() {
return (
<div>
<h1 id="rbac">RBAC & Permissions</h1>
<p>
Veylant IA enforces Role-Based Access Control on every request. Roles are embedded in the
Keycloak JWT and cannot be elevated at runtime.
</p>
<h2 id="roles">Roles</h2>
<div className="space-y-4 my-4">
{[
{
role: "admin",
color: "bg-red-100 dark:bg-red-900/40 text-red-700 dark:text-red-300",
description: "Full access. Can manage policies, users, providers, feature flags, and read all compliance/audit data. Has unrestricted model access.",
},
{
role: "manager",
color: "bg-amber-100 dark:bg-amber-900/40 text-amber-700 dark:text-amber-300",
description: "Read-write access to routing policies and users. Can run AI inference with any model. Cannot manage feature flags or access compliance reports.",
},
{
role: "user",
color: "bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300",
description: "Inference only. Restricted to the model list in rbac.user_allowed_models (default: gpt-4o-mini, mistral-medium). No admin API access.",
},
{
role: "auditor",
color: "bg-purple-100 dark:bg-purple-900/40 text-purple-700 dark:text-purple-300",
description: "Read-only access to audit logs and compliance data. Cannot call /v1/chat/completions. Intended for compliance officers and DPOs.",
},
].map((item) => (
<div key={item.role} className="rounded-lg border bg-card p-4 flex items-start gap-3">
<span className={`text-xs font-bold px-2 py-0.5 rounded font-mono shrink-0 ${item.color}`}>
{item.role}
</span>
<p className="text-sm text-muted-foreground leading-relaxed">{item.description}</p>
</div>
))}
</div>
<h2 id="permission-matrix">Permission Matrix</h2>
<div className="overflow-x-auto my-4">
<table className="w-full text-sm border rounded-lg overflow-hidden">
<thead>
<tr className="bg-muted/50 border-b">
<th className="text-left px-4 py-2.5 font-semibold">Endpoint</th>
<th className="text-center px-3 py-2.5 font-semibold">admin</th>
<th className="text-center px-3 py-2.5 font-semibold">manager</th>
<th className="text-center px-3 py-2.5 font-semibold">user</th>
<th className="text-center px-3 py-2.5 font-semibold">auditor</th>
</tr>
</thead>
<tbody>
{[
{ ep: "POST /v1/chat/completions", admin: "✓", manager: "✓", user: "✓ (limited models)", auditor: "✗" },
{ ep: "POST /v1/pii/analyze", admin: "✓", manager: "✓", user: "✓", auditor: "✓" },
{ ep: "GET /v1/admin/policies", admin: "✓", manager: "✓", user: "✗", auditor: "✗" },
{ ep: "POST/PUT/DELETE /v1/admin/policies", admin: "✓", manager: "✓", user: "✗", auditor: "✗" },
{ ep: "GET/POST/PUT /v1/admin/users", admin: "✓", manager: "read only", user: "✗", auditor: "✗" },
{ ep: "GET /v1/admin/logs", admin: "✓", manager: "✓", user: "✗", auditor: "✓" },
{ ep: "GET /v1/admin/costs", admin: "✓", manager: "✓", user: "✗", auditor: "✓" },
{ ep: "GET /v1/admin/compliance/*", admin: "✓", manager: "✗", user: "✗", auditor: "✓" },
{ ep: "POST /v1/admin/compliance/*", admin: "✓", manager: "✗", user: "✗", auditor: "✗" },
{ ep: "GET/PUT/DELETE /v1/admin/flags", admin: "✓", manager: "✗", user: "✗", auditor: "✗" },
{ ep: "GET /v1/admin/providers/status", admin: "✓", manager: "✓", user: "✗", auditor: "✗" },
].map((row) => (
<tr key={row.ep} className="border-b last:border-0">
<td className="px-4 py-2.5 font-mono text-xs">{row.ep}</td>
{[row.admin, row.manager, row.user, row.auditor].map((v, i) => (
<td key={i} className={`px-3 py-2.5 text-xs text-center ${v === "✗" ? "text-muted-foreground" : v === "✓" ? "text-green-600 dark:text-green-400 font-medium" : "text-amber-600 dark:text-amber-400"}`}>
{v}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
<h2 id="model-restrictions">Model Restrictions</h2>
<p>
Users with the <code>user</code> role can only access models listed in{" "}
<code>rbac.user_allowed_models</code>. Requests to other models are rejected with 403:
</p>
<CodeBlock
language="yaml"
code={`# config.yaml
rbac:
user_allowed_models:
- "gpt-4o-mini"
- "mistral-medium-latest"
- "claude-3-haiku-20240307"`}
/>
<CodeBlock
language="json"
code={`// 403 response when user requests gpt-4o:
{
"error": {
"type": "permission_error",
"message": "role 'user' does not have access to model 'gpt-4o'. Allowed: gpt-4o-mini, mistral-medium-latest, claude-3-haiku-20240307",
"code": "permission_denied"
}
}`}
/>
<Callout type="tip" title="Admin and manager bypass model restrictions">
The <code>admin</code> and <code>manager</code> roles have unrestricted model access {" "}
<code>user_allowed_models</code> does not apply to them.
</Callout>
<h2 id="keycloak-setup">Setting Up Roles in Keycloak</h2>
<p>Assign roles to users in Keycloak:</p>
<ol>
<li>Log in to Keycloak Admin Console (http://localhost:8080, admin/admin)</li>
<li>Go to <strong>Realm: veylant</strong> <strong>Users</strong></li>
<li>Select a user <strong>Role Mappings</strong> <strong>Realm Roles</strong></li>
<li>Assign one of: <code>admin</code>, <code>manager</code>, <code>user</code>, <code>auditor</code></li>
</ol>
</div>
);
}

View File

@ -0,0 +1,146 @@
import { CodeBlock } from "../components/CodeBlock";
import { Callout } from "../components/Callout";
export function RoutingGuide() {
return (
<div>
<h1 id="routing-rules">Routing Rules Engine</h1>
<p>
The routing engine matches incoming AI requests against a prioritized list of rules and
dispatches them to the appropriate LLM provider. Rules are stored in PostgreSQL (JSONB
conditions) and cached in memory.
</p>
<h2 id="evaluation-order">Evaluation Order</h2>
<ul>
<li>Rules are sorted <strong>ascending by priority</strong> (lower number = evaluated first)</li>
<li><strong>First match wins</strong> once a rule matches, evaluation stops</li>
<li>All conditions within a rule are <strong>AND-joined</strong></li>
<li>An empty <code>conditions</code> array is a <strong>catch-all</strong> that matches everything</li>
</ul>
<Callout type="tip" title="Priority design">
Use gaps between priorities (10, 20, 30, ...) so you can insert rules later without
renumbering. The catch-all rule should have the highest priority number (e.g. 999).
</Callout>
<h2 id="condition-fields">Condition Fields & Operators</h2>
<div className="overflow-x-auto my-4">
<table className="w-full text-sm border rounded-lg overflow-hidden">
<thead>
<tr className="bg-muted/50 border-b">
<th className="text-left px-4 py-2.5 font-semibold">Field</th>
<th className="text-left px-4 py-2.5 font-semibold">Supported Operators</th>
<th className="text-left px-4 py-2.5 font-semibold">Value Type</th>
</tr>
</thead>
<tbody>
{[
{ field: "user.role", ops: "eq, neq, in, nin", type: "string" },
{ field: "user.department", ops: "eq, neq, in, nin, contains", type: "string" },
{ field: "request.model", ops: "eq, neq, in, nin", type: "string (model ID)" },
{ field: "request.sensitivity", ops: "eq, neq, gte, lte", type: "low | medium | high" },
{ field: "request.use_case", ops: "eq, neq, contains, matches", type: "string" },
{ field: "request.token_estimate", ops: "gte, lte", type: "integer (token count)" },
].map((row) => (
<tr key={row.field} className="border-b last:border-0">
<td className="px-4 py-2.5 font-mono text-xs">{row.field}</td>
<td className="px-4 py-2.5 text-xs text-muted-foreground">{row.ops}</td>
<td className="px-4 py-2.5 text-xs text-muted-foreground">{row.type}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 id="examples">Example Rule Configurations</h2>
<h3 id="example-department">Route legal department to Anthropic</h3>
<CodeBlock
language="json"
code={`{
"name": "Legal → Claude",
"priority": 10,
"provider": "anthropic",
"target_model": "claude-3-5-sonnet-20241022",
"fallback_providers": ["openai"],
"conditions": [
{"field": "user.department", "operator": "eq", "value": "legal"}
]
}`}
/>
<h3 id="example-cost-optimize">Cost-optimize small requests</h3>
<CodeBlock
language="json"
code={`{
"name": "Small requests → GPT-4o-mini",
"priority": 20,
"provider": "openai",
"target_model": "gpt-4o-mini",
"conditions": [
{"field": "request.token_estimate", "operator": "lte", "value": "1000"},
{"field": "request.sensitivity", "operator": "eq", "value": "low"}
]
}`}
/>
<h3 id="example-multi-condition">HR department, high-sensitivity requests</h3>
<CodeBlock
language="json"
code={`{
"name": "HR high-sensitivity → Azure GPT-4",
"priority": 15,
"provider": "azure",
"fallback_providers": ["anthropic", "openai"],
"conditions": [
{"field": "user.department", "operator": "eq", "value": "hr"},
{"field": "request.sensitivity", "operator": "eq", "value": "high"}
]
}`}
/>
<h3 id="example-catchall">Catch-all fallback</h3>
<CodeBlock
language="json"
code={`{
"name": "Default → OpenAI GPT-4o",
"priority": 999,
"provider": "openai",
"target_model": "gpt-4o",
"fallback_providers": ["mistral"],
"conditions": []
}`}
/>
<h2 id="fallback-chain">Fallback Chain</h2>
<p>
The <code>fallback_providers</code> array defines ordered fallbacks. The routing engine
tries providers in order:
</p>
<ol>
<li>Check if primary provider's circuit breaker is open skip if open</li>
<li>Try primary provider if error, try next fallback</li>
<li>Continue through the fallback chain</li>
<li>If all providers fail return 503</li>
</ol>
<h2 id="caching">Rule Caching</h2>
<p>
Rules are loaded from PostgreSQL and cached in memory. The cache is invalidated when a
rule is created, updated, or deleted via the admin API. You can also force a reload:
</p>
<CodeBlock
language="bash"
code={`# Restart the proxy to force rule cache reload
# (or wait for the TTL refresh default 60s)
docker compose restart proxy`}
/>
<Callout type="info" title="Rule cache TTL">
The in-memory rule cache refreshes every 60 seconds from PostgreSQL. Admin API changes
(create/update/delete) invalidate the cache immediately via a shared channel.
</Callout>
</div>
);
}

View File

@ -0,0 +1,152 @@
import { CodeBlock } from "../components/CodeBlock";
import { Callout } from "../components/Callout";
import { ParamTable } from "../components/ParamTable";
export function ConfigurationPage() {
return (
<div>
<h1 id="configuration">Configuration Reference</h1>
<p>
Veylant IA is configured via <code>config.yaml</code> at the repository root. Any key can
be overridden via an environment variable using the <code>VEYLANT_</code> prefix and
replacing <code>.</code> with <code>_</code>.
</p>
<Callout type="info" title="Environment variable override">
<code>server.port: 9000</code> <code>VEYLANT_SERVER_PORT=9000</code>
<br />
<code>pii.timeout_ms: 200</code> <code>VEYLANT_PII_TIMEOUT_MS=200</code>
</Callout>
<h2 id="full-config">Full config.yaml</h2>
<CodeBlock
language="yaml"
code={`server:
port: 8090
env: development # development | production
read_timeout: 30s
write_timeout: 60s
allowed_origins:
- "http://localhost:3000"
database:
url: "postgres://veylant:veylant@localhost:5432/veylant?sslmode=disable"
max_open_conns: 25
max_idle_conns: 5
redis:
url: "redis://localhost:6379"
db: 0
clickhouse:
dsn: "clickhouse://localhost:9000/veylant?dial_timeout=5s"
keycloak:
base_url: "http://localhost:8080"
realm: "veylant"
client_id: "veylant-proxy"
pii:
enabled: true
grpc_addr: "localhost:50051"
timeout_ms: 100
fail_open: true # if true, continue on PII service failure
providers:
openai:
api_key: "" # or VEYLANT_PROVIDERS_OPENAI_API_KEY
base_url: "https://api.openai.com/v1"
anthropic:
api_key: ""
base_url: "https://api.anthropic.com"
azure:
api_key: ""
endpoint: ""
api_version: "2024-02-01"
mistral:
api_key: ""
base_url: "https://api.mistral.ai/v1"
ollama:
base_url: "http://localhost:11434"
rbac:
user_allowed_models:
- "gpt-4o-mini"
- "mistral-medium"
# admin and manager have unrestricted model access
circuit_breaker:
threshold: 5 # failures before opening
open_ttl: 60s # how long to stay open
rate_limit:
default_rpm: 1000 # requests per minute per tenant
default_tpm: 100000 # tokens per minute per tenant
metrics:
enabled: true
path: "/metrics"
audit:
batch_size: 100
flush_interval: 5s
crypto:
key: "" # 32-byte AES key (base64), REQUIRED in production
vault:
enabled: false
addr: "http://localhost:8200"
token: ""`}
/>
<h2 id="server">Server Settings</h2>
<ParamTable
title="server.*"
params={[
{ name: "server.port", type: "int", required: false, default: "8090", description: "HTTP server port" },
{ name: "server.env", type: "string", required: false, default: "development", description: "Environment: development | production. In production, all service failures are fatal." },
{ name: "server.allowed_origins", type: "[]string", required: false, default: '["http://localhost:3000"]', description: "CORS allowed origins for /v1/* routes" },
]}
/>
<h2 id="pii-config">PII Settings</h2>
<ParamTable
title="pii.*"
params={[
{ name: "pii.enabled", type: "bool", required: false, default: "true", description: "Enable PII detection. When false, prompts are forwarded as-is." },
{ name: "pii.grpc_addr", type: "string", required: false, default: "localhost:50051", description: "gRPC address of the PII service" },
{ name: "pii.timeout_ms", type: "int", required: false, default: "100", description: "PII detection timeout in milliseconds (latency budget: <50ms)" },
{ name: "pii.fail_open", type: "bool", required: false, default: "true", description: "If true, continue on PII failure (request proceeds unredacted). If false, return 503." },
]}
/>
<h2 id="production-checklist">Production Checklist</h2>
<Callout type="warning" title="Required in production">
When <code>server.env=production</code>, these values must be set or the proxy fails to
start:
</Callout>
<ul>
<li>
<code>crypto.key</code> 32-byte AES-256 key (base64 encoded)
</li>
<li>
<code>database.url</code> PostgreSQL DSN with TLS
</li>
<li>
<code>keycloak.base_url</code> reachable Keycloak instance
</li>
<li>At least one provider API key</li>
</ul>
<CodeBlock
language="bash"
code={`# Generate a secure AES-256 key
openssl rand -base64 32
# Set in environment (never commit to git)
export VEYLANT_CRYPTO_KEY="$(openssl rand -base64 32)"`}
/>
</div>
);
}

View File

@ -0,0 +1,133 @@
import { CodeBlock } from "../components/CodeBlock";
import { Callout } from "../components/Callout";
export function DockerComposePage() {
return (
<div>
<h1 id="docker-compose">Docker Compose Setup</h1>
<p>
The recommended way to run Veylant IA locally or in a single-server staging environment is
Docker Compose. The full stack is defined in <code>docker-compose.yml</code> at the
repository root.
</p>
<h2 id="services">Services</h2>
<div className="overflow-x-auto my-4">
<table className="w-full text-sm border rounded-lg overflow-hidden">
<thead>
<tr className="bg-muted/50 border-b">
<th className="text-left px-4 py-2.5 font-semibold">Service</th>
<th className="text-left px-4 py-2.5 font-semibold">Image</th>
<th className="text-left px-4 py-2.5 font-semibold">Port</th>
<th className="text-left px-4 py-2.5 font-semibold">Purpose</th>
</tr>
</thead>
<tbody>
{[
{ service: "proxy", image: "Custom (Go, distroless)", port: "8090", purpose: "Main AI gateway" },
{ service: "pii", image: "Custom (Python FastAPI)", port: "8091 / 50051", purpose: "PII detection" },
{ service: "postgres", image: "postgres:16-alpine", port: "5432", purpose: "Config, users, policies" },
{ service: "redis", image: "redis:7-alpine", port: "6379", purpose: "Sessions, rate limits, PII maps" },
{ service: "clickhouse", image: "clickhouse:24.3-alpine", port: "8123 / 9000", purpose: "Audit logs & analytics" },
{ service: "keycloak", image: "keycloak:24.0", port: "8080", purpose: "IAM & SSO" },
{ service: "prometheus", image: "prom/prometheus:v2.53.0", port: "9090", purpose: "Metrics scraper" },
{ service: "grafana", image: "grafana:11.3.0", port: "3001", purpose: "Dashboards" },
{ service: "web", image: "node:20-alpine", port: "3000", purpose: "React dashboard" },
].map((row) => (
<tr key={row.service} className="border-b last:border-0">
<td className="px-4 py-2.5 font-mono text-xs font-medium">{row.service}</td>
<td className="px-4 py-2.5 text-muted-foreground text-xs">{row.image}</td>
<td className="px-4 py-2.5 font-mono text-xs">{row.port}</td>
<td className="px-4 py-2.5 text-muted-foreground text-xs">{row.purpose}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 id="commands">Make Commands</h2>
<CodeBlock
language="bash"
code={`# Start the full stack (build images + run)
make dev
# Tail logs from all services
make dev-logs
# Stop all containers and remove volumes
make dev-down
# Check proxy health
make health
# Open Swagger UI in browser (proxy must be running)
make docs`}
/>
<h2 id="startup-order">Startup Order & Health Checks</h2>
<p>Services start in dependency order:</p>
<ol>
<li>PostgreSQL Redis ClickHouse (databases)</li>
<li>Keycloak (waits for PostgreSQL health check)</li>
<li>PII service (independent)</li>
<li>Go proxy (waits for PostgreSQL, uses <code>service_started</code> for others)</li>
<li>React web (waits for proxy)</li>
<li>Prometheus Grafana (monitoring)</li>
</ol>
<Callout type="warning" title="Proxy health check">
The proxy Docker image uses <code>distroless/static</code> no shell, no{" "}
<code>wget</code>. Services that depend on the proxy use{" "}
<code>condition: service_started</code> rather than a health check command.
</Callout>
<h2 id="first-run">First Run: Database Migrations</h2>
<p>
On first start, the proxy automatically applies PostgreSQL migrations (9 migration files)
and ClickHouse DDL. You can also run migrations manually:
</p>
<CodeBlock
language="bash"
code={`# Apply all pending migrations
make migrate-up
# Check current migration version
make migrate-status
# Roll back last migration
make migrate-down`}
/>
<h2 id="proto">Protocol Buffer Generation</h2>
<p>
If the <code>gen/</code> or <code>services/pii/gen/</code> directories are missing (e.g.,
fresh clone), regenerate the gRPC stubs before starting:
</p>
<CodeBlock
language="bash"
code={`# Requires buf CLI: brew install buf
make proto
# Lint proto definitions
make proto-lint`}
/>
<Callout type="info">
The PII service starts but rejects all gRPC requests if <code>services/pii/gen/</code> is
missing. Run <code>make proto</code> first.
</Callout>
<h2 id="logs">Viewing Logs</h2>
<CodeBlock
language="bash"
code={`# All services
make dev-logs
# Single service
docker compose logs -f proxy
docker compose logs -f pii
docker compose logs -f postgres`}
/>
</div>
);
}

View File

@ -0,0 +1,116 @@
import { CodeBlock } from "../components/CodeBlock";
import { Callout } from "../components/Callout";
export function ProvidersPage() {
return (
<div>
<h1 id="provider-setup">Provider Setup</h1>
<p>
Veylant IA supports 5 LLM providers out of the box. Each provider implements the{" "}
<code>provider.Adapter</code> interface (<code>Send</code>, <code>Stream</code>,{" "}
<code>Validate</code>, <code>HealthCheck</code>).
</p>
<Callout type="info" title="Adding a new provider">
Implement the <code>provider.Adapter</code> interface in{" "}
<code>internal/provider/&lt;name&gt;/</code> and register it in{" "}
<code>cmd/proxy/main.go</code>. No other changes needed.
</Callout>
<h2 id="openai">OpenAI</h2>
<CodeBlock
language="yaml"
code={`providers:
openai:
api_key: "sk-..." # VEYLANT_PROVIDERS_OPENAI_API_KEY
base_url: "https://api.openai.com/v1" # optional override`}
/>
<p>Supported models: <code>gpt-4o</code>, <code>gpt-4o-mini</code>, <code>gpt-4-turbo</code>, <code>gpt-3.5-turbo</code></p>
<h2 id="anthropic">Anthropic</h2>
<CodeBlock
language="yaml"
code={`providers:
anthropic:
api_key: "sk-ant-..." # VEYLANT_PROVIDERS_ANTHROPIC_API_KEY
base_url: "https://api.anthropic.com" # optional`}
/>
<p>Supported models: <code>claude-3-5-sonnet-20241022</code>, <code>claude-3-opus-20240229</code>, <code>claude-3-haiku-20240307</code></p>
<h2 id="azure">Azure OpenAI</h2>
<p>
Azure requires a deployment name instead of a model name. The proxy maps OpenAI model IDs
to Azure deployment names via the routing rule's <code>target_model</code> field.
</p>
<CodeBlock
language="yaml"
code={`providers:
azure:
api_key: "..." # VEYLANT_PROVIDERS_AZURE_API_KEY
endpoint: "https://YOUR_RESOURCE.openai.azure.com"
api_version: "2024-02-01" # Azure API version`}
/>
<h2 id="mistral">Mistral AI</h2>
<CodeBlock
language="yaml"
code={`providers:
mistral:
api_key: "..." # VEYLANT_PROVIDERS_MISTRAL_API_KEY
base_url: "https://api.mistral.ai/v1" # optional`}
/>
<p>Supported models: <code>mistral-large-latest</code>, <code>mistral-medium-latest</code>, <code>mistral-small-latest</code></p>
<h2 id="ollama">Ollama (Self-hosted)</h2>
<p>
Ollama requires no API key. Ensure the Ollama server is reachable from the proxy container.
</p>
<CodeBlock
language="yaml"
code={`providers:
ollama:
base_url: "http://host.docker.internal:11434" # from Docker to host`}
/>
<CodeBlock
language="bash"
code={`# Pull a model before routing to it
ollama pull llama3
ollama pull mistral
ollama pull codellama`}
/>
<Callout type="tip" title="GPU acceleration">
Ollama automatically uses GPU if available. For Docker, add{" "}
<code>--gpus all</code> to the container runtime command or use the{" "}
<code>deploy.resources.reservations.devices</code> key in docker-compose.yml.
</Callout>
<h2 id="provider-status">Check Provider Status</h2>
<p>
The admin API exposes circuit breaker state for all providers:
</p>
<CodeBlock
language="bash"
code={`curl http://localhost:8090/v1/admin/providers/status \\
-H "Authorization: Bearer $TOKEN"
# Response:
{
"data": [
{"provider": "openai", "state": "closed", "failures": 0},
{"provider": "anthropic", "state": "closed", "failures": 0},
{"provider": "azure", "state": "open", "failures": 5},
{"provider": "mistral", "state": "closed", "failures": 1},
{"provider": "ollama", "state": "closed", "failures": 0}
]
}`}
/>
<p>Circuit breaker states:</p>
<ul>
<li><strong>closed</strong> Normal operation, requests forwarded</li>
<li><strong>open</strong> Provider bypassed, fallback chain used</li>
<li><strong>half-open</strong> Testing if provider has recovered</li>
</ul>
</div>
);
}

88
web/src/pages/docs/nav.ts Normal file
View File

@ -0,0 +1,88 @@
export interface NavItem {
title: string;
path: string;
}
export interface NavSection {
title: string;
items: NavItem[];
}
export const NAV_SECTIONS: NavSection[] = [
{
title: "Getting Started",
items: [
{ title: "What is Veylant IA?", path: "/docs/getting-started/what-is-veylant" },
{ title: "Quick Start", path: "/docs/getting-started/quick-start" },
{ title: "Key Concepts", path: "/docs/getting-started/concepts" },
],
},
{
title: "Installation",
items: [
{ title: "Docker Compose", path: "/docs/installation/docker" },
{ title: "Configuration", path: "/docs/installation/configuration" },
{ title: "Provider Setup", path: "/docs/installation/providers" },
],
},
{
title: "API Reference",
items: [
{ title: "Authentication", path: "/docs/api/authentication" },
{ title: "Chat Completions", path: "/docs/api/chat-completions" },
{ title: "PII Analysis", path: "/docs/api/pii" },
{ title: "Admin — Policies", path: "/docs/api/admin/policies" },
{ title: "Admin — Users", path: "/docs/api/admin/users" },
{ title: "Admin — Audit Logs", path: "/docs/api/admin/logs" },
{ title: "Admin — Compliance", path: "/docs/api/admin/compliance" },
{ title: "Admin — Feature Flags", path: "/docs/api/admin/flags" },
],
},
{
title: "Guides",
items: [
{ title: "PII Detection & Anonymization", path: "/docs/guides/pii" },
{ title: "Routing Rules Engine", path: "/docs/guides/routing" },
{ title: "RBAC & Permissions", path: "/docs/guides/rbac" },
{ title: "GDPR & AI Act Compliance", path: "/docs/guides/compliance" },
{ title: "Monitoring & Alerting", path: "/docs/guides/monitoring" },
{ title: "Circuit Breaker & Failover", path: "/docs/guides/circuit-breaker" },
],
},
{
title: "Deployment",
items: [
{ title: "Docker Compose", path: "/docs/deployment/docker" },
{ title: "Kubernetes (Helm)", path: "/docs/deployment/kubernetes" },
{ title: "Blue/Green Deployment", path: "/docs/deployment/blue-green" },
],
},
{
title: "Security",
items: [
{ title: "Security Model", path: "/docs/security/model" },
{ title: "API Key Management", path: "/docs/security/api-keys" },
],
},
{
title: "Changelog",
items: [{ title: "v1.0.0 (Latest)", path: "/docs/changelog" }],
},
];
/** Flatten all nav items into a list, preserving section context */
export const ALL_NAV_ITEMS: (NavItem & { section: string })[] = NAV_SECTIONS.flatMap((s) =>
s.items.map((item) => ({ ...item, section: s.title }))
);
/** Get previous and next pages for pagination */
export function getPrevNext(currentPath: string): {
prev: NavItem | null;
next: NavItem | null;
} {
const idx = ALL_NAV_ITEMS.findIndex((i) => i.path === currentPath);
return {
prev: idx > 0 ? ALL_NAV_ITEMS[idx - 1] : null,
next: idx < ALL_NAV_ITEMS.length - 1 ? ALL_NAV_ITEMS[idx + 1] : null,
};
}

View File

@ -0,0 +1,86 @@
import { CodeBlock } from "../components/CodeBlock";
import { Callout } from "../components/Callout";
export function ApiKeysPage() {
return (
<div>
<h1 id="api-keys">API Key Management</h1>
<p>
Veylant IA never stores plain-text API keys. Provider keys (OpenAI, Anthropic, etc.) are
stored in HashiCorp Vault and rotated on a 90-day cycle. User-facing API keys use a
prefix+hash scheme.
</p>
<h2 id="key-format">API Key Format</h2>
<p>
Veylant IA user API keys follow the format <code>sk-vyl_{"{prefix}"}{"{hash}"}</code>:
</p>
<CodeBlock
language="text"
code={`sk-vyl_ab12cd34ef56gh78ij90kl12mn34op56qr78st90
prefix display hash (SHA-256, truncated, stored in DB)
prefix`}
/>
<ul>
<li>The display prefix (<code>ab12cd34</code>) is stored in plaintext for key identification in the dashboard</li>
<li>The full key is SHA-256 hashed; only the hash is stored in PostgreSQL</li>
<li>If a key is compromised, only the prefix reveals which key to revoke not the key value</li>
</ul>
<h2 id="provider-keys">Provider API Keys</h2>
<Callout type="warning" title="Never commit provider keys">
Provider API keys (OpenAI, Anthropic, etc.) must never be committed to the repository.
Use environment variables or HashiCorp Vault.
</Callout>
<CodeBlock
language="bash"
code={`# Set via environment variable (recommended for docker-compose)
export VEYLANT_PROVIDERS_OPENAI_API_KEY=sk-...
# Or load from HashiCorp Vault
vault kv put secret/veylant/providers openai_api_key=sk-... anthropic_api_key=sk-ant-...
# Vault agent sidecar injects secrets as environment variables at runtime`}
/>
<h2 id="rotation">Key Rotation</h2>
<p>Provider API keys are rotated on a 90-day cycle via Vault:</p>
<ol>
<li>Generate new API key from the provider portal</li>
<li>Write new key to Vault: <code>vault kv patch secret/veylant/providers openai_api_key=sk-new...</code></li>
<li>Vault agent syncs the new value to running pods automatically (no restart needed)</li>
<li>Revoke the old key from the provider portal</li>
</ol>
<h2 id="secret-detection">Secret Detection in CI</h2>
<p>
Every commit is scanned by <strong>gitleaks</strong> for accidentally committed secrets.
Any string matching common API key patterns (starting with <code>sk-</code>,{" "}
<code>sk-ant-</code>, etc.) blocks the CI pipeline.
</p>
<CodeBlock
language="bash"
code={`# Run gitleaks locally before committing
gitleaks detect --source .
# The Semgrep rule also catches sk- literals in Go/Python source:
# ID: hardcoded-api-key`}
/>
<h2 id="audit">Key Usage Auditing</h2>
<p>
Every AI request records which API key (by prefix) was used in the ClickHouse audit log.
This allows you to trace the source of any request to a specific service or developer.
</p>
<CodeBlock
language="bash"
code={`# Find all requests from a specific API key prefix
curl "http://localhost:8090/v1/admin/logs?api_key_prefix=ab12cd34" \\
-H "Authorization: Bearer $TOKEN"`}
/>
</div>
);
}

View File

@ -0,0 +1,125 @@
import { Callout } from "../components/Callout";
export function SecurityModelPage() {
return (
<div>
<h1 id="security-model">Security Model</h1>
<p>
Veylant IA is designed with a Zero Trust security model. Every component assumes the
network is hostile and authenticates and authorizes each request independently.
</p>
<h2 id="zero-trust">Zero Trust Architecture</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 my-4">
{[
{
title: "mTLS Between Services",
desc: "All internal service-to-service communication uses mutual TLS. The proxy, PII service, PostgreSQL, Redis, and ClickHouse all authenticate each other via certificates.",
},
{
title: "TLS 1.3 Externally",
desc: "External traffic uses TLS 1.3 minimum. TLS 1.0 and 1.1 are disabled at the Traefik/nginx gateway level.",
},
{
title: "JWT Validation",
desc: "Every API request carries a signed JWT. The proxy validates the signature against Keycloak's JWKS endpoint on every request (cached with TTL).",
},
{
title: "Network Policies",
desc: "Kubernetes NetworkPolicies restrict pod-to-pod communication. Only the proxy can reach the PII service; only Prometheus can scrape /metrics.",
},
].map((item) => (
<div key={item.title} className="rounded-lg border bg-card p-4">
<h3 className="font-semibold text-sm mb-2">{item.title}</h3>
<p className="text-sm text-muted-foreground leading-relaxed">{item.desc}</p>
</div>
))}
</div>
<h2 id="encryption">Encryption at Rest</h2>
<ul>
<li>
<strong>Prompt storage</strong> Encrypted with AES-256-GCM using the{" "}
<code>crypto.key</code> config value (application-level, independent of disk encryption)
</li>
<li>
<strong>PII pseudonymization mappings</strong> Encrypted in Redis with AES-256-GCM per
mapping entry
</li>
<li>
<strong>API keys</strong> Stored as SHA-256 hashes only. The prefix (e.g.{" "}
<code>sk-vyl_ab12cd34</code>) is kept for display, but the full key is never stored
</li>
<li>
<strong>HashiCorp Vault</strong> Provider API keys and the crypto key are stored in
Vault; 90-day rotation cycle
</li>
</ul>
<h2 id="audit-security">Audit-of-the-Audit</h2>
<p>
All accesses to audit logs and compliance reports are themselves logged. This two-level
audit trail satisfies requirements for sensitive data access monitoring.
</p>
<h2 id="semgrep">Custom Security Rules (SAST)</h2>
<p>
CI enforces custom Semgrep rules (<code>.semgrep.yml</code>) that catch common security
issues specific to this codebase:
</p>
<div className="overflow-x-auto my-4">
<table className="w-full text-sm border rounded-lg overflow-hidden">
<thead>
<tr className="bg-muted/50 border-b">
<th className="text-left px-4 py-2.5 font-semibold">Rule</th>
<th className="text-left px-4 py-2.5 font-semibold">What it catches</th>
</tr>
</thead>
<tbody>
{[
{ rule: "context-background-in-handler", catches: "context.Background() in HTTP handlers — use r.Context() to propagate cancellation" },
{ rule: "sql-string-concat", catches: "SQL string concatenation — use parameterized queries ($1, $2, ...)" },
{ rule: "sensitive-field-in-log", catches: "Logging password, api_key, token, secret, Authorization, email, prompt" },
{ rule: "hardcoded-api-key", catches: "String literals starting with sk- hardcoded in source" },
{ rule: "request-body-without-limit", catches: "json.NewDecoder(r.Body) without http.MaxBytesReader" },
{ rule: "python-eval-exec", catches: "eval() or exec() on variables in the PII service" },
].map((row) => (
<tr key={row.rule} className="border-b last:border-0">
<td className="px-4 py-2.5 font-mono text-xs">{row.rule}</td>
<td className="px-4 py-2.5 text-xs text-muted-foreground">{row.catches}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 id="pentest">Penetration Test Results (v1.0.0)</h2>
<Callout type="tip" title="Pentest passed — February 2026">
Grey-box penetration test completed June 920, 2026. Results:
<ul className="mt-2 space-y-1">
<li>
<strong>Critical:</strong> 0
</li>
<li>
<strong>High:</strong> 0
</li>
<li>
<strong>Medium:</strong> 2 (remediated before launch)
</li>
<li>
<strong>Low:</strong> 3 (accepted risk, backlog)
</li>
</ul>
Full report available to enterprise customers under NDA.
</Callout>
<h2 id="ci-pipeline">CI Security Pipeline</h2>
<ul>
<li><strong>Semgrep</strong> SAST on every PR</li>
<li><strong>Trivy</strong> Container image scan (CRITICAL/HIGH blocking)</li>
<li><strong>gitleaks</strong> Secret detection in commits and history</li>
<li><strong>OWASP ZAP</strong> DAST (non-blocking, main branch only)</li>
</ul>
</div>
);
}

View File

@ -15,6 +15,36 @@ import { CostsPage } from "@/pages/CostsPage";
import { CompliancePage } from "@/pages/CompliancePage"; import { CompliancePage } from "@/pages/CompliancePage";
import { NotFoundPage } from "@/pages/NotFoundPage"; import { NotFoundPage } from "@/pages/NotFoundPage";
// Documentation site
import { DocLayout } from "@/pages/docs/DocLayout";
import { DocsHomePage } from "@/pages/docs/DocsHomePage";
import { WhatIsVeylantPage } from "@/pages/docs/getting-started/WhatIsVeylantPage";
import { QuickStartPage } from "@/pages/docs/getting-started/QuickStartPage";
import { KeyConceptsPage } from "@/pages/docs/getting-started/KeyConceptsPage";
import { DockerComposePage } from "@/pages/docs/installation/DockerComposePage";
import { ConfigurationPage } from "@/pages/docs/installation/ConfigurationPage";
import { ProvidersPage as DocProvidersPage } from "@/pages/docs/installation/ProvidersPage";
import { AuthenticationPage } from "@/pages/docs/api-reference/AuthenticationPage";
import { ChatCompletionsPage } from "@/pages/docs/api-reference/ChatCompletionsPage";
import { PiiAnalysisPage } from "@/pages/docs/api-reference/PiiAnalysisPage";
import { AdminPoliciesPage } from "@/pages/docs/api-reference/AdminPoliciesPage";
import { AdminUsersPage } from "@/pages/docs/api-reference/AdminUsersPage";
import { AdminLogsPage } from "@/pages/docs/api-reference/AdminLogsPage";
import { AdminCompliancePage } from "@/pages/docs/api-reference/AdminCompliancePage";
import { AdminFlagsPage } from "@/pages/docs/api-reference/AdminFlagsPage";
import { PiiGuide } from "@/pages/docs/guides/PiiGuide";
import { RoutingGuide } from "@/pages/docs/guides/RoutingGuide";
import { RbacGuide } from "@/pages/docs/guides/RbacGuide";
import { ComplianceGuide } from "@/pages/docs/guides/ComplianceGuide";
import { MonitoringGuide } from "@/pages/docs/guides/MonitoringGuide";
import { CircuitBreakerGuide } from "@/pages/docs/guides/CircuitBreakerGuide";
import { DockerPage } from "@/pages/docs/deployment/DockerPage";
import { KubernetesPage } from "@/pages/docs/deployment/KubernetesPage";
import { BlueGreenPage } from "@/pages/docs/deployment/BlueGreenPage";
import { SecurityModelPage } from "@/pages/docs/security/SecurityModelPage";
import { ApiKeysPage } from "@/pages/docs/security/ApiKeysPage";
import { ChangelogPage } from "@/pages/docs/ChangelogPage";
export const router = createBrowserRouter([ export const router = createBrowserRouter([
{ {
path: "/", path: "/",
@ -50,4 +80,45 @@ export const router = createBrowserRouter([
{ path: "*", element: <NotFoundPage /> }, { path: "*", element: <NotFoundPage /> },
], ],
}, },
// Documentation site — public, no auth required
{
path: "/docs",
element: <DocLayout />,
children: [
{ index: true, element: <DocsHomePage /> },
// Getting Started
{ path: "getting-started/what-is-veylant", element: <WhatIsVeylantPage /> },
{ path: "getting-started/quick-start", element: <QuickStartPage /> },
{ path: "getting-started/concepts", element: <KeyConceptsPage /> },
// Installation
{ path: "installation/docker", element: <DockerComposePage /> },
{ path: "installation/configuration", element: <ConfigurationPage /> },
{ path: "installation/providers", element: <DocProvidersPage /> },
// API Reference
{ path: "api/authentication", element: <AuthenticationPage /> },
{ path: "api/chat-completions", element: <ChatCompletionsPage /> },
{ path: "api/pii", element: <PiiAnalysisPage /> },
{ path: "api/admin/policies", element: <AdminPoliciesPage /> },
{ path: "api/admin/users", element: <AdminUsersPage /> },
{ path: "api/admin/logs", element: <AdminLogsPage /> },
{ path: "api/admin/compliance", element: <AdminCompliancePage /> },
{ path: "api/admin/flags", element: <AdminFlagsPage /> },
// Guides
{ path: "guides/pii", element: <PiiGuide /> },
{ path: "guides/routing", element: <RoutingGuide /> },
{ path: "guides/rbac", element: <RbacGuide /> },
{ path: "guides/compliance", element: <ComplianceGuide /> },
{ path: "guides/monitoring", element: <MonitoringGuide /> },
{ path: "guides/circuit-breaker", element: <CircuitBreakerGuide /> },
// Deployment
{ path: "deployment/docker", element: <DockerPage /> },
{ path: "deployment/kubernetes", element: <KubernetesPage /> },
{ path: "deployment/blue-green", element: <BlueGreenPage /> },
// Security
{ path: "security/model", element: <SecurityModelPage /> },
{ path: "security/api-keys", element: <ApiKeysPage /> },
// Changelog
{ path: "changelog", element: <ChangelogPage /> },
],
},
]); ]);

View File

@ -54,7 +54,7 @@ const config: Config = {
}, },
}, },
}, },
plugins: [], plugins: [require("@tailwindcss/typography")],
}; };
export default config; export default config;