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:#ca8a042. Stack Choices & Rationale
| Component | Choice | Rationale |
|---|---|---|
| API backend | Java 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. |
| CLI | Rust (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 portal | Next.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 store | PostgreSQL 16 | Relational 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. |
| Cache | None in MVP — Postgres + HTTP caching | Content-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 log | Rekor (public instance in MVP; self-hosted option documented) | NFR-4 tamper-evidence with zero build cost. |
| Dumps | Object storage (S3-compatible) JSONL bundles + signed manifest | Per schema doc §7. |
| Trust engine | In-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 style | REST v1 now; gRPC deferred | FR-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 / repo | Apache-2.0 monorepo: /server (Spring Boot), /web (Next.js), /crates (Rust CLI + verification lib), /schemas, /conformance, /docs | NFR-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
POST /v1/recordswith the signed bundle.- Validator runs schema-doc §8 checks (signature → canonical form → schema →
subject rules → operator co-sig → referential checks → tier rate limit).
- Insert into
records, submit to Rekor, extract derived rows
(subjects, scope_files, claims, record_refs) in one transaction.
- Emit
record.acceptedevent → 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_confidencefiltering 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/bulkwith 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
coveragerows 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
| Computation | Trigger | Latency requirement |
|---|---|---|
| Beta evidence update (E) | Event: verification accepted, flag resolved, ground-truth hit | Seconds — slashing must propagate fast (T4) |
| Coverage upsert | Event: record accepted or any input score changed | Seconds |
| EigenTrust (G) | Nightly batch over the endorsement graph | Daily is fine — graph trust moves slowly |
| Decay (λ, λ_age) | Nightly batch | Daily |
| Ground-truth ingest | OSV/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 upmirror**: thesignet-servercontainer (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.devis *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
- 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.
- 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.
- 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.
- 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.