← All docs

Signet — System Architecture & MVP Plan (Design v0.1)

Concrete system design: components, stack, storage layout, the <100 ms read path, trust-engine execution model, replication, and a phased MVP cut. Related: requirements.md (FR-1/2/3, NFR-1–4), reputation-trust-model.md, attestation-schema.md

1. Component Overview

%%{init: {'theme': 'base', 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'useMaxWidth': true }}}%%
flowchart TB
    subgraph CLIENTS["Clients"]
        CLI["signet CLI<br/>(CI/CD, researchers)"]:::cli
        PORTAL["Web portal<br/>(Next.js)"]:::portal
        AGENTS["AI agents /<br/>security tools"]:::agents
    end
    subgraph CORE["Registry core (Spring Boot)"]
        API["API service<br/>REST v1"]:::api
        VAL["Submission validator<br/>(DSSE, schema, tiers)"]:::val
        TRUST["Trust engine<br/>(beta + EigenTrust)"]:::trust
    end
    subgraph DATA["Data plane"]
        PG[("PostgreSQL<br/>records + indexes")]:::pg
        LOG[("Transparency log<br/>(Rekor)")]:::tlog
        OBJ[("Object storage<br/>daily dumps")]:::obj
    end
    GT["Ground-truth ingest<br/>(OSV/GHSA poller)"]:::gt

    CLI --> API
    PORTAL --> API
    AGENTS --> API
    API --> VAL
    VAL --> PG
    VAL --> LOG
    TRUST --> PG
    GT --> TRUST
    PG --> OBJ

    classDef cli fill:#dbeafe,stroke:#2563eb
    classDef portal fill:#dcfce7,stroke:#16a34a
    classDef agents fill:#fef3c7,stroke:#d97706
    classDef api fill:#e0e7ff,stroke:#4f46e5
    classDef val fill:#fce7f3,stroke:#db2777
    classDef trust fill:#ccfbf1,stroke:#0d9488
    classDef pg fill:#f3e8ff,stroke:#9333ea
    classDef tlog fill:#fee2e2,stroke:#dc2626
    classDef obj fill:#ffedd5,stroke:#ea580c
    classDef gt fill:#fef9c3,stroke:#ca8a04

2. Stack Choices & Rationale

ComponentChoiceRationale
API backendJava 21 + Spring Boot 3 (Spring MVC on virtual threads, JdbcClient)Team stack. sigstore-java covers Fulcio/Rekor first-party; virtual threads give NFR-2 bulk-read concurrency with plain blocking code; RFC 9457 ProblemDetail, Flyway, Micrometer are all framework-native. Ships as one container image for mirror operators (NFR-3). See backend.md.
CLIRust (crates/signet-cli, built on signet-bundle)Single static binary for CI/CD distribution — no runtime dependency in pipelines. Crypto parity with the Java server is enforced by shared conformance vectors (backend.md §9).
Web portalNext.js (App Router, TypeScript)Server components + ISR fit a registry perfectly: record pages are content-addressed and immutable → render once, cache forever (§6). SEO for audit pages matters (researchers land from CVE/Google). API calls stay server-side, so the browser never needs registry tokens.
Primary storePostgreSQL 16Relational integrity for the social graph (verifications → attestations); JSONB for predicates; covering indexes get us <100 ms without a second system (§5). Boring, mirror-operator-friendly.
CacheNone in MVP — Postgres + HTTP cachingContent-addressed records are immutable → Cache-Control: immutable + CDN does what Redis would. Add Redis only if coverage-query p99 demands it (measured, not assumed).
Transparency logRekor (public instance in MVP; self-hosted option documented)NFR-4 tamper-evidence with zero build cost.
DumpsObject storage (S3-compatible) JSONL bundles + signed manifestPer schema doc §7.
Trust engineIn-process Spring workers (jobs-table polling + @Scheduled batches)§7. No separate queue system in MVP — a Postgres jobs table with SKIP LOCKED is enough at launch scale.
API styleREST v1 now; gRPC deferredFR-2 says REST *or* gRPC. REST + JSON keeps mirror/tool integration friction lowest; the read path is cache-friendly GETs. Revisit gRPC when a bulk-streaming consumer actually asks for it.
License / repoApache-2.0 monorepo: /server (Spring Boot), /web (Next.js), /crates (Rust CLI + verification lib), /schemas, /conformance, /docsNFR-1. Two crypto implementations (Rust CLI, Java server) kept in lockstep by shared schemas and conformance vectors.

