diff --git a/app/services/danger_zone.py b/app/services/danger_zone.py
new file mode 100644
index 0000000..8309591
--- /dev/null
+++ b/app/services/danger_zone.py
@@ -0,0 +1,57 @@
+"""Irreversible bulk-delete of all domain (parts/taxonomy/fit) data.
+
+Leaves users, roles/permissions, refresh tokens, the audit log, and the
+ImportedPartConfig (OData connection settings) untouched.
+"""
+
+from sqlalchemy import delete
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.models.associations import augmented_part_imported_parts
+from app.models.attachment_point import AttachmentPoint
+from app.models.attachment_point_name import AttachmentPointName
+from app.models.augmented_part import AugmentedPart, AugmentedPartNote
+from app.models.clamp_connection import ClampConnection
+from app.models.fit import Fit, FitNote
+from app.models.imported_part import ImportedPart
+from app.models.iso_thread import IsoThread
+from app.models.manufacturer import Manufacturer
+from app.models.part_colour import PartColour
+from app.models.part_type import PartType
+from app.models.scope_info import ScopeInfo
+from app.models.scope_model import ScopeModel
+from app.models.static_connection import StaticConnection
+from app.models.sub_part_type import SubPartType
+from app.models.unc_thread import UncThread
+
+# Children before the RESTRICT-protected reference tables they point to.
+_TARGETS = [
+ ("Fit notes", FitNote),
+ ("Fits", Fit),
+ ("Augmented part notes", AugmentedPartNote),
+ ("Scope info", ScopeInfo),
+ ("Attachment points", AttachmentPoint),
+ ("Augmented part / imported part links", augmented_part_imported_parts),
+ ("Augmented parts", AugmentedPart),
+ ("Imported parts", ImportedPart),
+ ("Scope models", ScopeModel),
+ ("Attachment point names", AttachmentPointName),
+ ("ISO threads", IsoThread),
+ ("UNC threads", UncThread),
+ ("Clamp connections", ClampConnection),
+ ("Static connections", StaticConnection),
+ ("Manufacturers", Manufacturer),
+]
+
+
+async def purge_domain_data(db: AsyncSession) -> dict[str, int]:
+ """Delete every row from every domain table, in FK-safe order.
+
+ Returns a label -> rows-deleted map for reporting/audit purposes.
+ """
+ counts: dict[str, int] = {}
+ for label, target in _TARGETS:
+ result = await db.execute(delete(target))
+ counts[label] = result.rowcount or 0
+ await db.commit()
+ return counts
diff --git a/app/templates/_nav.html b/app/templates/_nav.html
index d1de06a..5245a66 100644
--- a/app/templates/_nav.html
+++ b/app/templates/_nav.html
@@ -145,6 +145,9 @@
{% if su or "audit_log:read" in p %}
{{ icon("list-checks") }} Audit Log
{% endif %}
+ {% if su %}
+ {{ icon("alert-circle") }} Danger Zone
+ {% endif %}
{% endif %}
diff --git a/app/templates/admin/_danger_zone_result.html b/app/templates/admin/_danger_zone_result.html
new file mode 100644
index 0000000..e45049d
--- /dev/null
+++ b/app/templates/admin/_danger_zone_result.html
@@ -0,0 +1,14 @@
+{% if total %}
+
+ Purge complete: {{ total }} row(s) deleted.
+
+
+ {% for label, n in counts.items() %}
+ {% if n %}
+
{{ label }}: {{ n }}
+ {% endif %}
+ {% endfor %}
+
+{% else %}
+
Nothing to purge — the database was already clean.
+{% endif %}
diff --git a/app/templates/admin/danger_zone.html b/app/templates/admin/danger_zone.html
new file mode 100644
index 0000000..7ed5d6d
--- /dev/null
+++ b/app/templates/admin/danger_zone.html
@@ -0,0 +1,32 @@
+{% extends "base.html" %}
+{% block title %}Danger Zone · {{ app_name }}{% endblock %}
+{% block body %}
+{% include "_nav.html" %}
+
+
Danger Zone
+
+
+
Purge database
+
+ Permanently deletes every part, taxonomy entry, attachment point, fit,
+ and imported part in the database — manufacturers, part colours, part
+ types, sub part types, scope models, ISO/UNC threads, clamp/static
+ connections, attachment point names, augmented parts, scope info,
+ attachment points, fits, fit notes, and imported parts.
+
+
+ Not deleted: users, roles & permissions, refresh
+ tokens, the audit log, and the OData connection configuration.
+
+
This cannot be undone. Take a database backup first if you might need this data again.