Core concepts
Five entities make up the Patchwire data model. Understanding their boundaries is the difference between using Patchwire correctly and fighting it.
Organisation (org)
The top-level tenant. Everything else belongs to exactly one org. Every database row has an org_id column, every query filters by it, and every JWT carries the org slug it was issued for.
Properties:
slug— URL-safe, unique across the whole installation, immutable after creation.name— display label.status—activefor normal use; archived/suspended states are reserved for future moderation features.
Nothing crosses an org boundary. A user from org A literally cannot see a project from org B; the SQL never joins them.
User
A person who can sign in. Belongs to one org.
email— unique within the org.role—adminormember. The first user (created via/register) is alwaysadmin.status—active,suspended, ordeleted.
Note the role list is intentionally short. Patchwire today does not have fine-grained roles; the assumption is small teams where everyone trusted to log in is also trusted to act.
Project
A repository registered for scanning. Belongs to one org.
slug— unique within the org. Becomes part of every URL and the<owner>/<project>mapping that webhooks resolve.name,description— human metadata.repo_url— the HTTPS clone URL. Pre-fills the scan form.default_branch— the branch scanned when none is specified (defaultmain).access_token_enc— encrypted PAT for private repos. Never returned by the API; the response carrieshas_access_token: boolinstead.access_token_user— the URL-embedded HTTP Basic username (x-access-tokenfor GitHub,oauth2for GitLab, your username for Bitbucket).
Scan
One execution of the scanner pipeline against one project at one branch.
id— UUID. The path component used in URLs.project_id,org_id— denormalised tenant scope.scanner_type— provenance label:webhook-all(all scanners run, fired by a push event),manual-all(all scanners, button-clicked).target_ref— the commit SHA from the webhook, or the branch name for manual runs.status—pending→running→completed(orfailedif any scanner errored).summary— JSON object with severity counts:{critical, high, medium, low, info, total}.started_at,finished_at— timestamps.
Scans are immutable once completed or failed. Re-running creates a new scan row.
Finding
A single issue surfaced by one of the scanners, attached to one scan.
external_id— the scanner's own identifier for this finding. The pair(scan_id, external_id)is unique — it's how dedupe works.title,description— what the rule says.severity—critical|high|medium|low|info. Maps from the scanner's native severity field.scanner—semgreporgitleaks.location_path,location_line,location_column— where in the repo.metadata— JSON blob for scanner-specific extras.
How they relate
organisation
├── users (1:N)
└── projects (1:N)
└── scans (1:N)
└── findings (1:N)A finding is always reachable from its org via two foreign-key hops. That's load-bearing for the auth model — the API never trusts a finding's org_id in isolation; it always cross-checks the parent project's org.
What about webhooks?
Webhooks aren't a database entity. They're configured on the SCM side — the only state Patchwire keeps is the shared HMAC secret in PATCHWIRE_WEBHOOK_SECRET (one secret for the whole installation, all four providers).
When a webhook arrives, Patchwire:
- Verifies the HMAC against the body bytes.
- Parses the repository's
full_name(owner/name) to find the org slug + project slug. - Looks up the project; if absent, returns 200 with
"status":"ignored"so retries stop. - Creates a scan row, kicks off the background pipeline, returns 200 within the provider's deadline.
See Webhooks for the setup walkthrough.