3. Storage Layout

Envelopes are the source of truth; everything else is a derived index and can be rebuilt from the record table (this is what makes dumps sufficient for mirrors).

-- Source of truth: the signed bundle, verbatim
records (
  id            text PRIMARY KEY,     -- urn:signet:att:sha256:...
  record_type   text NOT NULL,        -- audit | verification | flag | vouch
  bundle        jsonb NOT NULL,       -- full Sigstore bundle (DSSE + proof)
  signer_id     text NOT NULL REFERENCES identities(id),
  created_at    timestamptz NOT NULL  -- from Rekor inclusion (trusted time)
)

-- Derived indexes (rebuildable)
subjects (record_id, repo_url, commit_sha, purl)
  -- INDEX (repo_url, commit_sha) INCLUDE (record_id)  ← the hot path
  -- INDEX (purl)
scope_files (record_id, path, file_sha256, line_start, line_end)
  -- INDEX (file_sha256), INDEX (record_id, path)
claims (record_id, status, cwe)                    -- per-CWE coverage
record_refs (record_id, target_record_id, kind)    -- verification/flag edges

-- Trust engine
identities (id, tier, operator_id, pubkey_or_san, created_at)
reputation_events (id, identity_id, event_type, weight, occurred_at, cause_ref)
scores (identity_id, e_score, g_score, r_score, computed_at, params_version)

-- Materialized read model (§5)
coverage (repo_url, commit_sha, path, file_sha256,
          confidence, vuln_classes, status, disputed, updated_at)
  -- PK (repo_url, commit_sha, path)

4. Write Path

  1. POST /v1/records with the signed bundle.
  2. Validator runs schema-doc §8 checks (signature → canonical form → schema →

subject rules → operator co-sig → referential checks → tier rate limit).

  1. Insert into records, submit to Rekor, extract derived rows

(subjects, scope_files, claims, record_refs) in one transaction.

  1. Emit record.accepted event → trust engine updates incrementally →

coverage rows for the affected (repo, commit, path) tuples are recomputed and upserted.

Writes are allowed to be slow (hundreds of ms) — NFR-2 constrains only reads. Signature verification and Rekor submission happen before ack; coverage materialization is async (eventually consistent within seconds).

5. Read Path (< 100 ms, NFR-2)

The design insight: coverage is computed at write time, not query time. A query never walks attestations, verifications, and reputation scores — that work happened when the record (or a score change) landed. Reads are index lookups on the coverage table.

  • GET /v1/coverage?repo=…&commit=… → single index-only scan on the PK prefix.

