← All docs

Signet — Frontend Design (Next.js Portal, v0.1)

The web portal (web/): route map, rendering strategy, component architecture, data access, auth, and testing. Scaffolded at milestone M2. Related: architecture.md §6, api-reference.md

1. Stack

ConcernChoiceRationale
FrameworkNext.js (App Router), TypeScript strictarchitecture §2 — SSR/ISR keyed to data mutability, SEO for audit pages
StylingTailwind CSS + a small set of shared primitivesboring, greppable; no runtime CSS-in-JS cost on server components
Datanative fetch in server components + a typed API client generated from the OpenAPI specone source of truth for shapes; no client state library until something actually needs it
AuthAuth.js with GitHub OAuthportal sessions only — signing never happens in the browser (§5)
Charts (score history)server-rendered SVG sparklinesno client charting dependency for read-only trend lines

2. Route Map & Rendering Strategy

app/
├── page.tsx                      # home: search + registry stats        [SSR, 60s cache]
├── records/[id]/page.tsx         # record detail                        [ISR, content-addressed → effectively static]
├── repo/[host]/[owner]/[name]/
│   ├── page.tsx                  # repo overview: audited commits       [SSR, 60s]
│   └── [commit]/page.tsx         # coverage view for one commit        [SSR, 30s]
├── auditors/[id]/page.tsx        # identity profile: tier, R, history   [SSR, 300s]
├── disputes/page.tsx             # dispute queue (reviewer workflow)    [dynamic, no cache]
├── search/page.tsx               # search results                       [dynamic]
├── docs/[...slug]/page.tsx       # rendered design/user docs            [static]
└── api/auth/[...nextauth]/       # Auth.js routes (the only portal API routes)

Rendering rules (architecture §6):

  • Immutable data → ISR. /records/[id] content never changes; only the

registry annotation block (verifications, dispute status) does. The page splits accordingly: bundle content rendered at ISR time, the annotation panel in a Suspense boundary fetched per-request with a short TTL.

  • **Mutable data → SSR with fetch revalidate TTLs**, no client-side

polling. The portal is a reader, not a dashboard.

  • No portal-private API. Every page renders from the public REST API —

if a page needs data, tools get the same endpoint (backend.md rule). The only exception is Auth.js session routes.

3. Component Architecture

components/
├── record/        BundleViewer, PredicateView (per-type: Audit/Verification/Flag/Vouch),
│                  SignatureChain, VerificationList, DisputeBanner
├── coverage/      CoverageTable, ConfidenceBadge, CweChips, FreshnessIndicator
├── identity/      TierBadge, ScoreCard (E×G breakdown), ScoreSparkline, SlashHistory
├── shared/        UrnLink, ShaChip (truncate + copy + monospace), TimeAgo,
│                  SearchBox, EmptyState, ProblemError (renders problem+json)
└── layout/        Header, Footer, ThemeToggle

Conventions:

  • Server components by default; "use client" only for interactivity

(copy buttons, search box, theme toggle, dispute forms).

  • Trust-sensitive rendering rules live in exactly one place each:

ConfidenceBadge owns the confidence→color mapping, DisputeBanner owns "disputed = treat as uncovered" messaging, TierBadge owns tier labels. These encode trust-model semantics; scattering them invites inconsistency.

  • Everything renders from API types generated off OpenAPI — no hand-written

response interfaces.

4. Key Page Specs

  • Record detail — the page a CVE triager lands on. Above the fold:

status, subjects (repo @ commit), signer + tier + reputation, dispute banner if any. Then: claims table (status × CWEs), scope file list with hashes, false-positive entries (rule ID + justification — this is the FP registry's human UI), methodology, verification list with depths, raw bundle (collapsed, copyable).

  • Commit coverage — the "should I trust the skip-scan" page: per-file

table mirroring GET /v1/coverage exactly (same numbers the pipeline saw), with a confidence-threshold slider that filters client-side.

  • Auditor profile — tier, R score with E×G breakdown and params version,

score sparkline, attestation history, slash events with cause_ref links. Full transparency of the reputation math is a trust feature, not vanity UI.

  • Dispute queue — the one write-flow in the portal: reviewers (session +

reputation gate checked server-side by the API on submission) browse open flags with evidence and submit resolutions. Resolution submissions are signed records too — the portal hands the reviewer a prefilled signet flag resolve … CLI command rather than signing in the browser.

5. Auth & Key Boundary

  • GitHub OAuth session = *browsing* identity: linking your profile page,

prefilled CLI commands, dispute-queue visibility.

  • Private keys never touch the portal (cli-spec.md §9). Any action

requiring a signature renders as a copyable CLI command with context filled in. If a browser signing flow ever lands, it's WebAuthn-backed and a separate design review.

6. Quality & Testing

  • Playwright E2E against a seeded docker-compose stack (fixture records from

examples/generate-sample): record page renders and verifies, coverage table matches API response, dispute flow gates correctly.

  • Vitest for pure logic (confidence formatting, URN parsing).
  • Accessibility: badges/banners never encode meaning by color alone

(confidence and dispute states carry text labels); keyboard-first tables.

  • Web vitals budget: record page LCP < 1.5s on cold ISR miss; zero

client-side JS on the docs routes.

7. Self-Hosting Note (mirrors)

The portal is optional per mirror (architecture §10-Q4). It ships as a standalone Next.js Docker target with SIGNET_API_URL as its only required config; ISR uses the default filesystem cache handler — no external cache service. Minimal mirrors stay API-only.