diff --git a/package-lock.json b/package-lock.json
index f0a04f7..0599d9a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -117,7 +117,6 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -892,7 +891,6 @@
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.2.tgz",
"integrity": "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
@@ -982,7 +980,6 @@
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz",
"integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
@@ -992,7 +989,6 @@
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.40.0.tgz",
"integrity": "sha512-WA0zdU7xfF10+5I3HhUUq3kqOx3KjqmtQ9lqZjfK7jtYk4G72YW9rezcSywpaUMCWOMlq+6E0pO1IWg1TNIhtg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@codemirror/state": "^6.6.0",
"crelt": "^1.0.6",
@@ -1297,7 +1293,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -2663,7 +2658,6 @@
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@lezer/common": "^1.3.0"
}
@@ -2814,7 +2808,6 @@
"resolved": "https://registry.npmjs.org/@mdx-js/loader/-/loader-3.1.1.tgz",
"integrity": "sha512-0TTacJyZ9mDmY+VefuthVshaNIyCGZHJG2fMnGaDttCt8HmjUF7SizlHJpaCDoGnN635nK1wpzfpx/Xx5S4WnQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@mdx-js/mdx": "^3.0.0",
"source-map": "^0.7.0"
@@ -2874,7 +2867,6 @@
"resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz",
"integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/mdx": "^2.0.0"
},
@@ -3261,7 +3253,6 @@
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": "^14.21.3 || >=16"
},
@@ -6016,6 +6007,18 @@
}
}
},
+ "node_modules/@swagger-api/apidom-parser-adapter-yaml-1-2/node_modules/tree-sitter": {
+ "version": "0.22.4",
+ "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.22.4.tgz",
+ "integrity": "sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "node-addon-api": "^8.3.0",
+ "node-gyp-build": "^4.8.4"
+ }
+ },
"node_modules/@swagger-api/apidom-reference": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-reference/-/apidom-reference-1.7.0.tgz",
@@ -6846,7 +6849,6 @@
"integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"undici-types": "~7.18.0"
}
@@ -6871,7 +6873,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -6964,7 +6965,6 @@
"integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.57.1",
"@typescript-eslint/types": "8.57.1",
@@ -7467,7 +7467,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -8056,7 +8055,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -8266,7 +8264,6 @@
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.2.tgz",
"integrity": "sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg==",
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"@chevrotain/cst-dts-gen": "11.1.2",
"@chevrotain/gast": "11.1.2",
@@ -8440,7 +8437,6 @@
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=6"
}
@@ -8770,7 +8766,6 @@
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz",
"integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10"
}
@@ -9184,7 +9179,6 @@
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
- "peer": true,
"engines": {
"node": ">=12"
}
@@ -9783,8 +9777,7 @@
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/embla-carousel-react": {
"version": "8.6.0",
@@ -10174,7 +10167,6 @@
"integrity": "sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.2",
@@ -10318,7 +10310,6 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -10509,7 +10500,6 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -10912,7 +10902,6 @@
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
@@ -11987,7 +11976,6 @@
"integrity": "sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=16.9.0"
}
@@ -12139,7 +12127,6 @@
"resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.3.tgz",
"integrity": "sha512-AUY/VyX0E5XlibOmWt10uabJzam1zlYjwiEgQSDc5+UIkFNaF9WM0JxXKaNMGf+F/ffUF+7kRKXM9A7C0xXqMg==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -12917,6 +12904,7 @@
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
"integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==",
"license": "MIT",
+ "peer": true,
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
@@ -13217,6 +13205,7 @@
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.117.tgz",
"integrity": "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"isomorphic.js": "^0.2.4"
},
@@ -15072,7 +15061,6 @@
"resolved": "https://registry.npmjs.org/next/-/next-16.2.0.tgz",
"integrity": "sha512-NLBVrJy1pbV1Yn00L5sU4vFyAHt5XuSjzrNyFnxo6Com0M0KrL6hHM5B99dbqXb2bE9pm4Ow3Zl1xp6HVY9edQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@next/env": "16.2.0",
"@swc/helpers": "0.5.15",
@@ -15930,7 +15918,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -16393,7 +16380,6 @@
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.30.1.tgz",
"integrity": "sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw==",
"license": "MIT",
- "peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ramda"
@@ -16485,7 +16471,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -16504,7 +16489,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -16547,7 +16531,6 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz",
"integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -16659,7 +16642,6 @@
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
@@ -16951,8 +16933,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/redux-immutable": {
"version": "4.0.0",
@@ -18568,7 +18549,6 @@
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz",
"integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==",
"license": "MIT",
- "peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
@@ -18657,7 +18637,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -18766,6 +18745,18 @@
"node": ">=16"
}
},
+ "node_modules/tree-sitter": {
+ "version": "0.21.1",
+ "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.21.1.tgz",
+ "integrity": "sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "node-addon-api": "^8.0.0",
+ "node-gyp-build": "^4.8.0"
+ }
+ },
"node_modules/tree-sitter-json": {
"version": "0.24.8",
"resolved": "https://registry.npmjs.org/tree-sitter-json/-/tree-sitter-json-0.24.8.tgz",
@@ -19032,7 +19023,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -19968,7 +19958,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/public/devguard-structure-tables.png b/public/devguard-structure-tables.png
new file mode 100644
index 0000000..21c03f3
Binary files /dev/null and b/public/devguard-structure-tables.png differ
diff --git a/public/devguard-vulnerability-relations.png b/public/devguard-vulnerability-relations.png
new file mode 100644
index 0000000..c0f353b
Binary files /dev/null and b/public/devguard-vulnerability-relations.png differ
diff --git a/src/pages/explanations/architecture/database-schema.mdx b/src/pages/explanations/architecture/database-schema.mdx
index 9a87151..3de0b20 100644
--- a/src/pages/explanations/architecture/database-schema.mdx
+++ b/src/pages/explanations/architecture/database-schema.mdx
@@ -9,4 +9,105 @@ import PageContentComingSoon from '@/components/PageContentComingSoon'
# Database Schema
-
\ No newline at end of file
+This page gives an overview of DevGuard's database schema and explains the reasoning behind its structure. You'll find a map of the database layout, how the main table groups relate to each other, and the thinking behind key design decisions.
+
+---
+
+## Components
+
+DevGuard uses a **PostgreSQL** database. The PostgreSQL server runs in the same Docker container as the backend and communicates with it through **GORM** (Go's object-relational mapping library). The backend sends queries to PostgreSQL, which evaluates and executes them and returns the results.
+
+---
+
+## Table Groups
+
+DevGuard's tables fall into three broad categories:
+
+- **Structural Data** — describes your organisation's content hierarchy
+- **Vulnerability Database** — stores imported vulnerability and risk intelligence
+- **Repository Data** — holds vulnerability findings, component inventory, and dependency trees for each repository
+
+---
+
+### Structural Data
+
+This group contains everything needed to describe DevGuard's content hierarchy. It maps directly onto how you structure your work: from your top-level organisation down to individual build artifacts.
+
+
+
+- **Organizations** — the top-level container. An organisation holds members, compliance settings, and global configuration such as whether data is shared publicly.
+- **Projects** — logical groupings of related assets (repositories). Projects can be nested inside other projects, and releases are defined and monitored at this level.
+- **Releases** — group multiple artifacts into a named release so you can track vulnerability data across a whole release rather than per artifact. Releases can also contain other releases, allowing hierarchical release structures.
+- **Assets** — mirror your external repositories as closely as possible. Each asset holds repository-wide configuration, such as thresholds for automatic ticket creation and which external issue tracker to use.
+- **Asset Versions** — correspond to branches. Every asset has a default version (usually `main`) where most activity happens. Vulnerability data — dependency risks, license risks, and first-party findings — is attached at this level.
+- **Artifacts** — the smallest unit in the hierarchy. Vulnerability data is linked to artifacts through pivot tables to avoid duplicate entries across the same asset version. Risk history is also tracked at the artifact level.
+
+---
+
+### Vulnerability Database
+
+To detect risks in your repositories, DevGuard needs to compare your components against known vulnerabilities. This database is populated and refreshed multiple times a day from several external data sources that complement each other to provide richer, more accurate data.
+
+For details on how this data is imported and kept up to date, see [External Vulnerability Synchronization](./../vulnerability-management/external-vuln-sync/).
+
+---
+
+### Repository Data
+
+This is where the data specific to each of your repositories lives. It covers two closely related concerns: the vulnerability findings on each branch, and the component inventory that makes those findings — and your SBOMs — possible.
+
+#### Vulnerability Findings
+
+For each asset version, DevGuard stores three types of vulnerability records:
+
+- **Dependency Vulnerabilities** — vulnerabilities found in third-party packages your project depends on
+- **First-Party Vulnerabilities** — security issues found directly in your own source code
+- **License Risks** — cases where a dependency's license is not [OSI-approved](https://opensource.org/licenses)
+
+
+
+All three types share the same basic structure: each record belongs to a specific asset version, can reference a ticket in an external issue tracker (e.g. GitLab or Jira), and carries a state indicating whether the finding has been remediated.
+
+Beyond that shared structure, each type has its own specifics:
+
+**Dependency vulnerabilities** store the [package URL](https://github.com/package-url/purl-spec) of the affected component, the associated CVE, and the dependency path where the vulnerability was found. Tracking by path means each occurrence can be assessed and handled independently — a transitive dependency pulling in a vulnerable package via two different paths can be triaged separately for each.
+
+**First-party vulnerabilities** store the source file URI, the exact location of the affected code snippet (line and column numbers), and the full snippet text. This is what allows DevGuard to display the relevant code directly in the web interface. Each finding also records the commit, author, and timestamp of the change that introduced it.
+
+**License risks** flag licenses that are not OSI-approved. If a user needs to override the detected license — for example, because the detection was incorrect — that override is stored in a separate license overwrite table. Overrides are scoped to the organisation rather than to a single asset, so correcting a license detection once applies it everywhere within the org.
+
+#### Vulnerability Events
+
+Every vulnerability record can have multiple vulnerability events attached to it. Each event captures a state change over time — for example, a finding being triaged, accepted, or remediated. This event log is what powers the remediation history view in the interface. Events can be created manually by a user or automatically by a VEX rule firing.
+
+#### Components and Dependency Trees
+
+Alongside the vulnerability findings, DevGuard also stores a full picture of what each asset version is made of. This is the foundation for both dependency management and SBOM generation.
+
+The **components** table holds an entry for each known package across the entire DevGuard instance, identified primarily by its package URL. Each entry carries metadata such as license, component type, and publication date. Components are shared instance-wide rather than owned by a single repository, so the same package appearing in multiple projects is only stored once.
+
+The **component dependencies** table uses these components to build a dependency tree for each asset version. This serves two purposes: fast lookups to check whether a component is present in a given asset version, and on-the-fly SBOM generation. Rather than storing a static SBOM file on disk, DevGuard assembles it from the dependency tree at request time, so it always reflects the current state of the branch.
+
+---
+
+## Design Choices
+
+Some aspects of DevGuard's schema are worth explaining explicitly, because they look unusual at first glance but follow a deliberate logic.
+
+### Deterministic IDs
+
+Several tables — notably the vulnerability tables — use text primary keys with no auto-generated default. Rather than assigning a random UUID on insert, the application calculates the ID before writing the record by hashing the columns that uniquely identify that finding (the package URL, the CVE, and the dependency path for a dependency vulnerability, for example).
+
+The benefit is that the same logical finding always produces the same ID. When a new scan comes in, DevGuard can check whether a vulnerability already exists simply by computing its ID and looking it up — no full-table scan, no separate deduplication step. If the record exists, it updates it; if it doesn't, it inserts it. This keeps rescanning cheap and predictable.
+
+### Composite Natural Keys
+
+Not every table uses a single-column ID. Asset versions use a composite primary key of `(name, asset_id)` — the branch name together with the asset it belongs to. This reflects a straightforward reality: branch names are only unique within an asset, not globally. A `main` branch in one repository has nothing to do with `main` in another.
+
+Because artifact identity depends on which asset version it belongs to, the key cascades: artifacts carry `(artifact_name, asset_version_name, asset_id)` as their primary key. Any table that references an artifact must include all three columns. This is intentional — it keeps relationships explicit and allows the database to enforce referential integrity directly, rather than relying on application code to do so.
+
+### Pivot Tables for Vulnerability Deduplication
+
+Vulnerability findings are stored at the asset version level, but they also need to be associated with specific artifacts. The naive approach — copying a finding for every artifact that contains it — would create a lot of redundant rows, since most artifacts in a given asset version share the same dependencies.
+
+Instead, DevGuard uses pivot tables (`artifact_dependency_vulns` and `artifact_license_risks`) that store only the relationship between an artifact and a finding, not the finding data itself. A vulnerability is written once, and the pivot table records which artifacts are affected. Rescanning a branch with many similar artifacts therefore doesn't balloon the database — each distinct finding exists exactly once, and the associations are cheap rows in a join table.
\ No newline at end of file