Core concepts
This page walks the data model and the lifecycle of a single artifact end-to-end. It's the document you read once and then re-skim when you're trying to remember what a "repo" is in OrbitalReg's vocabulary.
Project
A project is the top-level governance boundary. A typical deployment has 5–500 projects: one per team, one per product, one per "line of business" — pick the granularity that matches how your existing IAM groups are scoped.
Projects own:
- A set of repositories
- A retention policy default (overridable per repository)
- A license policy
- A quarantine policy
- A list of project-owners (full admin within the project)
- A list of project-editors (push but not policy-edit)
A project's slug is unique platform-wide. Renames are in-place — the UUID stays stable so any downstream referencing FK doesn't break.
Projects are the isolation boundary within a deployment — a deployment itself is single organization. See Tenancy model for what that means and why.
Repository
A repository is a typed package store inside a project. Each repository has a single format (Maven, npm, PyPI, Docker, …) and is either local (uploads only), remote (proxy + caching mirror for an upstream), or virtual (federated view of multiple underlying repos).
The slug is (project, repo)-unique, not platform-unique, so acme-platform/maven and acme-finance/maven are both valid.
Artifact
An artifact is a single uploaded version. Identity is the tuple (repo_id, path, version):
pathis format-specific — for Maven,com/example/my-lib; for npm,@scope/pkg; for Docker,library/alpine.versionis the format's version string. SemVer for npm, Maven coordinates for Maven, OCI tag/digest for Docker, etc.
The artifact row carries the SHA-256 digest of the bytes, the content-type, and a JSONB metadata blob with format-specific fields.
Scanning and Detection
Every upload triggers a scan job. The scan-job worker pool runs the configured scanners against the artifact and writes findings to scan_findings. A finding is a tuple of:
(artifact_id, scanner, vuln_id, severity, license_id, …)vuln_idis the canonical CVE / GHSA / OSV id when availableseverityis normalised to CVSS-style buckets (CRITICAL,HIGH,MEDIUM,LOW,UNKNOWN)
A single artifact typically produces 10–100 findings — most are informational. The interesting ones are the ones above the project's quarantine threshold; those flip the artifact's quarantined bit.
A quarantined artifact:
- Still appears in listings (operators need to see it to triage)
- Returns HTTP 451 on download with a "why-blocked" envelope
- Cannot be promoted across repository boundaries
Security blocks
Detection is reactive — the scanner found something. Security blocks are proactive — an admin authored a rule that says "do not serve artifacts matching this pattern, with this customer-facing message":
block_type: sha256
pattern: "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
reason: "Confirmed-malicious sample from FAS-2025-0042"
customer_message: |
This artifact was blocked because it matches a known supply-chain
attack indicator. Contact security@example.com if you believe this
is in error.Block types: path (exact match against the request path), sha256 (content-hash match), pattern (a path.Match glob against the request path), package, and auto_cve (written automatically by auto-quarantine, matched like path). Blocks fire on every download path — Maven, npm, PyPI, Docker, the lot.
package blocks match a full path segment, not a substring. The pattern must equal one of the segments of the request path bounded by / on both sides — this is a heuristic that works because every format adapter puts the package name in its own segment, but the segment boundary means a pattern that includes the version (like log4j-core-2.14.1) will never match, because the version is part of the file name segment, not a segment of its own. Drop the version:
| Ecosystem | Example request path | Matching package pattern | Pattern that never matches |
|---|---|---|---|
| Maven | org/apache/logging/log4j/log4j-core/2.14.1/log4j-core-2.14.1.jar | log4j-core | log4j-core-2.14.1 |
| npm | npm/<project>/<repo>/lodash/-/lodash-4.17.21.tgz | lodash | lodash-4.17.21 |
| NuGet | nuget/<project>/<repo>/v3-flatcontainer/contoso.utils/1.0.10/contoso.utils.1.0.10.nupkg | contoso.utils | contoso.utils.1.0.10 |
If you need to block one specific version, use a pattern block with a glob (e.g. **/log4j-core-2.14.1.jar) or a path block with the exact path instead.
The public /blocked/<id> page (no auth) renders the customer message so a developer who ran npm install and got blocked has somewhere to go.
Retention
A retention policy is a per-repository rule that prunes old versions on a schedule:
keep-N-newest— keep the latest N versions of eachpathkeep-by-age— keep versions newer than N days- Combinations are allowed; the union of all matching rules survives
Pruned artifacts soft-delete first (visible in Trash for 30 days), then hard-delete the row + S3 object.
Retention runs hourly. The retention_audit table records every deletion with the rule that triggered it, the artifact metadata, and the operator (or service account) who installed the rule.
Promotion
Promotion moves an artifact between repositories under a gate. The gate has three sub-checks:
- CVE policy — no findings above the project's threshold
- License policy — every detected license appears in the project's allow-list
- Scan rule policy — no scanner-specific custom rules failing
A promotion request that passes the gate runs as a copy + a write to promotion_audit; one that fails returns the failing checks so CI can decide whether to block the build or surface a warning.
Sigstore trust policies
When you wire OrbitalReg into a CI flow that signs artifacts via cosign keyless, the verifier needs to know which (identity_pattern, issuer_pattern, fulcio_root, rekor_url) tuples it trusts. Those live in sigstore_trust_policies and are managed under Admin → Sigstore → Trust policies.
Service accounts + tokens
Service accounts are non-human principals scoped to a project. Each can have one or more tokens (the actual orbsa_… bearer strings used in Authorization: Bearer … headers).
For CI, prefer the OIDC token exchange flow — it eliminates long-lived bearer tokens entirely. See OIDC token exchange.
Who sees what
Project visibility is least-privilege: a non-org_admin user sees only the projects they have an explicit role_bindings row for — either a direct binding on that project, or an org-wide binding (project_id IS NULL) that grants every project.
There are two independent ways a user gets a role, and only one of them creates visibility:
- Config-mapped groups —
ORBITALREG_AUTH_ADMIN_GROUPS/_DEVELOPER_GROUPS/_READER_GROUPS(auth.adminGroups/developerGroups/readerGroupsin the Helm values) map an IdP group claim to a role name at login time. This decides what a user is allowed to do once they can see a project, and grantsorg_adminits all-projects visibility — but membership inreaderGroups/developerGroupsby itself creates norole_bindingsrow. role_bindingsrows — created explicitly (project owner adding a member, an org-wide grant, or SCIM group provisioning intouser_groups) — are whatVisibleProjectIDsactually filters on.
The practical effect: a freshly-provisioned SSO user who is only in the readerGroups claim, with no role_bindings row yet, logs in to a completely empty project list — not a bug, but easy to mistake for one. The /api/v1/whoami response's hasAnyBinding flag (true for org_admin, an org-wide binding, or any project-scoped row) lets the frontend distinguish "no projects exist yet" from "you have no access yet" and show an explanatory empty state instead of a bare table. Give new non-admin users an explicit role_bindings row (project membership or an org-wide grant) — don't rely on group mapping alone to grant them visibility.
Webhook subscriptions
Outbound events: artifact uploaded, artifact promoted, finding above threshold, retention deletion, etc. Each subscription has:
- A target URL (HTTPS only, validated at CRUD time)
- A shared HMAC secret (write-only — list responses return
has_secret: bool) - An event filter (subset of the published catalog)
- A delivery target type — generic webhook, Slack, Microsoft Teams
The full event catalog lives at Webhook events.