Target p99 < 20 ms from Postgres, leaving headroom for the network. Measured (single-row coverage, local Postgres, 300 reqs incl. HTTP client overhead): mean 18 ms, p95 23 ms, p99 28 ms — well inside the NFR-2 <100 ms budget.

  • min_confidence filtering happens API-side on the returned rows (confidence is

precomputed per file); custom trust_root recomputation is not served by the hosted read path (it's a mirror feature — run your own with your seed set).

  • Bulk/SBOM endpoint: POST /v1/coverage/bulk with up to 5,000 purls →

single = ANY(...) indexed query, paginated response. This is the CI/CD entry point, so it gets its own rate-limit class (high, per NFR-2 concurrency).

  • Record fetches (GET /v1/records/{id}) are immutable → `Cache-Control:

public, max-age=31536000, immutable` → CDN absorbs repeat traffic.

  • Score changes (slashing, decay) invalidate affected coverage rows via the

trust engine's write-back — a slashed auditor's coverage drops within one engine cycle, which is the property T4 mitigation depends on.

6. Web Portal (Next.js)

  • App Router + TypeScript, server components by default; the portal is a

read-mostly surface over the same public REST API (no private endpoints — if the portal needs it, tools get it too).

  • Rendering strategy keyed to data mutability:
  • /records/[id] — content-addressed, immutable → **ISR with long

revalidate** (effectively static after first render).

  • /repo/[...slug] coverage views — mutable (new attestations, score changes)

→ server-rendered with short-TTL fetch cache.

  • Search — server actions hitting /v1/search.
  • Auth: GitHub OAuth (Auth.js) for portal sessions — used for *browsing

identity linkage* and flag submission UX. Signing never happens server-side: attestation submission from the browser hands off to the CLI (copy-paste command) or a WebAuthn-backed flow post-MVP. Private keys never touch the portal.

  • Pages that matter at MVP: record detail (rendered predicate + verification

chain + dispute status), repo/commit coverage view, auditor profile (score history, tier, attestation list), flag/dispute queue for reviewers.

7. Trust Engine Execution Model

ComputationTriggerLatency requirement
Beta evidence update (E)Event: verification accepted, flag resolved, ground-truth hitSeconds — slashing must propagate fast (T4)
Coverage upsertEvent: record accepted or any input score changedSeconds
EigenTrust (G)Nightly batch over the endorsement graphDaily is fine — graph trust moves slowly
Decay (λ, λ_age)Nightly batchDaily
Ground-truth ingestOSV/GHSA feed poll (hourly) → match against subjects (repo + commit ancestry) and scope_files → falsification events into the dispute queue (trust model §10-Q2: never auto-slash)Hours

All engine outputs are deterministic functions of (records, params_version, seed set) — a mirror replaying the dump with the same signed params reproduces identical scores and coverage (NFR-3).

8. Decentralization & Deployment (NFR-3)

  • **One docker compose up mirror**: the signet-server container (Spring Boot), Postgres, and a

sync job that pulls daily dumps, verifies bundle-by-bundle, and rebuilds derived tables. Portal is optional per mirror.

  • The hosted instance at signet.dev is *a* mirror with write acceptance, not

*the* registry — any record is a self-verifying bundle acceptable at any mirror (schema doc §8).

  • Federation of *writes* between mirrors (gossip/pull) is deliberately post-MVP;

MVP decentralization = verified read replicas + full data portability.

9. MVP Cut

M0 — Foundations (skeleton): monorepo scaffolding, JSON Schemas published, Rust bundle verification library (CLI), Spring Boot service skeleton, shared conformance vectors, Postgres migrations.

M1 — Write + Read core: submit/validate/store audit records (keyless profile only), Rekor logging, coverage materialization, GET /coverage + GET /records, bulk endpoint, daily dumps. *Trust model simplification:* tiers + probation + rate limits only — confidence = author-tier prior (no E/G computation yet).

M2 — Social layer: verification/flag/vouch records, beta evidence engine, verifier slashing, dispute queue, ground-truth ingest, Next.js portal (record, coverage, profile, dispute pages).

M3 — Full trust + ecosystem: EigenTrust batch, signed params config, mirror sync tooling (signet mirror), self-managed-key profile, agent-based simulation harness (trust model §10-Q5) gating parameter defaults.

Deferred beyond M3: gRPC, write federation, diff-aware line re-anchoring, per-ecosystem scores, ZK operator proofs.

10. Open Questions

  1. Commit-ancestry matching for ground truth. Mapping "CVE fixed at commit X"

to "attestations on ancestor commits touching the fixed files" needs git graph data the registry doesn't store. Options: shallow clone on demand (slow, but it's an hourly batch), or GitHub commit-compare APIs (rate limits). Prototype in M2.

  1. Coverage-table cardinality. One row per (repo, commit, path) explodes on

monorepos with many attested commits. Likely fine to 10⁸ rows in Postgres with the PK index; needs a load test in M1 before betting the read path on it.

  1. Rekor dependency. Public Rekor imposes external rate limits and

availability coupling on the write path. MVP accepts this; measure and decide in M2 whether to self-host or make log submission async-with-pending-status.

  1. Next.js self-hosting for mirrors. ISR requires a Node server (or

cache-handler config) — fine in Docker, but document the portal as optional so minimal mirrors stay API-only.