Skip to content

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.
  • statusactive for 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.
  • roleadmin or member. The first user (created via /register) is always admin.
  • statusactive, suspended, or deleted.

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 (default main).
  • access_token_enc — encrypted PAT for private repos. Never returned by the API; the response carries has_access_token: bool instead.
  • access_token_user — the URL-embedded HTTP Basic username (x-access-token for GitHub, oauth2 for 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.
  • statuspendingrunningcompleted (or failed if 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.
  • severitycritical | high | medium | low | info. Maps from the scanner's native severity field.
  • scannersemgrep or gitleaks.
  • 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:

  1. Verifies the HMAC against the body bytes.
  2. Parses the repository's full_name (owner/name) to find the org slug + project slug.
  3. Looks up the project; if absent, returns 200 with "status":"ignored" so retries stop.
  4. Creates a scan row, kicks off the background pipeline, returns 200 within the provider's deadline.

See Webhooks for the setup walkthrough.

Released under a proprietary licence.