diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..4f9a9aeccb9f12978ba6fa3cc5a5a0a3b9689079
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,85 @@
+# SPDX-FileCopyrightText: OpenTalk GmbH <mail@opentalk.eu>
+#
+# SPDX-License-Identifier: EUPL-1.2
+
+---
+# For setup instructions see the backend onboarding doc.
+# https://git.opentalk.dev/a.weiche/the-knowledge/-/blob/main/backend-onboarding.md?ref_type=heads#pre-commit
+exclude: target/|.*\.snap|^api/docs/openapi\.yml$
+
+repos:
+  # These are "general" hooks provided by the pre-commit devs
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v3.2.0
+    hooks:
+      - id: trailing-whitespace
+      - id: end-of-file-fixer
+      - id: check-added-large-files
+  # A repo containing hooks for certain cargo subcommands
+  # Used for tools that don't bring official hooks
+  # Maybe we should create such a "dummy" project ourselves
+  - repo: https://github.com/AndrejOrsula/pre-commit-cargo
+    rev: 0.4.0
+    hooks:
+      - id: cargo-fmt
+      - id: cargo-clippy
+        args:
+          - --all-targets
+          - --all-features
+          - --tests
+          - --
+          - --deny
+          - warnings
+      - id: cargo-doc
+        args:
+          - --workspace
+          - --no-deps
+  - repo: https://github.com/EmbarkStudios/cargo-deny
+    rev: 0.18.2
+    hooks:
+      - id: cargo-deny
+        args:
+          - --all-features
+          - --workspace
+          - check
+          - --deny
+          - unmatched-skip
+          - --deny
+          - license-not-encountered
+          - --deny
+          - advisory-not-detected
+  # There currently is no official taplo hook, but this one got recommended in the corresponding issue
+  - repo: https://github.com/ComPWA/taplo-pre-commit
+    rev: v0.9.3
+    hooks:
+      - id: taplo-format
+      - id: taplo-lint
+  - repo: https://github.com/adrienverge/yamllint.git
+    rev: v1.29.0
+    hooks:
+      - id: yamllint
+        args:
+          - .
+  - repo: https://github.com/fsfe/reuse-tool
+    rev: v5.0.2
+    hooks:
+      - id: reuse-lint-file
+  - repo: https://github.com/markdownlint/markdownlint
+    rev: v0.13.0
+    hooks:
+      - id: markdownlint
+        args:
+          - --style
+          - .markdown_style.rb
+          - --warnings
+  # No official hooks for commitlint available, this one seems actively maintained
+  - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
+    rev: v9.22.0
+    hooks:
+      - id: commitlint
+        stages: [commit-msg]
+  - repo: https://github.com/daveshanley/vacuum
+    rev: v0.16.5
+    hooks:
+      - id: vacuum
+        files: ^api/.*\.(json|ya?ml)$
diff --git a/crates/kustos/src/internal/diesel_adapter.rs b/crates/kustos/src/internal/diesel_adapter.rs
index 5b7ca28f97731723bbeb674ec319cf6873386a1b..5672243327dc28023ae55d8854f9b3b24f0032bd 100644
--- a/crates/kustos/src/internal/diesel_adapter.rs
+++ b/crates/kustos/src/internal/diesel_adapter.rs
@@ -379,16 +379,16 @@ mod tests {
     const MODEL2: &str = r#"
     [request_definition]
     r = sub, dom, obj, act
-    
+
     [policy_definition]
     p = sub, dom, obj, act
-    
+
     [role_definition]
     g = _, _, _
-    
+
     [policy_effect]
     e = some(where (p.eft == allow))
-    
+
     [matchers]
     m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act"#;
 
diff --git a/crates/opentalk-db-storage/src/migrations/V12__require_oidc_issuer.sql b/crates/opentalk-db-storage/src/migrations/V12__require_oidc_issuer.sql
index 9d87874c47d4265902b2270cd0ff07d45ef2fa93..5f403ec403e3459e4c993898934e3a8e266200da 100644
--- a/crates/opentalk-db-storage/src/migrations/V12__require_oidc_issuer.sql
+++ b/crates/opentalk-db-storage/src/migrations/V12__require_oidc_issuer.sql
@@ -1,5 +1,5 @@
---- DELETE all user_group rows where a group without oidc issuer is referenced 
-DELETE FROM user_groups USING groups 
+--- DELETE all user_group rows where a group without oidc issuer is referenced
+DELETE FROM user_groups USING groups
 WHERE user_groups.group_id = groups.id AND groups.oidc_issuer IS NULL;
 
 --- DELETE all groups that have no oidc_issuer
diff --git a/crates/opentalk-db-storage/src/migrations/V23__tariffs.sql b/crates/opentalk-db-storage/src/migrations/V23__tariffs.sql
index 72afe539610f2c149305adcfe002f220601de897..5d09c1cf8efa101de773b5f06b6dd063f1d960b5 100644
--- a/crates/opentalk-db-storage/src/migrations/V23__tariffs.sql
+++ b/crates/opentalk-db-storage/src/migrations/V23__tariffs.sql
@@ -16,7 +16,7 @@ DO $$
 DECLARE
     DefaultTariffId UUID := gen_random_uuid();
 BEGIN
-    -- Create a new default tarrif all current users are assigned to 
+    -- Create a new default tarrif all current users are assigned to
     INSERT INTO tariffs VALUES (DefaultTariffId, 'OpenTalkDefaultTariff', DEFAULT, DEFAULT, '{}', '{}');
 
     ALTER TABLE users ADD COLUMN tariff_id UUID REFERENCES tariffs(id);
diff --git a/crates/opentalk-db-storage/src/migrations/V27__module_resources.sql b/crates/opentalk-db-storage/src/migrations/V27__module_resources.sql
index 477914a3d3038cacf3ac3cbfd645feb186a7db4e..6e655c466d19ac1b3c23211d3d21d86f9adb450a 100644
--- a/crates/opentalk-db-storage/src/migrations/V27__module_resources.sql
+++ b/crates/opentalk-db-storage/src/migrations/V27__module_resources.sql
@@ -16,7 +16,7 @@ DECLARE
 legal_vote RECORD;
 BEGIN
     FOR legal_vote IN SELECT * FROM legal_votes WHERE room IS NOT NULL
-    
+
     LOOP
         INSERT INTO module_resources
         VALUES
@@ -140,7 +140,7 @@ $$
 BEGIN
     IF NOT ot_jsonb_path_exists(target, path) THEN
         RAISE 'ot_invalid_path'
-            USING 
+            USING
                 ERRCODE = 'OTALK',
                 DETAIL = format('path "%s" does not exist', path);
     END IF;
@@ -199,7 +199,7 @@ BEGIN
         EXCEPTION
             -- catch any exception with error code 'OTALK' and overwrite the error details with the current array index
             WHEN SQLSTATE 'OTALK' THEN
-                GET STACKED DIAGNOSTICS 
+                GET STACKED DIAGNOSTICS
                     err_msg = MESSAGE_TEXT,
                     err_detail = PG_EXCEPTION_DETAIL;
 
@@ -252,4 +252,4 @@ BEGIN
 
     return regexp_split_to_array(path_string, '/');
 END;
-$$;
\ No newline at end of file
+$$;
diff --git a/crates/opentalk-db-storage/src/migrations/V31__invite_roles.sql b/crates/opentalk-db-storage/src/migrations/V31__invite_roles.sql
index e3c1c50a7ee955645118bad77c8edf52f5644e38..91cd8cda307d5159ccad28f925d1a111de67dd74 100644
--- a/crates/opentalk-db-storage/src/migrations/V31__invite_roles.sql
+++ b/crates/opentalk-db-storage/src/migrations/V31__invite_roles.sql
@@ -1,2 +1,2 @@
 CREATE TYPE invite_role AS ENUM ('user', 'moderator');
-ALTER TABLE event_invites ADD COLUMN role invite_role DEFAULT 'user' NOT NULL;
\ No newline at end of file
+ALTER TABLE event_invites ADD COLUMN role invite_role DEFAULT 'user' NOT NULL;
diff --git a/crates/opentalk-db-storage/src/migrations/V7__uuid_for_ids.sql b/crates/opentalk-db-storage/src/migrations/V7__uuid_for_ids.sql
index e3599aaa72f558efdffc63c54e6234d737f6050f..c93609bf513fc47630f6a793fdc89f044ea92393 100644
--- a/crates/opentalk-db-storage/src/migrations/V7__uuid_for_ids.sql
+++ b/crates/opentalk-db-storage/src/migrations/V7__uuid_for_ids.sql
@@ -13,14 +13,14 @@ ALTER TABLE users DROP CONSTRAINT users_pkey CASCADE;
 ALTER TABLE users ADD PRIMARY KEY (id);
 
 -- Build UNIQUE index over the oidc_issuer and oidc_sub.
--- These make queries faster trying to identify a user 
+-- These make queries faster trying to identify a user
 -- from an id or access token. Also the unique contraint
 -- over these both ensure that no user is duplicated
 CREATE UNIQUE INDEX users_oidc_sub_issuer_key ON users (oidc_issuer, oidc_sub);
 
--- Change groups from 
+-- Change groups from
 -- old - { id: TEXT }
--- to 
+-- to
 -- new - { id: UUID, id_serial: BIGSERIAL, issuer: TEXT, name: TEXT }
 -- where new.name = old.id
 ALTER TABLE groups RENAME id TO old_id;
@@ -66,9 +66,9 @@ ALTER TABLE rooms ALTER id SET DEFAULT gen_random_uuid();
 ALTER TABLE rooms ADD  PRIMARY KEY (id);
 
 -- Restore all foreign keys from dropping the rooms primary key
-ALTER TABLE sip_configs ADD CONSTRAINT sip_config_room_fkey FOREIGN KEY (room) REFERENCES rooms(id); 
-ALTER TABLE invites ADD CONSTRAINT invite_room_fkey FOREIGN KEY (room) REFERENCES rooms(id); 
-ALTER TABLE legal_votes ADD CONSTRAINT legal_votes_room_fkey FOREIGN KEY (room_id) REFERENCES rooms(id); 
+ALTER TABLE sip_configs ADD CONSTRAINT sip_config_room_fkey FOREIGN KEY (room) REFERENCES rooms(id);
+ALTER TABLE invites ADD CONSTRAINT invite_room_fkey FOREIGN KEY (room) REFERENCES rooms(id);
+ALTER TABLE legal_votes ADD CONSTRAINT legal_votes_room_fkey FOREIGN KEY (room_id) REFERENCES rooms(id);
 
 -- Change room's owner from type
 -- BIGINT REFERENCES users(id_serial)
@@ -126,12 +126,11 @@ ALTER TABLE legal_votes ALTER COLUMN initiator SET NOT NULL;
 
 -- Change the old stringified group id (string, now groups.name) in v0 and v1 in casbin_rule to the new group identifier
 UPDATE casbin_rule
-SET v1 = CONCAT('group:', subquery.id) 
+SET v1 = CONCAT('group:', subquery.id)
 FROM (SELECT id, name FROM groups) AS subquery
 WHERE casbin_rule.v1 like 'group:%' AND SPLIT_PART(casbin_rule.v1, ':', 3) = subquery.name;
 
 UPDATE casbin_rule
-SET v0 = CONCAT('group:', subquery.id) 
-FROM (SELECT id, name FROM groups) AS subquery      
+SET v0 = CONCAT('group:', subquery.id)
+FROM (SELECT id, name FROM groups) AS subquery
 WHERE casbin_rule.v0 like 'group:%' AND SPLIT_PART(casbin_rule.v0, ':', 3) = subquery.name;
-