# Migrations

> Audience: vendor implementation team replicating the schema in Azure PostgreSQL.
> Goal: list every SQL file in the repository, classify it, and document a recommended apply order.
> Priority: **Critical / Must do before handover**.

> ⚠️ **Do not apply these files to production without operator approval.** They are documented here for handover, schema introspection, and an Azure-side replay. Any direct execution against production must go through the operator's change-management process. See `docs/foundation/rls-audit-2026-04-21.md` for the most recent live audit.

---

## How to read this document

| Column            | Meaning                                                                                                       |
| ----------------- | ------------------------------------------------------------------------------------------------------------- |
| #                 | Apply order (1 = first).                                                                                       |
| File              | Relative path in the repo.                                                                                    |
| Class             | One of: `schema`, `module-schema`, `rls`, `cleanup`, `control-plane`, `audit`, `seed`, `rollback`, `review-only`. |
| Depends on        | Files that must already be applied. `—` means it is a top-level entry.                                        |
| RLS effect        | `enables` / `tightens` / `loosens (legacy)` / `none`.                                                         |
| Notes             | Operator + vendor notes specific to that file.                                                                |

`rollback` files are **never** applied as part of a forward migration — they exist to undo a corresponding forward file if a deploy goes wrong.

`review-only` files document a desired end-state but were not applied yet at the time this document was authored. The vendor must decide whether to apply them in Azure.

---

## 0. Bootstrap (recommended for vendor onboarding to a fresh DB)

> **Postgres version**: This bootstrap was generated by `pg_dump` from PostgreSQL 17. It uses `transaction_timeout` (PG 17+ feature) and `gin_trgm_ops` (requires `pg_trgm`). Apply on PostgreSQL 17 or newer. Azure Database for PostgreSQL Flexible Server PG 17 is the validated target.

> **Apply order** (verified 2026-05-09 against a clean Postgres 17 instance, `docker compose up`, 0 errors):

| #  | File                                       | Class     | Depends on | RLS effect | Notes |
| -: | ------------------------------------------ | --------- | ---------- | ---------- | ----- |
| 0a | `supabase/bootstrap/supabase-bootstrap-prerequisites.sql`     | bootstrap | —          | none       | **Apply FIRST** on a non-Supabase Postgres (vanilla, Azure Flexible Server, local Docker). Mocks Supabase platform layer: extensions (`pgcrypto`, `pg_trgm`, `uuid-ossp`), built-in roles (`anon`, `authenticated`, `service_role`), and the `auth` schema (`auth.uid()`, `auth.role()`, `auth.jwt()`, `auth.users`). On Supabase-hosted projects the platform provides these — skip this file. |
| 0b | `supabase/bootstrap/supabase-bootstrap-from-prod.sql`         | bootstrap | 0a (vanilla PG) | enables | **Recommended one-shot bootstrap** for a NEW Kadi-v2 (or vendor-target) Postgres project. Reproduces the full production schema (66 tables, 159 RLS policies, 90 functions, 217 indexes — verified 2026-05-09) from a `pg_dump --schema-only` snapshot of `Test-1`. After this, apply Section 7a closures (#47–#51), then seed-bearing migrations from Section 4. The individual files in Sections 1–6 below are kept for audit/forensic traceability — they describe the **incremental history** of how the schema was built. They will NOT reproduce the schema cleanly on a fresh DB (six tables — `master_data_types/values`, `holiday_*`, `user_holiday_preferences`, `app_settings` — have no `CREATE TABLE` in the repo). |

For step-by-step vendor onboarding with `docker compose`, see [`docs/foundation/clean-setup.md`](docs/foundation/clean-setup.md).

> **Drift-Befund 2026-08-04 (gemessen, nicht geschätzt).** Der Dump `0b` stammt vom
> 2026-05-07. Ein Vergleich der Tabellenlisten (`list_tables` gegen das Live-Projekt
> vs. die Ladekette aus `scripts/rls-test/load-schema.sh`) ergab zunächst **40
> Abweichungen**. Nach dem Nachziehen der sicherheitsrelevanten Migrationen (Intake
> V1 + öffentliche Einreichungs-Token, `cross-dept-visibility-r40`, sowie die beiden
> KAR-777-Fixes #85 `consultants-write-rls-admin-only` und #91
> `using-true-write-tranche-a`) sind es **30**:
>
> - 12 `missing_from_bootstrap` — die Module Agenda (6) und LSC-Workshop V2 (6)
> - 14 `obsolete_bootstrap_object` — 13 `cp_*` der am 2026-05-26 entfernten
>   Control-Plane, `shift_outputs`
> - 2 `expected_migration_delta` — die Meta-Tabellen aus #117, produktiv noch nicht
>   angewendet
> - 2 `unresolved_without_prod_dump` — `project_code_counters`, `project_id_sequence`
>
> Der laufende Stand steht in `docs/foundation/schema-drift/` und wird von
> `scripts/schema-drift/compare.mjs` als CI-Schritt bewacht: neue Abweichungen ODER
> weggefallene, die niemand nachgezogen hat, machen den `rls`-Job rot.
>
> Folge: Die RLS-Abnahme in CI belegt die Policies der Tabellen, die sie kennt — für
> die 12 fehlenden sagt ein grüner Lauf nichts, und die 14 überzähligen prüfen
> Regeln auf Tabellen, die niemand mehr hat. Die dauerhafte Lösung ist ein frischer
> `pg_dump --schema-only` aus Produktion als neues `0b`; das ist Operator-Arbeit
> (Prod-Zugang), siehe die Stop-Regel „prod state is operator-owned" in CLAUDE.md.
> Offener Punkt dazu: `production_schema_dump_validation_pending`.

## 1. Foundation

| #  | File                                                       | Class             | Depends on | RLS effect | Notes                                                                                  |
| -: | ---------------------------------------------------------- | ----------------- | ---------- | ---------- | -------------------------------------------------------------------------------------- |
| 1  | `supabase/bootstrap/supabase-schema.sql`                                      | schema            | —          | enables    | Source of truth for the core tables. Applied long before this branch.                   |

## 2. Control plane and platform metadata

| #  | File                                                      | Class             | Depends on    | RLS effect | Notes                                                                                                                          |
| -: | --------------------------------------------------------- | ----------------- | ------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------- |
| 2  | `supabase/migrations/supabase-migration-control-plane.sql`                    | control-plane     | 1             | enables    | Adds the `cp_*` tables (tenants, modules, plans, support sessions). ADR 002 colocation policy.                                  |
| 3  | `supabase/migrations/supabase-migration-platform-v2.sql`                      | control-plane     | 2             | tightens   | V2 platform shape (entitlements + provisioning).                                                                                |
| 4  | ~~`supabase-rls-cp-tables-authenticated-read.sql`~~       | rls (archived)    | —             | —          | **Archived 2026-05-11 (KAR-50)** — moved to `supabase-archive/`. Was the first attempt at module-gated layout fix; superseded by row 5 the same day. DO NOT APPLY. |
| 5  | `supabase/rls/supabase-rls-cp-tables-public-read.sql`                  | rls               | 3             | loosens    | **Canonical RLS for cp_* tables.** Grants SELECT to anon+authenticated on 7 metadata tables (tenants/plans/features/entitlements/overrides/branding/rollouts). Required by `createControlPlaneReadonlyClient()` which runs under the anon role. Other cp_* tables (cp_tenant_environments, cp_audit_events, cp_provisioning_*, cp_support_access_sessions, cp_owner_users) remain service_role-only. See file header for the multi-tenant caveat. |

## 3. Core modules (in DAG order)

| #  | File                                              | Class            | Depends on | RLS effect | Notes                                                                                          |
| -: | ------------------------------------------------- | ---------------- | ---------- | ---------- | ---------------------------------------------------------------------------------------------- |
| 6  | `supabase/migrations/supabase-migration-subprojects.sql`              | schema            | 1          | none       | Adds `parent_project_id` + indexes.                                                            |
| 7  | `MO-23/planning-tables.sql`                       | module-schema     | 1          | enables    | Planning module: `consultants`, `appointment_types`, `planning_projects`, `assignments`.        |
| 8  | `MO-23/planning-phase3.sql`                       | module-schema     | 7          | tightens   | Planning Phase 3 schema additions.                                                              |
| 9  | `MO-23/planning-phase4.sql`                       | module-schema     | 8          | tightens   | Planning Phase 4 schema additions.                                                              |
| 10 | `MO-24/project-form-phase1.sql`                   | module-schema     | 1          | none       | Project-form fields (responsibilities, statuses).                                              |
| 11 | `MO-24/assessment-phase1.sql`                     | module-schema     | 1          | enables    | Assessment Phase 1 (categories + questions tables).                                            |
| 12 | `MO-25/assessment-schema-v2.sql`                  | module-schema     | 11         | tightens   | Assessment v2 schema (multi-language).                                                          |
| 13 | `MO-25/repository-phase-arch.sql`                 | module-schema     | 1          | enables    | Repository module architecture tables.                                                          |
| 14 | `MO-25/repository-phase1.sql`                     | module-schema     | 13         | tightens   | Repository Phase 1.                                                                             |
| 15 | `MO-25/repository-phase4.sql`                     | module-schema     | 14         | tightens   | Repository Phase 4 (department / supplier links).                                              |
| 16 | `MO-26/wertstrom-schema.sql`                      | module-schema     | 1          | enables    | Value-stream-map tables.                                                                        |
| 17 | `MO-26/phase2.sql`                                | module-schema     | 16         | tightens   | VSM Phase 2.                                                                                    |
| 18 | `MO-26/user-permissions.sql`                      | module-schema     | 1          | tightens   | Module-level permission grants.                                                                  |
| 19 | `MO-27/assignments-origin-project.sql`            | module-schema     | 7          | tightens   | Adds `assignments.origin_project_id` (planning ↔ project link).                                |
| 20 | `MO-LSC/lsc-schema.sql`                           | module-schema     | 1          | enables    | LSC module: shifts, shift hours, measures.                                                      |
| 21 | `MO-OEE/oee-phase1.sql`                           | module-schema     | 1          | enables    | OEE module Phase 1.                                                                             |
| 22 | `MO-OEE/oee-phase3.sql`                           | module-schema     | 21         | tightens   | OEE Phase 3 additions (loss categories).                                                        |
| 23 | `supabase/migrations/supabase-migration-oee-vsm-permissions.sql`      | module-schema     | 16, 21     | tightens   | OEE + VSM permission rows.                                                                      |
| 24 | `supabase/migrations/supabase-migration-assessment-i18n.sql`          | module-schema     | 12         | none       | Assessment i18n lookup tables.                                                                  |
| 25 | `supabase/seed/assessment-i18n-zh.sql`                          | seed              | 24         | none       | Chinese assessment text (202 questions).                                                        |

## 4. Phase migrations

| #  | File                                              | Class       | Depends on | RLS effect | Notes                                                              |
| -: | ------------------------------------------------- | ----------- | ---------- | ---------- | ------------------------------------------------------------------ |
| 26 | `supabase/migrations/supabase-migration-placeholders.sql`             | schema      | 1          | none       | Placeholder text rows used by lookup tables.                        |
| 27 | `supabase/migrations/supabase-migration-phase3.sql`                   | schema      | 1          | tightens   | Phase 3 cross-module schema changes.                                |
| 28 | `supabase/migrations/supabase-migration-phase4.sql`                   | schema      | 27         | tightens   | Phase 4 cross-module schema changes.                                |
| 29 | `supabase/migrations/supabase-migration-phase5.sql`                   | schema      | 28         | tightens   | Phase 5 cross-module schema changes.                                |
| 30 | `supabase/migrations/supabase-migration-cleanup.sql`                  | cleanup     | 29         | tightens   | FK indexes + tightening passes from earlier audits.                 |

## 5. RLS enable + tightening

These files were applied incrementally as RLS rolled out across the schema. They should be applied in order on a fresh Azure replica.

| #  | File                                                              | Class | Depends on | RLS effect | Notes                                                                                                                            |
| -: | ----------------------------------------------------------------- | ----- | ---------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------- |
| 31 | `supabase/rls/supabase-rls-master-data-types-enable.sql`                       | rls   | 1          | enables    | Enables RLS on `master_data_types`.                                                                                              |
| 32 | `supabase/rls/supabase-rls-master-data-values-enable.sql`                      | rls   | 31         | enables    | Enables RLS on `master_data_values`.                                                                                             |
| 33 | `supabase/rls/supabase-rls-master-data-audit-log-enable.sql`                   | rls   | 31         | enables    | Enables RLS on `master_data_audit_log`.                                                                                          |
| 34 | `supabase/rls/supabase-rls-department-master-data-enable.sql`                  | rls   | 1          | enables    | Enables RLS on `department_master_data`.                                                                                         |
| 35 | `supabase/rls/supabase-rls-holiday-calendars-enable.sql`                       | rls   | 1          | enables    | Enables RLS on `holiday_calendars`.                                                                                              |
| 36 | `supabase/rls/supabase-rls-holiday-entries-enable.sql`                         | rls   | 35         | enables    | Enables RLS on `holiday_entries`.                                                                                                |
| 37 | `supabase/rls/supabase-rls-user-holiday-preferences-enable.sql`                | rls   | 35         | enables    | Enables RLS on `user_holiday_preferences`.                                                                                       |
| 38 | `supabase/rls/supabase-rls-email-queue-enable.sql`                             | rls   | 1          | enables    | Enables RLS on `email_queue`.                                                                                                    |
| 39 | `supabase/rls/supabase-rls-lsc-shift-hours-enable.sql`                         | rls   | 20         | enables    | Enables RLS on `lsc_shift_hours` (broad initial policy — see notes in `docs/DATABASE_SCHEMA.md` §5).                              |
| 40 | `supabase/rls/supabase-rls-test-1-project-type-assignments.sql`                | review-only | 1     | tightens   | Replacement for the broad `pta_*` policies. Filename is "test"; confirm with operator before applying in Azure.                  |
| 41 | `supabase/rls/supabase-rls-test-2-project-suppliers.sql`                       | review-only | 1     | tightens   | Replacement for the broad `ps_*` policies. Same caveat as #40.                                                                   |
| 42 | `supabase/rls/supabase-rls-test-3-lsc-shift-hours.sql`                         | review-only | 39    | tightens   | Replacement for the initial broad `lsc_shift_hours` policy. Confirm before applying.                                              |
| 43 | `supabase/migrations/supabase-migration-rls-phase1-cleanup.sql`                       | rls   | 1          | tightens   | RLS Phase 1 cleanup — applied to operator's Supabase as part of PR #35.                                                          |
| 44 | `supabase/migrations/supabase-migration-rls-phase1a-cleanup.sql`                      | rls   | 43         | tightens   | RLS Phase 1a — 9 tables, excludes `user_profiles` (recursion bug, see #45).                                                      |
| 45 | `supabase/migrations/supabase-migration-rls-phase1b-user-profiles.sql`                | rls   | 44         | tightens   | RLS Phase 1b — `up_own` policy rewritten to fix infinite-loop self-join on `user_profiles`. Applied via PR #35.                   |

## 6. Audits

| #  | File                                  | Class | Depends on | RLS effect | Notes                                                                                                                  |
| -: | ------------------------------------- | ----- | ---------- | ---------- | ---------------------------------------------------------------------------------------------------------------------- |
| 46 | `supabase/bootstrap/supabase-audit-phase1-2.sql`         | audit | 1          | none       | Read-only audit query set. Output is consumed by `docs/foundation/rls-audit-2026-04-21.md`. Safe to run in any DB.     |

## 7a. Kadi-v2 vendor-ready closure (new — 2026-05-07)

These five migrations close gaps surfaced by the 2026-05-07 full-stack
analysis (full-stack security audit, 2026-05-07). They are the
**ready-to-apply COMMIT-version** of either previously review-only closure
files (with `BEGIN; ... ROLLBACK;`) or, in the case of the OEE and planning
gaps, freshly authored. They are the recommended apply set for a fresh
Kadi-v2 deployment after sections 1–6 have been replayed.

| #  | File                                                              | Class | Depends on | RLS effect | Notes                                                                                                                            |
| -: | ----------------------------------------------------------------- | ----- | ---------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------- |
| 47 | `supabase/migrations/supabase-migration-kadi-v2-rls-oee.sql`                          | rls   | 21, 22     | tightens   | Replaces `MO-OEE/oee-phase1.sql` lax CRUD on `oee_records` + `oee_loss_categories` with `_own` (created_by) + `_admin`. New gap. |
| 48 | `supabase/migrations/supabase-migration-kadi-v2-rls-planning.sql`                     | rls   | 7          | tightens   | Replaces `MO-23/planning-tables.sql` lax CRUD on `planning_projects` (admin-only writes) and `assignments` (`_own` + `_admin`). |
| 49 | `supabase/migrations/supabase-migration-kadi-v2-rls-pta-ps.sql`                       | rls   | 1          | tightens   | COMMIT-version of `supabase/rls/supabase-rls-closure-{pta,ps}-ownership.sql`. Replaces `pta_*` and `ps_*` lax policies.                      |
| 50 | `supabase/migrations/supabase-migration-kadi-v2-rls-deferred-lax.sql`                 | rls   | 1, 13      | tightens   | COMMIT-version of `supabase/rls/supabase-rls-closure-deferred-lax-policies.sql`. Tightens `appointment_types`, `document_*`.                  |
| 51 | `supabase/migrations/supabase-migration-kadi-v2-key-vault-column.sql`                 | control-plane | 2  | none       | Adds `cp_tenant_environments.key_vault_secret_name` (nullable). Forward-compat for Azure Key Vault. No secret movement.          |
| 52 | `supabase/migrations/supabase-migration-kadi-v2-fk-coverage-indexes.sql`              | cleanup       | 2, 3, 7    | none       | **KAR-48** — 6 hot-path FK-coverage indexes (CONCURRENTLY, IF NOT EXISTS) on `assessment_responses(question_id,updated_by)`, `assignments(created_by,updated_by)`, `cp_subscriptions(current_plan_id)`, `cp_tenants(template_id)`. Apply *outside* a transaction (psql without `-1`). NOT mountable via docker-compose init (CONCURRENTLY incompatible with implicit txn). On local Docker setups, copy/paste statements after bootstrap. |
| 53 | `supabase/migrations/supabase-migration-index-dedup.sql`                              | cleanup       | 2          | none       | **KAR-49** — 8 redundante Indizes droppen (DROP INDEX CONCURRENTLY). Behält jeweils selektivste/canonical Variante: `idx_up_role`, `idx_user_profiles_status` (partial), `idx_user_audit_log_actor` (partial+composite), `idx_user_audit_log_target_user` (partial), `idx_lsc_measures_project_id`, `idx_lsc_shifts_project_id`. Apply *outside* einer Transaction (analog Eintrag 52). Pre-Apply: `pg_stat_user_indexes`-Check empfohlen. |
| 54 | `supabase/migrations/supabase-migration-medium-fk-coverage-indexes.sql`               | cleanup       | 2, 3, 7    | none       | **KAR-87** — 23 MEDIUM-FK-Coverage Indexes (CONCURRENTLY, IF NOT EXISTS): master-data creator/lookup, projects value-lookups, misc creator/updater, cp_* control-plane. Ergänzt Row 52 (HIGH FKs). Apply *outside* einer Transaction (psql ohne `-1`). NICHT mountable via docker-compose init. |
| 55 | `supabase/migrations/supabase-migration-lsc-workshop-v2-schema.sql`                   | feature       | 2, 3, 7    | additive   | **LSC PR-A** — LSC Workshop V2 Schema: `qaf_uploads` zu Version-Stack erweitert (`is_active`, `source_file_hash`, `version`); version_type CHECK additive (alte + 4 neue Werte prequote/awarded/current_with_claim/current); `process_steps` mit 9 Workshop-Stammdaten-Spalten ergänzt; 7 neue Tabellen (`process_step_observations`, `_optimizations`, `qaf_summary`, `qaf_process_mapping`, `lsc_tooltips`, `lsc_color_config`, `observation_potentials`); RLS analog `lsc_measures`-Pattern; Default-Seed `lsc_color_config` mit BMW-CI-Farben. Apply *outside* einer Transaction. KEIN `tenant_id`-FK (V1 BMW Stand-alone, Multi-Tenant später). |
| 56 | `supabase/migrations/supabase-migration-oee-v2-schema.sql`                            | feature       | 2, 3, 7    | additive   | **OEE PR-A** — OEE V2 Schema additive (kein Rename `oee_records`): 9 neue Spalten auf `oee_records` (`name`, `is_orphan`, `orphan_*`, `soft_deleted_at`, `period_*`, `analysis_date`); 4 neue Tabellen (`oee_stations`, `oee_measurements`, `oee_comparison_links`, `oee_orphan_reminders`); Cross-Supplier-Trigger auf `oee_comparison_links` (DB-Constraint via PL/pgSQL); RLS analog `lsc_measures`-Pattern für 4 neue Tabellen; Sub-Projekt-FK via `projects` Self-Join. Apply *outside* einer Transaction. KEIN `tenant_id`-FK (V1 BMW Stand-alone). RLS-Fix für `oee_records.auth_*`-Bug folgt in separatem PR. |
| 57 | `supabase/migrations/supabase-migration-intake-v1-schema.sql`                         | feature       | 1, 7       | enables    | **Intake PR-A** — Auftragseingang V1 Schema: 9 neue Tabellen (`fachbereich_users`, `intakes`, `intake_status_history`, `intake_field_definitions`, `intake_field_options`, `intake_field_values`, `consultant_availability`, `project_phase_config`, `intake_id_sequence`); `generate_intake_id()` SECURITY DEFINER mit Advisory Lock (Format `A-YYYY-NNN`); `projects.source_intake_id` additive FK; RLS-Policies pro Tabelle (fachbereich-own + consultant/admin-all); Default-Seed `project_phase_config` mit BMW-CI-Farben. Apply *outside* einer Transaction. KEIN `tenant_id`-FK (V1 BMW Stand-alone). DnD-Library-Entscheidung in Kanban-UI-PR. |
| 58 | `supabase/migrations/supabase-migration-intake-v1-seed-field-definitions.sql`         | seed          | 57         | none       | **Intake PR-B** — Seed für `intake_field_definitions` + `intake_field_options`: ~50 Field-Definitions in 9 Blöcken (BMW-Auftragsklärungs-Checkliste V2.0) + ~37 Options für Dropdowns/Multi-Selects. Conditional-Visibility via `visible_if` jsonb (Block 4 cost_engineering nur bei lsc_workshop; Block 6 nur bei lsc_workshop/invest_workshop; Block 7 nur bei fabrikanalyse/lieferantenbefaehigung). Idempotent via `ON CONFLICT DO NOTHING`. Apply in Transaction OK. |
| 59 | `supabase/migrations/supabase-migration-oee-records-rls-tightening.sql`               | rls           | 56         | tightens   | **OEE RLS-Fix** — Sicherheits-Tightening für `oee_records` + `oee_loss_categories`: ersetzt `auth_*` Policies (alle `USING(true)`, Sicherheits-Gap aus DB-Audit) durch `own` (project_id JOIN auf projects.user_id), `orphan_own` (is_orphan=true + created_by=auth.uid()) und `admin` (current_user_role). Rollback-File `supabase/migrations/supabase-migration-oee-records-rls-tightening-rollback.sql` (falls App-Code RLS-blocked wird). Apply in Transaction. |

### Apply procedure

1. Verify the target Supabase project is `kadi-v2` (project ref + DB URL).
2. For each file 47–51, paste into Supabase SQL Editor.
3. Run inside an explicit `BEGIN; ... ROLLBACK;` first to inspect
   `pg_policies` output. The bottom verification SELECT in each file
   shows the expected policy snapshot.
4. Run with `COMMIT` only after the smoke-test signs off.
5. After all five are applied, re-run `supabase/bootstrap/supabase-audit-phase1-2.sql`
   (file #46) and reconcile the output against
   `docs/foundation/rls-audit-2026-04-21.md`.

### Why these are separate files instead of editing the originals

The originals — `supabase/rls/supabase-rls-closure-{pta,ps}-ownership.sql`,
`supabase/rls/supabase-rls-closure-deferred-lax-policies.sql`, and
`supabase/migrations/supabase-migration-key-vault-secret-name-review.sql` — remain in place
unchanged. They are the operator's review-only artefacts (BEGIN /
ROLLBACK pattern with the inspection-first instructions in the header).
The Kadi-v2 closure files duplicate the body with `COMMIT`, so a vendor
running through MIGRATIONS.md does not have to manually flip ROLLBACK
to COMMIT in five places.

### Apply-order risk on a fresh database (Finding #SEC-MOD-1)

Running module schemas (`MO-OEE/oee-phase1.sql`, `MO-23/planning-tables.sql`,
`MO-24/assessment-phase1.sql`, `MO-25/repository-phase-arch.sql`,
`MO-26/wertstrom-schema.sql`) on a fresh Postgres in the documented
order **recreates** the lax / phantom / bypass policies that later
cleanup migrations drop. The vendor's intended end-state is **the
post-cleanup policy set, not the per-module bootstrap set.** For a
fresh Kadi-v2 install, the apply order is therefore:

```
sections 1–6  →  sections 7–7a  →  audit (#46)
```

Skipping the cleanup tail leaves the database in a **measurably
unsafe** state (cross-tenant write surface on OEE, planning, and
documents). Operators replaying the schema in any other order should
re-run the audit and reconcile.

### Multi-tenant caveat for `cp_*` public-read fallback (Finding #SEC-CP-1)

`supabase/rls/supabase-rls-cp-tables-public-read.sql` (file #5) grants
`anon, authenticated SELECT USING(true)` on seven `cp_*` metadata
tables. **This is acceptable for a single-tenant Kadi-v2 deployment
only.** Before provisioning a second live tenant, the policies on
`cp_tenants`, `cp_plans`, `cp_features`, `cp_plan_feature_entitlements`,
`cp_tenant_feature_overrides`, `cp_branding_profiles`, and
`cp_release_rollouts` must be replaced with tenant-scoped variants
(JWT tenant claim, `cp_tenant_users` mapping, or a non-`anon`
readonly channel). Tracked in `docs/foundation/follow-ups.md` item #15.

---

## 7b. Control-plane teardown (single-tenant BMW — NOT YET APPLIED)

> **Status**: FILE ONLY — 2026-05-26. Not applied to any DB. See pre-conditions
> in the file header (iOS audit + key rotation + BMW sign-off required first).

| #  | File                                                                  | Class         | Depends on | Notes                                                                                                                                    |
| -: | --------------------------------------------------------------------- | ------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| 60 | `supabase/migrations/supabase-migration-control-plane-teardown.sql`                       | control-plane | 2, 3, 5    | Drops all 18 `cp_*` tables + their RLS policies. Idempotent (`IF EXISTS`). **⚠️ cp_tenant_environments may contain plaintext keys — rotate before applying.** Read file header carefully. |
| R4 | `supabase/migrations/supabase-migration-control-plane-teardown-rollback.sql`              | rollback      | 60         | Recovery instructions only — no DDL (data not restorable without a DB snapshot). Points to canonical schema sources for table recreation. |

### Pre-apply checklist (mandatory)

1. [ ] Code deploy (`refactor/remove-control-plane`) has been live ≥ 1 sprint with no incidents.
2. [ ] iOS team confirmed no `cp_*` reads in iOS app queries.
3. [ ] `SELECT id, tenant_id FROM cp_tenant_environments;` row count verified; if non-zero, rotate all listed Supabase keys first.
4. [ ] BMW branding confirmed in `app/globals.css` + `DESIGN_SYSTEM.md` (kanonische Token-Quelle; das frühere `branding/design-system.ts` wurde 2026-07-03 entfernt).
5. [ ] Run with `ROLLBACK` first (inspect verification SELECT), then re-run with `COMMIT` after BMW sign-off.

---

## 7c. Data-model corrections (Batch 2/3 — 2026-05-26)

These five migrations close data-model gaps surfaced during the Batch 2/3 master-data
and reporting consolidation audit. All are **NOT YET APPLIED** to any environment;
apply in the order shown after section 7a is confirmed green.

| #  | File                                                                        | Class   | Depends on | RLS effect | Notes                                                                                                                                                                     |
| -: | --------------------------------------------------------------------------- | ------- | ---------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 61 | `supabase/migrations/supabase-migration-master-data-types-kadi-core-seed.sql`                   | seed    | 1          | none       | Seeds core `master_data_types` rows and KIFAG_AREA values. Idempotent (`ON CONFLICT DO NOTHING`). Safe to apply in a transaction. Addresses audit finding F-02 (P0). |
| 62 | `supabase/migrations/supabase-migration-master-data-audit-log-merge-action.sql`                 | schema  | 31         | none       | Adds `action='merge'` to the CHECK constraint on `master_data_audit_log`. Required for the master-data deduplication workflow. Rollback reverts the CHECK. Addresses audit finding F-01 (P0). |
| 63 | `supabase/migrations/supabase-migration-oee-records-project-scoped-unique.sql`                  | schema  | 21, 56     | none       | Adds a partial unique index on `oee_records(project_id, analysis_date)` to prevent duplicate OEE entries per project/date. Safe to apply outside a transaction (CONCURRENTLY). Rollback drops the index. Addresses audit finding F-05 (P1). |
| 64 | `supabase/migrations/supabase-migration-requesting-department-real-codes.sql`                   | seed    | 24         | none       | Replaces placeholder REQUESTING_DEPARTMENT values with real BMW department codes. Idempotent (UPDATE with WHERE). Rollback restores placeholders. Addresses audit finding F-06 (P1). |
| R5 | `supabase/migrations/supabase-migration-control-plane-teardown-rollback.sql`                    | rollback | 60        | —          | See Section 7b. Recovery instructions only; no DDL.                                                                                                                       |
| R6 | `supabase/migrations/supabase-migration-master-data-audit-log-merge-action-rollback.sql`        | rollback | 62        | —          | Reverts the audit-log CHECK extension. Only safe if no `action='merge'` rows exist.                                                                                      |
| R7 | `supabase/migrations/supabase-migration-oee-records-project-scoped-unique-rollback.sql`         | rollback | 63        | —          | Drops the partial unique index added by file 63.                                                                                                                          |
| R8 | `supabase/migrations/supabase-migration-master-data-types-kadi-core-seed-rollback.sql`          | rollback | 61        | —          | Removes the seeded rows added by file 61 (DELETE WHERE source='kadi-core-seed').                                                                                          |
| R9 | `supabase/migrations/supabase-migration-requesting-department-real-codes-rollback.sql`          | rollback | 64        | —          | Restores placeholder REQUESTING_DEPARTMENT values.                                                                                                                        |

## 7d. Feature additions (Batch 7 — 2026-05-26)

NOT YET APPLIED. Depend on `supabase/bootstrap/supabase-bootstrap-from-prod.sql` (which already includes `department_master_data`).

| #  | File                                                                                             | Class   | Depends on | RLS effect | Notes                                                                                                                                                                   |
| -: | ------------------------------------------------------------------------------------------------ | ------- | ---------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 65 | `supabase/migrations/supabase-migration-projects-department-and-reporting-flag.sql`                                  | schema  | 1          | none       | Adds `projects.department_id` (UUID FK → `department_master_data`) and `projects.reporting_relevant` (boolean NOT NULL DEFAULT true). Index on `department_id`. Addresses audit findings F-002/F-003 (KAR-632). Idempotent (ADD COLUMN IF NOT EXISTS). Safe to apply in a transaction. |
| R10 | `supabase/migrations/supabase-migration-projects-department-and-reporting-flag-rollback.sql`                        | rollback | 65        | —          | Drops the index and both columns. Only safe when no app code referencing these columns is live.                                                                          |
| 66 | `supabase/migrations/supabase-migration-lsc-workshop-v2-schema.sql`                                                  | schema  | 1          | adds 7 RLS tables | LSC-Workshop V2: erweitert `qaf_uploads` (Versions-Stack) + `process_steps` (Workshop-Stammdaten), 7 neue Tabellen (`process_step_observations`, `process_step_optimizations`, `qaf_summary`, `qaf_process_mapping`, `lsc_tooltips`, `lsc_color_config`, `observation_potentials`) mit RLS + Farb-Seed. KAR-698 Welle 2c. **SQL-Editor: CONCURRENTLY-freie Editor-Variante nutzen** (Original hat `CREATE INDEX CONCURRENTLY`). Idempotent. |
| 67 | `supabase/migrations/supabase-migration-lsc-tooltips-fields.sql`                                                     | schema + seed | 66    | none       | Adds `lsc_tooltips.formula` + `.example` und seedet 14 Workshop-Feld-Hinweise. KAR-700 (admin-editierbare Feld-Hinweise). Editor-tauglich (kein CONCURRENTLY), idempotent (`ADD COLUMN IF NOT EXISTS`, Seed `ON CONFLICT DO NOTHING` — Admin-Edits bleiben). |
| 68 | `supabase/migrations/supabase-migration-rls-read-all-for-select.sql`                                                 | rls-fix | 66, intake-v1 | restricts 5 read_all policies to FOR SELECT | **Security-Fix (KAR-701):** stellt 5 `*_read_all`-Policies (`lsc_tooltips`, `lsc_color_config`, `intake_field_definitions`, `intake_field_options`, `project_phase_config`) von versehentlichem `FOR ALL` auf `FOR SELECT` um — vorher konnte jeder authenticated User via PostgREST schreiben. `*_admin_write`-Policies unverändert. Editor-tauglich, idempotent (`DROP POLICY IF EXISTS`). Kein non-admin-Schreibpfad im Code betroffen (verifiziert). |
| 69 | `supabase/migrations/supabase-migration-field-hints-batch1.sql`                                                      | seed    | 67         | none       | Seedet 13 Feld-Hinweise für Schicht (`shift.*`, 9) + Stoppuhr (`stopwatch.*`, 4) in `lsc_tooltips`. KAR-700 Rollout Batch 1. Editor-tauglich (nur INSERT), idempotent (`ON CONFLICT DO NOTHING` — Admin-Edits bleiben). |
| 70 | `supabase/migrations/supabase-migration-field-hints-oee.sql`                                                         | seed    | 67         | none       | Seedet 26 OEE-Feld-Hinweise (`oee.*`) in `lsc_tooltips` — migriert die bisher hardcodierten Tooltip-Texte des OEE-Rechners (admin-editierbar). KAR-700. Idempotent (`ON CONFLICT DO NOTHING`). |
| 71 | `supabase/migrations/supabase-migration-field-hints-projektanlage.sql`                                               | seed    | 67         | none       | Seedet 35 Projektanlage-Feld-Hinweise (`project.*`). KAR-700. Einige kundeninterne Felder mit Basis-Hinweis (`-- [intern: prüfen]`, vom Admin zu verfeinern). Idempotent (`ON CONFLICT DO NOTHING`). |
| 72 | `supabase/migrations/supabase-migration-planning-core-project-fk.sql`                                                | schema  | 1          | none       | KAR-698 A4: `planning_projects.core_project_id` (nullable FK → `projects.id`, ON DELETE SET NULL) + Index — verbindet Kalender-/Einsatzplanung mit den Projekten. Additiv, kein Backfill, idempotent (ADD COLUMN/INDEX IF NOT EXISTS), Editor-tauglich. Promote-Flow (UI) = separater späterer Schritt. Rollback: `…-planning-core-project-fk-rollback.sql`. |
| 73 | `supabase/migrations/supabase-migration-intake-token-revoke.sql`                                                     | schema + rpc | intake-v1-public-submission-tokens | none | KAR-698 C3: `intake_submission_tokens.is_revoked` + `revoke_intake_submission_token`-RPC (Consultant/Admin-only, Gate 1:1 aus `create_intake_submission_token`). `get_intake_submission_form` + `submit_intake_via_token` um is_revoked-Check ergänzt (sonst 1:1 übernommen). Partial-Index um is_revoked erweitert. Idempotent (ADD COLUMN IF NOT EXISTS, CREATE OR REPLACE), Editor-tauglich, `BEGIN/COMMIT`. Rollback: `…-intake-token-revoke-rollback.sql` (⚠️ RPCs erst zurücksetzen, dann Spalte droppen). |
| 74 | `supabase/migrations/supabase-migration-intake-requesting-department-other.sql`                                      | schema  | intake-v1-schema | adds 'OTHER' | KAR-698 C5: `intakes.requesting_department`-CHECK um `'OTHER'` erweitert (Angleich an `intake_submission_tokens.department` + `fachbereich_users.department`). Vorher schlug ein Token-Submit mit department='OTHER' am CHECK fehl. `BEGIN/COMMIT`, idempotent (DROP CONSTRAINT IF EXISTS + ADD). **Vor dem Run Constraint-Namen verifizieren** (siehe Skript-Header). Rollback: `…-intake-requesting-department-other-rollback.sql`. |
| 75 | `supabase/migrations/supabase-migration-planning-supplier-fk.sql`                                                    | schema  | 1          | none       | KAR-698 A1 (Teil 1): FK `planning_projects.supplier_id` → `supplier_master_data(id)` als **NOT VALID** (kein Lock/Scan; neue Zeilen geprüft). **Schritt 0** (Konsistenz-Check) + **Schritt 2** (`VALIDATE CONSTRAINT` nach Check=0) im Skript-Header. Idempotent (DO-Block). Rollback: `…-planning-supplier-fk-rollback.sql`. |
| 76 | `supabase/migrations/supabase-migration-assessments-supplier-fk.sql`                                                 | schema + backfill | 1, projects | none | KAR-698 A1 (Teil 2): `assessments.supplier_id` (nullable FK → `supplier_master_data`) + Index + **deterministischer Backfill via project_id → projects.supplier_id** (project_id ist NOT NULL, kein Name-Matching). `supplier_name` bleibt (Code-Umstieg = separat). Idempotent (ADD IF NOT EXISTS, Backfill nur auf NULL). `BEGIN/COMMIT`. Rollback: `…-assessments-supplier-fk-rollback.sql`. |
| 77 | `supabase/migrations/supabase-migration-intake-department-fk.sql`                                                    | schema + backfill | intake-v1-schema | none | KAR-698 A2 (Teil 2): `intakes.department_id` + `fachbereich_users.department_id` (nullable FK → `department_master_data`) + Indizes + Backfill via `department_code`-Match. `'OTHER'` + Codes ohne Stammdaten-Eintrag → NULL (bewusst, OTHER ist kein echtes Department). Text-Spalten bleiben parallel. Idempotent (ADD IF NOT EXISTS, DO-Block, Backfill nur auf NULL). `BEGIN/COMMIT`. Rollback: `…-intake-department-fk-rollback.sql`. |
| 78 | `supabase/migrations/supabase-migration-a3-s0s1-user-consolidation.sql`                                              | schema + view | 1, intake-v1-schema | adds 'extern' to consultants.role | KAR-698 A3 (S0/S1, KAR-702): **S1** consultants.role-CHECK um `'extern'` erweitert (Angleich an `roles.code`). **S0** read-only View `v_all_users` (SECURITY INVOKER — RLS der Basistabellen greift, kein Zusatz-Zugriff) vereint consultants + user_profiles + fachbereich_users. **Nicht** enthalten: S3 FK-Umbiegung (hochriskant, eigene Session). Idempotent (DROP/ADD CONSTRAINT, CREATE OR REPLACE VIEW). `BEGIN/COMMIT`. Rollback: `…-a3-s0s1-user-consolidation-rollback.sql`. |
| 90 | `supabase/migrations/supabase-migration-agenda-participant-department-fk.sql`                                       | schema + backfill | agenda-schema, 1 | none | KAR-704 A2 (Rest): `agenda_participant.department_id` (nullable FK → `department_master_data`, ON DELETE SET NULL) + Index + zweistufiger Backfill (case-insensitive Match auf `department_code`, dann auf eindeutige `department_name`). Freitext-Werte ohne Stammdaten-Treffer bleiben NULL (Info-Check im Skript-Header listet sie). Text-Spalte `department` bleibt parallel (Export/Auto-Import). Idempotent (ADD IF NOT EXISTS, DO-Block, Backfill nur auf NULL). `BEGIN/COMMIT`, Editor-tauglich. Rollback: `…-agenda-participant-department-fk-rollback.sql`. |
| 91 | `supabase/migrations/supabase-migration-using-true-write-tranche-a.sql`                                              | rls-fix | bootstrap, `current_user_role()` | tightens write to admin | **Security (KAR-777 R43/R44, Tranche A):** entfernt die legacy `USING(true)`-Write-Policies auf 5 Tabellen, deren Writes ausschließlich admin-gated UI oder service_role sind: `appointment_types`, `email_queue`, `master_data_import_jobs` (→ admin-only ersetzt), `planning_projects` (dead UI), `supplier_master_data`. Die jeweils existierende Admin-Policy (`at_admin`/`email_queue_admin`/`planning_projects_admin`/`smd_write`) bleibt und erzwingt admin-only; service_role-Writes (Import/Notifications) unberührt. `project_id_sequence` bewusst NICHT enthalten (RLS aus, Write via `next_project_id()`-Funktion/service_role). Gegen PG17 verifiziert (Vuln vorher; non-admin INSERT blockiert, Admin + service_role erlaubt). `BEGIN/COMMIT`, idempotent (`DROP POLICY IF EXISTS`). Rollback: `…-using-true-write-tranche-a-rollback.sql`. |
| 92 | `supabase/migrations/supabase-migration-using-true-write-tranche-b1.sql`                                             | rls-fix | bootstrap, `current_user_role()` | tightens write to owner/admin | **Security (KAR-777 R43/R44, Tranche B-1):** entfernt die legacy `USING(true)`-Write-Duplikate auf 5 Tabellen, die bereits korrekte owner-scoped + admin Policies haben: `assignments` (keep `assignments_own` created_by + `assignments_admin`), `document_departments`/`document_metadata`/`document_suppliers` (keep `_own` = doc uploaded_by/project-owner + `_admin`), `oee_loss_categories` (keep `oee_loss_own` via parent oee_records.created_by + `_admin`). Gegen PG17 verifiziert (beide Ownership-Modelle: created_by direkt + document→project Join; owner darf eigene, fremder via RLS blockiert, admin + service_role durch). NICHT enthalten: `oee_records`/`project_type_assignments` (schon clean), `documents`/`project_consultants` (brauchen neue Policy → B-2). `BEGIN/COMMIT`, idempotent. Rollback: `…-using-true-write-tranche-b1-rollback.sql`. |
| 93 | `supabase/migrations/supabase-migration-using-true-write-tranche-b2.sql`                                             | rls-fix | bootstrap, `current_user_role()` | tightens write to owner/admin | **Security (KAR-777 R43/R44, Tranche B-2):** die letzten 2 `USING(true)`-Write-Tabellen mit NEUEN Policies. `documents`: hatte gar keine owner-Write-Policy → `documents_own` (`uploaded_by=auth.uid()` ODER parent-project-owner) + `documents_admin`, permissive insert/update/delete gedroppt (SELECT via docs_select unberührt — die hier noch mitgenannte Alt-Policy `documents_select` neutralisierte docs_select per OR bis sie durch KAR-978/§7p #112 gedroppt wurde, siehe dort). `project_consultants`: `auth_insert`/`auth_delete` (project-owner-scoped) bleiben; permissive `pc_insert/pc_update/pc_delete` gedroppt; neue scoped `auth_update_project_consultants` (project-owner ODER admin). Gegen PG17 verifiziert (owner/project-owner/admin dürfen, fremder via RLS blockiert, service_role durch; pc-UPDATE benötigt SELECT-Policy-Sichtbarkeit — auf Prod vorhanden). `BEGIN/COMMIT`, idempotent. Rollback: `…-using-true-write-tranche-b2-rollback.sql`. |
| 94 | `supabase/migrations/supabase-migration-master-data-suggestions-owner-insert.sql`                                  | rls-fix | bootstrap | scopes INSERT to owner | **Security (KAR-777 R43/R44, Tranche C):** `master_data_suggestions.mds_insert` war `WITH CHECK(true)` (jeder User konnte beliebige Zeile inkl. gefälschtem `created_by` einfügen). Ersetzt durch `mds_insert_own` `WITH CHECK (created_by = auth.uid())`; UPDATE bleibt admin (`mds_admin`), kein authenticated-DELETE. PG17-verifiziert (own erlaubt, forged blockiert). Idempotent. Rollback: `…-master-data-suggestions-owner-insert-rollback.sql`. |
| 95 | `supabase/migrations/supabase-migration-cross-dept-visibility-r40.sql`                                               | schema + rls + data | bootstrap, `consultants.department_id` (#89), `projects.department_id` (#65) | adds dept-visibility tables + projects SELECT/UPDATE policies | **KAR-784 (R40 erweitert), Phase 1:** 2 neue Tabellen — `department_visibility(viewer,visible)` (admin-konfigurierbar, geseedet MO-23↔MO-24) + `project_view_grants(project_id,user_id)` (per-Projekt-Sichtrecht). Backfillt `projects.department_id` aus `responsibility_id`. Neue projects-Policies: `projects_dept_visible` (SELECT: eigene Abteilung ODER konfigurierte Cross-Dept), `projects_view_granted` (SELECT: per-Projekt-Grant), `projects_dept_edit` (UPDATE: nur eigene Abteilung). `pvg_*` write-Policies per-Command gesplittet (verhindert Policy-Rekursion projects↔grants). Gegen PG17 verifiziert (MO-23↔24 Cross-Sicht, own-dept-edit-only, Grant view-only, admin all, owner/non-owner grant-management, keine Rekursion). UI = Phase 2. `BEGIN/COMMIT`, idempotent. Rollback: `…-cross-dept-visibility-r40-rollback.sql`. |
| 96 | `supabase/migrations/supabase-migration-consultants-show-in-calendar.sql`                                            | schema  | bootstrap (`consultants`) | none | **KAR-785 Teil D:** `consultants.show_in_calendar` (boolean NOT NULL DEFAULT true) — pro Berater im Einsatzplanungs-Kalender ein-/ausblendbar. Default + Backfill true → bestehendes Verhalten unverändert (alle inkl. MO-23/24 sichtbar), Einzelne im Mitarbeiterverwaltung-Editor abschaltbar. Kalender-Queries (`app/kalender/page.tsx`, `app/kalender/consultants/page.tsx`) filtern jetzt `.eq('show_in_calendar', true)`. Gegen PG17 verifiziert (Spalte, default true, backfill, idempotent, Rollback). Additiv. Rollback: `…-consultants-show-in-calendar-rollback.sql`. |
| 97 | `supabase/migrations/supabase-migration-user-profiles-guard-department.sql`                                          | rls-fix | #84 (guard_up_self_update trigger/function) | tightens self-update | **KAR-785 Teil B:** erweitert den KAR-770 self-update-Guard (`guard_user_profiles_self_update`, CREATE OR REPLACE) — ein normaler User darf sein eigenes `department_id` NICHT mehr ändern (nur Admin; Abteilung ist admin-managed). role_id/account_status-Guards aus KAR-770 unverändert. Trigger aus #84 bleibt. Gegen PG17 verifiziert (self department blockiert, Name erlaubt, self role weiterhin blockiert, Admin ändert Abteilung OK). `BEGIN/COMMIT`, idempotent. Rollback: `…-user-profiles-guard-department-rollback.sql` (stellt KAR-770-Guard ohne department-Block wieder her). |
| 98 | `supabase/migrations/supabase-migration-consultants-unique-email.sql`                                                | schema  | bootstrap (`consultants`), Dedup-Cleanup (KAR-778) | none | **KAR-775 R5:** partieller UNIQUE-Index `consultants_active_email_key` auf `lower(btrim(email))` `WHERE is_active = true AND email IS NOT NULL AND btrim(email) <> ''` — verbietet zwei AKTIVEN Beratern dieselbe (case-/whitespace-normalisierte) E-Mail. Inaktive bleiben ausgenommen (Dedup inaktiviert die Verlierer, die ihre E-Mail behalten dürfen). **PRE-CONDITION:** Dedup-Cleanup zuerst — `scripts/dedup-dry-run-report.sql` ausführen, Verlierer inaktivieren. `DO $$`-Guard bricht fail-fast mit klarer Meldung ab, falls noch aktive E-Mail-Dubletten existieren. Spiegelt das bestehende Partial-Unique-Muster (`uq_consultants_active_auth_user`, `consultants_employee_number_key`). Gegen PG17 verifiziert (Guard blockt bei Dubletten; nach Inaktivierung Index erstellt; aktive Dublette inkl. Case-Variante abgelehnt; inaktive Dubletten erlaubt; idempotent; Rollback dropt Index → aktive Dublette wieder erlaubt). `BEGIN/COMMIT`, idempotent. **Apply = operator-gated.** Rollback: `…-consultants-unique-email-rollback.sql`. |
| 99 | `supabase/migrations/supabase-migration-projects-released-scope.sql`                                                 | schema + rls | bootstrap (`projects`/`consultants`), #89 (`consultants.department_id`), #95 (`projects.department_id`, `project_view_grants`, dept-visible policy), `current_user_role()` | tightens SELECT for MO-2/Extern | **KAR-783 (R42):** Release-Scope. `projects.is_released` (boolean NOT NULL default false) + Helper `current_user_is_release_scoped()` (MO-2/Extern-Viewer) + **RESTRICTIVE** SELECT-Policy `projects_released_scope` (release-scoped Viewer sehen ein Projekt nur wenn released ODER eigenes ODER admin ODER explizit granted ODER sie der Projektleiter sind; Nicht-Scoped via `NOT current_user_is_release_scoped()` unberührt) + BEFORE-UPDATE-Guard `guard_projects_release` (nur admin/masteradmin oder der Projektleiter darf `is_released` flippen; mirror KAR-770-Guard, SECURITY INVOKER → service_role/Migrationen durch). Gegen PG17 verifiziert (Matrix: MO-2 sieht released+own+granted+lead, MO-23 unberührt, admin alle, Lead sieht eigenes unreleased, Guard blockt Owner-Nicht-Lead, erlaubt Lead+Admin, idempotent, Rollback). `BEGIN/COMMIT`. **Apply = operator-gated.** UI-Toggle für `is_released` = Folge-Item (KAR-790). Rollback: `…-projects-released-scope-rollback.sql`. |
| 100 | `supabase/migrations/supabase-migration-email-queue-pending-unique.sql`                                              | cleanup + schema | bootstrap (`email_queue`) | none | **KAR-789 (R22-Follow-up):** race-proofer für die email_queue-Idempotenz. **(1) DESTRUKTIV, scoped:** löscht redundante doppelte pending-Rows (`status='pending' AND template IN ('account_created','password_reset')`, behält die ÄLTESTE je `lower(btrim(to_email))`+template). **(2)** partieller UNIQUE-Index `email_queue_pending_idempotent_key` auf `(lower(btrim(to_email)), template)` `WHERE status='pending' AND template IN (...)` — schließt das TOCTOU-Race des App-Guards (PR #210). Informationsmails (password_changed etc.) bleiben unberührt (nicht im Index). `queueEmail` behandelt 23505 als idempotenten No-op. Gegen PG17 verifiziert (Dedup behält ältesten, Case/Space kollabiert, Index lehnt Variante ab, Infomail-Dup erlaubt, sent-Rows unangetastet, idempotent, Rollback). `BEGIN/COMMIT`. **Apply = operator-gated** (enthält scoped DELETE — Backup empfohlen). Rollback: `…-email-queue-pending-unique-rollback.sql`. |
| 79 | `supabase/seed/assessment-fabrikanalyse-seed.sql`                                                                    | seed (data) | 1 (assessment_* Tabellen) | none | **KAR-703 (Übergabe-kritisch):** Fabrikanalyse-Stammdaten — 6 Haupt- + 27 Unterkategorien + **202 Fragen** (DE/EN/CN). Lagen bisher nur in der Original-DB (Test-1), nie im Repo → frische DB = leere Fabrikanalyse. Dollar-Quoting ($fab$, escaping-frei), idempotent (ON CONFLICT id). **Auf jeder frischen DB anwenden.** BMW-Inhalte: bewusste Ausnahme zu ADR-020, siehe `docs/adr/023-fabrikanalyse-seed-data.md`. **Live-Verifikation Pflicht:** `SELECT (SELECT count(*) FROM assessment_main_categories), (SELECT count(*) FROM assessment_sub_categories), (SELECT count(*) FROM assessment_questions);` → erwartet **6 / 27 / 202**. |
| 80 | `supabase/migrations/supabase-migration-process-steps-station-takt.sql`                                              | schema  | 1          | none       | **KAR-727:** Stations-Takt-Overrides — `process_steps.customer_takt_time_sec` + `.target_cycle_time_sec` (NUMERIC(8,2), nullable; NULL = erbt Projektwert aus `projects`). Grundlage für die verbundene Stufen-Linie im Takt-Diagramm. Additiv, kein Backfill, kein RLS-Effekt (bestehende process_steps-Policies decken Writes ab). Idempotent (ADD COLUMN IF NOT EXISTS), Editor-tauglich. |
| 81 | `supabase/migrations/supabase-migration-field-hints-batch2-stopwatch.sql`                                            | seed    | 67         | none       | **KAR-726/727:** Seedet 7 Stoppuhr-Feld-Hinweise (`stopwatch.avg_ct`, `.count`, `.vs_takt`, `.customer_takt`, `.target_ct`, `.station_customer_takt`, `.station_target_ct`) in `lsc_tooltips`. Editor-tauglich, `BEGIN/COMMIT`, idempotent (`ON CONFLICT DO NOTHING` — Admin-Edits bleiben). |
| 82 | `supabase/migrations/supabase-migration-field-hints-batch3-vsm.sql`                                                  | seed    | 67         | none       | **KAR-726 (Sweep):** Seedet 16 VSM-Editor-Feld-Hinweise (`vsm.cycle_time`, `.machine_time`, `.manual_time`, `.setup_time`, `.wait_time`, `.capacity_per_hour`, `.num_workers`, `.process_type`, `.value_added`, `.oee`, `.inventory_quantity`, `.inventory_wait_time`, `.distance`, `.transport_time`, `.demand_per_day`, `.time_value`) in `lsc_tooltips`. Editor-tauglich, `BEGIN/COMMIT`, idempotent (`ON CONFLICT DO NOTHING`). |
| 84 | `supabase/migrations/supabase-migration-user-profiles-self-update-guard.sql`                                         | rls-fix | bootstrap (user_profiles) | restricts self-UPDATE columns | **Security-Fix (KAR-770):** BEFORE-UPDATE-Trigger `guard_up_self_update` verbietet einem normalen `authenticated`-User, auf der **eigenen** Zeile `role_id` zu ändern (Privilege-Escalation) oder `account_status` zu self-reaktivieren (erlaubt nur `invited`/`pending_registration` → `active`). `must_change_password` + sonstige Felder bleiben offen → change-password/reset/account-form-Flows unberührt. SECURITY INVOKER → `service_role`/Migrationen/Admins (`current_user_role()`) laufen durch. Gegen PG17 verifiziert (8/8 Matrix). `BEGIN/COMMIT`, idempotent. Rollback: `…-user-profiles-self-update-guard-rollback.sql`. |
| 83 | `supabase/migrations/supabase-migration-email-queue-protect-template-data.sql`                                       | rls-fix | 38 (email_queue RLS), bootstrap | restricts column SELECT | **Security-Fix (KAR-771):** entzieht `authenticated` das blanket `SELECT` auf `email_queue` und re-grantet alle Spalten **außer `template_data`** (hält den Recovery-/Magic-Link aus KAR-769). Vorher konnte jeder Admin via `email_queue_admin`-Policy den Link lesen und sich als der eingeladene User einloggen. Worker liest per `service_role` (umgeht Column-Privileges) → Versand unberührt. **Deploy-Reihenfolge:** erst forward-kompatiblen App-Code deployen (selektiert nie `template_data`), DANN diese Migration — sonst bricht altes `select('*')`. `BEGIN/COMMIT`, idempotent. Rollback: `…-email-queue-protect-template-data-rollback.sql`. |
| 85 | `supabase/migrations/supabase-migration-consultants-write-rls-admin-only.sql`                                        | rls-fix | bootstrap (`consultants` RLS), `current_user_role()` | restricts INSERT/UPDATE/DELETE to admins | **Security-Fix (KAR-777 R43/R44):** ersetzt die `USING(true)`/`WITH CHECK(true)` Write-Policies auf `consultants` durch `current_user_role() = ANY('admin','masteradmin')`. Vorher konnte **jeder authenticated User** via Browser-Client seine eigene `consultants.role` auf `masteradmin` setzen (Privilege Escalation). Legit-Writes laufen entweder über `service_role`-API-Routen (umgehen RLS) oder über die admin-gated Stammdaten-UI `/repository/masterdata` → unberührt. `SELECT` bleibt bewusst breit. Gegen PostgreSQL 17 verifiziert (Vuln vorher reproduziert; 6/6 Matrix nachher: Non-Admin update/insert/delete blockiert, Admin + service_role erlaubt; Rollback stellt wieder her). `BEGIN/COMMIT`, idempotent (`DROP POLICY IF EXISTS`). Rollback: `…-consultants-write-rls-admin-only-rollback.sql`. |
| 86 | `supabase/migrations/supabase-migration-projects-admin-rls.sql`                                                      | rls     | bootstrap (`projects` RLS), `current_user_role()` | admin sees/manages all projects | **KAR-777 R37:** additive `projects_admin`-Policy auf `projects`. Vorher nur `projects_own USING(user_id=auth.uid())` → Admin/Masteradmin sahen nur eigene Projekte. Neue `FOR ALL TO authenticated`-Policy `USING/WITH CHECK current_user_role() = ANY('admin','masteradmin')`; Postgres OR-kombiniert permissive Policies → Admin = alle Zeilen, normale User = weiterhin nur eigene. `projects_own` unverändert. Gegen PostgreSQL 17 verifiziert (Gap vorher; 5/5 nachher: non-admin own-only, admin sieht+verwaltet alle; Rollback). `BEGIN/COMMIT`, idempotent. Rollback: `…-projects-admin-rls-rollback.sql`. |
| 87 | `supabase/migrations/supabase-migration-consultants-system-dummy.sql`                                                | schema+seed | bootstrap (`consultants`) | none | **KAR-776 R34/R36:** fügt `consultants.is_system_dummy` (boolean, default false) + partiellen UNIQUE-Index (`WHERE is_system_dummy` → max. EIN globaler Dummy) hinzu und seedet die Dummy-Zeile `(System) Nicht zugewiesen` (is_active=false, status=inactive). Genutzt vom Delete-Reassignment-Flow (`lib/employees/reassign-lead.ts`): wenn ein Projektleiter gelöscht wird und kein „Weiterer Berater" existiert, erbt der Dummy das Projekt statt eines dangling RESTRICT-FK. Gegen PG17 verifiziert (idempotent 2×, genau ein Dummy, zweiter Dummy vom Index abgelehnt, Rollback entfernt Spalte). `BEGIN/COMMIT`. Rollback: `…-consultants-system-dummy-rollback.sql`. |
| 88 | `supabase/migrations/supabase-migration-consultants-employee-number.sql`                                             | schema  | bootstrap (`consultants`) | none | **KAR-775 R4:** fügt `consultants.employee_number` (integer, nullable) + Sequence (`consultants_employee_number_seq`, als DEFAULT für neue Zeilen) + partiellen UNIQUE-Index hinzu und backfillt bestehende Berater fortlaufend in stabiler Reihenfolge (`created_at`, dann `id`). Anzeige zero-padded (`001`) via `lib/employees/employee-number.ts`. Entkoppelt vom Dummy (#87) — kein `is_system_dummy`-Bezug, flexible Apply-Order. Gegen PG17 verifiziert (Backfill 1/2/3, neue Zeile = nächste Nr., Duplikat abgelehnt, idempotent ohne Renumber, Rollback). `BEGIN/COMMIT`. Rollback: `…-consultants-employee-number-rollback.sql`. |
| 89 | `supabase/migrations/supabase-migration-department-single-source-mo-codes.sql`                                       | schema+data | bootstrap (`department_master_data`, `consultants`), **#65** (`projects.department_id` — Schritt 0 nullt diese Spalte, ohne #65 bricht die Datei mit `column "department_id" does not exist` ab) | none | **KAR-775 R11/R13/R48/R14 (Kais-Decision 22.06.):** macht `department_master_data` zur EINEN Department-Quelle = MO-Codes. **Destruktiv:** löscht die falschen generischen Rows (AE/EK/FE/LO/PR/QM), seedet `MO-2/MO-23/MO-24/Extern`; KADI + joint raus. FKs sicher (`user_profiles`/`intakes`/`fachbereich_users` SET NULL, `document_departments` CASCADE, `projects` defensiv vorab genullt). Fügt `consultants.department_id`-FK → `department_master_data` hinzu + Backfill aus `team_code` (MO-* match, KADI→NULL). Damit zeigen User- + Projekt-Dropdown dieselben MO-Codes; fixt den `create-user-modal`-Select auf die zuvor nicht-existente `consultants.department_id`. Gegen PG17 verifiziert (Content-Tausch, EK-User SET NULL, Backfill, idempotent, Rollback). **Apply destruktiv → operator-gated, Backup vorab.** Rollback: `…-department-single-source-mo-codes-rollback.sql` (stellt alte generische Rows NICHT wieder her — aus Backup). |

---

## 7e. Storage provisioning (KAR-698 E1/E2 — **muss auf jeder DB laufen**)

> **Warum hier:** Die einzelnen Bucket-SQLs (avatars, project-notes, supplier-logos)
> standen nie in dieser Apply-Order. Folge: auf einer frischen/migrierten DB konnte
> der Bucket fehlen → „Bucket not found" beim Profilbild-Upload. Dieses konsolidierte
> Skript ist die **kanonische** Storage-Provisionierung.

| #  | File | Class | Notes |
| -: | --- | --- | --- |
| S1 | `scripts/provision-storage.sql` | storage | **Kanonisch.** Legt alle 4 Buckets (`avatars`, `project-notes`, `supplier-logos`, `documents`) + RLS-Policies idempotent an (Bucket `ON CONFLICT DO NOTHING`, Policy `DROP+CREATE`). `documents` war zuvor **gar nicht** provisioniert (E2). **Vor dem Run Abschnitt 0 im Skript ausführen** (Live-Bucket/Policy-Check, v.a. ob `documents` schon abweichende Policies hat). Editor-tauglich, kein CONCURRENTLY. Ersetzt die Einzel-SQLs `…-avatars-storage.sql`, `…-notes-storage.sql` und den supplier-logos-Teil von `…-agenda-schema.sql` (diese bleiben als Historie, müssen aber nicht mehr separat appliziert werden). |

---

## 7f. QAF-Differences feature (KAR-799 — 2026-06-25)

| #   | File                                                              | Class         | Depends on | RLS effect | Notes |
| --: | ---------------------------------------------------------------- | ------------- | ---------- | ---------- | ----- |
| 101 | `supabase/migrations/supabase-migration-qaf-differences.sql`     | module-schema | bootstrap (`projects`, `current_user_role()`) | enables | **KAR-799:** 14 Tabellen der QAF Diff Engine (`qaf_batch`, `qaf_file`, `qaf_part`, `qaf_summary_metric`, `qaf_manufacturing_step`, `qaf_comparison`, `qaf_step_match`, `qaf_summary_diff`, `qaf_manufacturing_diff`, `qaf_structure_change`, `qaf_plausibility_issue`, `qaf_root_cause`, `qaf_export`, `qaf_audit_log`). Single-tenant ownership-RLS: jede Tabelle `project_id`-Anker, `_own` (`project_id IN (SELECT id FROM projects WHERE user_id=auth.uid())`) + `_admin` (`current_user_role() = ANY('admin','masteradmin')`), erzeugt per DO-Loop. Gegen PostgreSQL 17 verifiziert (14 Tabellen, 2 Policies/Tabelle, idempotent, Rollback sauber). `BEGIN/COMMIT`, `IF NOT EXISTS`/`DROP POLICY IF EXISTS`. **Operator-applied (Service-Key kann kein DDL), Staging zuerst (BEGIN…ROLLBACK inspizieren).** Rollback: `…-qaf-differences-rollback.sql`. |

## 7g. Planning supplier-FK completion (KAR-704 A1 Stufe 2 — 2026-07-03)

| # | File | Type | Depends on | Special | Notes |
|---|------|------|------------|---------|-------|
| 102 | `supabase/migrations/supabase-migration-assignments-supplier-fk.sql`                                                | schema  | 1          | none       | KAR-704 A1 (Stufe 2): `assignments.supplier_id` (nullable FK → `supplier_master_data(id)`, ON DELETE SET NULL) + Index. **Kein Backfill** — `supplier_name` war in Prod komplett leer (03.07.2026: 390 Zeilen, 0 nicht-leer). Text-Spalte bleibt parallel (Anzeige/Export, Freitext-Fallback). Code-Seite (Picker persistiert FK, Copy/Duplicate ziehen ihn mit, API-v1-PATCH inkl. beidseitigem Stale-FK-Guard) deployt NACH Apply. Idempotent (ADD IF NOT EXISTS, DO-Block). `BEGIN/COMMIT`. Rollback: `…-assignments-supplier-fk-rollback.sql`. |

## 7h. Datenmodell-Cleanup Stufe 4 (KAR-704 R5/A3-S3 — 2026-07-03, applied)

| #   | File | Class | Depends on | RLS effect | Notes |
| --: | ---- | ----- | ---------- | ---------- | ----- |
| 103 | `supabase/migrations/supabase-migration-drop-dead-pmo-tables.sql` | drop | — | removes RLS w/ tables | KAR-704 R5/R9: droppt die 3 toten Tabellen `pmo_measures`, `pmo_log_entries`, `observation_potentials` (03.07. je 0 Zeilen, 0 Code-Refs, keine eingehenden FKs, keine Views). CASCADE als Sicherheitsnetz. `BEGIN/COMMIT`, idempotent (`IF EXISTS`). **Operator-applied, Staging zuerst.** Rollback R31 rekonstruiert Struktur+Indizes+RLS aus Live-DDL (Tabellen waren leer → kein Datenverlust). |
| 104 | `supabase/migrations/supabase-migration-consultants-user-profile-softlink.sql` | schema + backfill | 1 (`user_profiles`) | none | KAR-704 A3-S3: additiver Soft-Link `consultants.user_profile_id` (nullable FK → `user_profiles`, ON DELETE SET NULL) + Index + eindeutiger E-Mail-Backfill (41/42, Nicht-Treffer = „Dummy User" bleibt NULL). Keine FK-Umbiegung, kein Code-Bruch. Idempotent (ADD IF NOT EXISTS, DO-Block, Backfill nur auf NULL). `BEGIN/COMMIT`. Rollback R32. |

## 7i. QAF G60 detail data (KAR-840 G60-Strecke — 2026-07-04)

| #   | File                                                        | Class         | Depends on | RLS effect | Notes |
| --: | ----------------------------------------------------------- | ------------- | ---------- | ---------- | ----- |
| 105 | `supabase/migrations/supabase-migration-qaf-g60.sql`        | module-schema | 101 (`qaf_file`), bootstrap (`projects`, `current_user_role()`) | enables | **KAR-840 G60:** 2 neue Tabellen `qaf_g60_tab` (je Kostenreiter: V11-Aggregat + Komponentenzeilen als JSONB, UNIQUE (file_id, tab_name)) + `qaf_input_card` (INPUT-Karten-C-Codes, UNIQUE (file_id, code)) sowie additive Spalte `qaf_file.g60_meta JSONB` (rates + volumes). RLS `_own`/`_admin` per DO-Loop wie #101. Idempotent (`IF NOT EXISTS`, `DROP POLICY IF EXISTS`), `BEGIN/COMMIT`, Verify-Queries am Dateiende. **Operator-applied (Service-Key kann kein DDL).** Rollback: `…-qaf-g60-rollback.sql` (droppt nur die Netto-Neu-Objekte). |

---

## 7j. QAF comparison title (KAR-846 — 2026-07-06)

| #   | File                                                                  | Class         | Depends on | RLS effect | Notes |
| --: | ---------------------------------------------------------------------- | ------------- | ---------- | ---------- | ----- |
| 106 | `supabase/migrations/supabase-migration-qaf-comparison-title.sql`      | module-schema | 101 (`qaf_comparison`) | none (Spalte erbt _own/_admin) | **KAR-846:** additive Spalte `qaf_comparison.title TEXT` — editierbarer Anzeigename; NULL = Fallback Sachnummer/Modus. Idempotent, `BEGIN/COMMIT`, Verify-Query am Dateiende. **Operator-applied.** Rollback: `…-title-rollback.sql`. |

## 7k. QAF comparison tags (KAR-848 — 2026-07-06)

| #   | File                                                                 | Class         | Depends on | RLS effect | Notes |
| --: | --------------------------------------------------------------------- | ------------- | ---------- | ---------- | ----- |
| 107 | `supabase/migrations/supabase-migration-qaf-comparison-tags.sql`      | module-schema | 101 (`qaf_comparison`) | none (Spalte erbt _own/_admin) | **KAR-848:** additive Spalte `qaf_comparison.tags TEXT[] NOT NULL DEFAULT '{}'` — Produktlinien-Tags (G60/G5x/NA0, mehrere). Auto-Ableitung aus Dateinamen beim Analyze, manuell editierbar. Idempotent, `BEGIN/COMMIT`, Verify am Dateiende. **Operator-applied** (05.07. durch Kais, live verifiziert). Rollback: `…-tags-rollback.sql`. |

## 7l. qaf_process_mappings RLS-Fix (KAR-890 — 2026-07-09, applied)

| #   | File                                                                              | Class     | Depends on | RLS effect | Notes |
| --: | ---------------------------------------------------------------------------------- | --------- | ---------- | ---------- | ----- |
| 108 | `supabase/migrations/supabase-migration-qaf-process-mappings-rls-fix.sql`          | rls-fix   | `qaf_process_mappings` (Stoppuhr-Matching, 05.06.2026), `public.current_user_role()` | tightens | **KAR-890:** hebt die schwache Alt-Policy `qaf_process_mappings_auth_all` (`USING (auth.uid() IS NOT NULL)` — jeder eingeloggte User liest/schreibt projektübergreifend) auf das etablierte projekt-scoped `_own`/`_admin`-Paar an, exakt analog zu den 14 `qaf_*`-Tabellen aus #101 (KAR-799). Verhaltensänderung: Alt-Board-Nutzer sehen Mappings nur noch in eigenen Projekten (oder als admin/masteradmin). Idempotent (`DROP POLICY IF EXISTS`), `BEGIN/COMMIT`, Verify-Query am Dateiende. **Operator-applied** (Kais, Supabase SQL Editor, 09.07.2026). Rollback: `…-rls-fix-rollback.sql`. |

## 7m. PMO team-write RLS (FB-48/FB-49a — 2026-07-17)

| #   | File                                                                  | Class | Depends on | RLS effect | Notes |
| --: | --------------------------------------------------------------------- | ----- | ---------- | ---------- | ----- |
| 109 | `supabase/migrations/supabase-migration-pmo-team-write-policies.sql`   | rls   | `supabase-migration-pmo-phase-1.sql` (erzeugt die 9 PMO-Tabellen — noch nicht separat in diesem Katalog, Backfill-Follow-up), #103 (`drop-dead-pmo-tables`, entfernte `pmo_measures`/`pmo_log_entries`), `public.current_user_role()` | widens team write | **FB-48/FB-49a:** additive Team-Schreibrechte für die 9 live PMO-Tabellen (`project_members`, `project_notes`, `pmo_workstreams`, `pmo_arbeitspakete`, `pmo_weekly_goals`, `pmo_daily_activities`, `pmo_meetings`, `pmo_handovers`, `project_note_attachments`). Behebt den Prod-Bug „new row violates row-level security policy" (owner-only `*_own_project`-Policies ohne separates `WITH CHECK`). Je Tabelle additive PERMISSIVE `<table>_team` (Team-Mitgliedschaft via SECURITY-DEFINER-Helper `public.is_project_member()`, fester `search_path` gegen RLS-Selbstrekursion) + `<table>_admin` (documents_admin-Muster, #21/tranche-b2) + `projects_member_visible` (FOR SELECT auf `projects`, Eltern-SELECT-Falle). **Vertrauensmodell (Kais 2026-07-17, „Option B"):** Team-Mitglieder teilen alle Projekt-Inhalte gegenseitig (Cross-Member-Write bewusst erlaubt), Isolation nur nach außen; `project_members.role` ist deskriptiv (Anzeige/Sortierung, `lib/pmo/members.ts`), gated keine Rechte. Bewusst offen: `projects_released_scope` (RESTRICTIVE, #99/KAR-783) unberührt. Gegen PG 17 verifiziert (65/65 RLS-Harness inkl. Cross-Member + NULL-user_id + Außen-Isolation; `scripts/rls-test/run.sh`). Idempotent (`DROP POLICY IF EXISTS`, `CREATE OR REPLACE FUNCTION`), `BEGIN/COMMIT`. **Operator-applied.** Rollback: `…-pmo-team-write-policies-rollback.sql`. |

## 7n. Offline-Sync updated_at (Prod-Fehler-Fix — 2026-07-17)

| #   | File                                                                  | Class  | Depends on | RLS effect | Notes |
| --: | --------------------------------------------------------------------- | ------ | ---------- | ---------- | ----- |
| 110 | `supabase/migrations/supabase-migration-offline-sync-updated-at.sql`   | schema | bootstrap (`process_steps`, `cycle_measurements`, `workshop_actions`) | none | **Prod-Fehler-Fix (aus Postgres-Logs):** additive `updated_at timestamptz DEFAULT now()` auf die **3 realen** via `createRepository` (`lib/repositories/project-repo.ts`) offline-syncbaren Modul-Tabellen. `lib/offline/sync-engine.ts` braucht `updated_at` für Push-Upsert, Delta-Pull (`gte('updated_at', since)`) und Last-Write-Wins-Konfliktauflösung; nur `projects` hatte die Spalte → Sync dieser Tabellen scheiterte mit „column … updated_at does not exist". **`shift_outputs` bewusst ausgelassen** — die 5. `createRepository`-Tabelle wurde per `r6-shift-outputs-merge` (KAR-704 R6, nicht separat in diesem Katalog) gedroppt (→ `lsc_shift_hours`), existiert in Prod nicht mehr (verifiziert gegen Live-Tabellenstand); `shiftRecordRepo` ist toter Code (Cleanup = Follow-up). **Kein** `set_updated_at`-Trigger (der Sync setzt den Zeitstempel client-seitig; ein Trigger würde LWW + Delta-Pull verfälschen — `projects` hat aus demselben Grund keinen). Backfill aus `created_at` (bzw. `measured_at` für `cycle_measurements`), danach `NOT NULL`; Prod-Zeilenzahlen klein (174/327/4), Lock unkritisch. Test `__tests__/offline/sync-updated-at-schema.test.ts` (rot ohne / grün mit, im rls-test-Harness). Gegen PG 17 verifiziert (47/47). Idempotent (`ADD COLUMN IF NOT EXISTS`, Backfill nur auf NULL), `BEGIN/COMMIT`. **Operator-applied.** Rollback: `…-offline-sync-updated-at-rollback.sql`. |

## 7o. value_stream_imports (QVS-P2, KAR-971 — 2026-07-17, applied to prod 2026-07-18)

| #   | File                                                                       | Class         | Depends on | RLS effect | Notes |
| --: | ---------------------------------------------------------------------------- | ------------- | ---------- | ---------- | ----- |
| 111 | `supabase/migrations/supabase-migration-value-stream-imports.sql`            | module-schema | `value_stream_maps` (MO-26), `qaf_file` (#101/KAR-799), `public.current_user_role()` | new (own table) | **KAR-971 (QVS-P2):** net-new audit/lineage table `value_stream_imports` for the QAF→Wertstrom import program (KAR-968 Epic, `reports/qaf-value-stream-architecture.md` §3.2). RLS `_own`/`_admin` from day one, exact KAR-799/KAR-890 pattern (NOT the lax `auth.uid() IS NOT NULL` predicate KAR-890 closed). Also creates `create_value_stream_from_qaf` (SECURITY DEFINER RPC, same "one function body = one transaction" pattern as `promote_intake_to_project`) so `value_stream_maps` + `value_stream_imports` are written atomically; idempotent on a `previewToken` carried in `engine_context`, enforced by a partial unique index beyond the literal §3.2 index list. `qafValueStream` stayed `false` in all 3 profiles (`config/profiles/`) at P2 merge time — no app code path could reach this table then. **Stand P7 (PR #341, 2026-07-18, Review-Fix):** `qafValueStream` is now `true` in `default`+`bmw` (`_template` stays `false`, no real deployment — no third production profile exists). App code paths reach this table since then via the flag-gated QVS server actions (`app/wertstrom/qaf-actions.ts`, `requireFlagEnabled()`/`getProfile().features.qafValueStream` as the first check of every action) and `app/wertstrom/page.tsx`'s list query when the flag is on for the active profile. See `docs/runbooks/qaf-value-stream-rollout.md` for the rollback (flag-only, no schema touch). Idempotent (`CREATE TABLE/INDEX IF NOT EXISTS`, `DROP POLICY IF EXISTS`, `CREATE OR REPLACE FUNCTION`), `BEGIN/COMMIT`, editor-tauglich (no `CONCURRENTLY`). **APPLIED to production 2026-07-18 ~10:31 CEST** (operator-applied via SQL editor; same-day verification against the live project: table exists, RLS enabled, 0 rows, `project_id` nullable — the SET-NULL review fix is present — and `create_value_stream_from_qaf` exists as SECURITY DEFINER). Operator package remains in `work/qaf-wertstrom/p2-migration/`. Rollback: `…-value-stream-imports-rollback.sql`. |

## 7p. documents_select-Duplikat entfernen (KAR-978 — 2026-07-18, applied to prod 2026-07-18)

| #   | File                                                                | Class   | Depends on | RLS effect | Notes |
| --: | ---------------------------------------------------------------------- | ------- | ---------- | ---------- | ----- |
| 112 | `supabase/migrations/supabase-migration-documents-select-fix.sql`  | rls-fix | bootstrap (`documents`-Tabelle + Alt-Policy `documents_select`, keine separate Autoren-Migration im Repo), `supabase-migration-cleanup.sql` (#30, `docs_select`-Ursprung), #93 (Tranche B-2, WRITE bereits gefixt) | tightens | **KAR-978 (High):** droppt die ungescope-te Alt-Policy `documents_select` (`USING (is_deleted = false)`, `supabase-bootstrap-from-prod.sql:4860`), die die beabsichtigte, scope-geprüfte `docs_select` (`supabase-migration-cleanup.sql:577`, `USING (is_deleted=false AND (access_scope='global' OR eigenes Projekt OR admin/masteradmin))`) per OR-Verknüpfung permissiver Policies komplett neutralisiert hat — Postgres OR-kombiniert permissive Policies derselben Aktion, effektiv galt SELECT auf `documents` bislang nur „nicht gelöscht"; `access_scope='project'` schützte real nichts, jeder authenticated User las projektübergreifend alle nicht-gelöschten Dokumente. Erstmals als real-live Gap dokumentiert im KAR-788-Harness (PR #342, Eskalation im Header von `rls-user-scoped.test.ts`, dort NICHT gefixt da RLS-Changes Operator-Sign-off brauchen); Tranche B-2 (#93, KAR-777) hatte INSERT/UPDATE/DELETE bereits gefixt, SELECT aber bewusst unangetastet gelassen (Kommentar nannte beide Policy-Namen). Write-Policies (`documents_own`/`documents_admin`) unberührt, `service_role` unberührt. Idempotent (`DROP POLICY IF EXISTS`), editor-tauglich, kein `CONCURRENTLY`, eine Transaktion. Real gegen PostgreSQL 17 verifiziert (embedded-postgres-Harness, volle `scripts/rls-test/run.sh`-Kette inkl. dieser Migration + der geflippte `documents`-SELECT-Fall in `rls-user-scoped.test.ts` — Zahlen im CHANGELOG). **Status: APPLIED to production 2026-07-18 ~20:56 CEST** (operator-applied via SQL editor; same-day verification per `pg_policies`: `documents_select` gone, remaining on `documents` only `docs_select` (SELECT) + `documents_own`/`documents_admin` (ALL)). Rollback: `…-documents-select-fix-rollback.sql` (stellt die Alt-Policy exakt wieder her — Notfall-Pfad, siehe Rollback-Datei-Header für den empfohlenen Folge-Schritt). |

## 7q. QAF-Vergleich Eingaben-Autosave (KAR-985 — 2026-07-20, not yet applied)

| #   | File                                                                          | Class         | Depends on | RLS effect | Notes |
| --: | ------------------------------------------------------------------------------ | ------------- | ---------- | ---------- | ----- |
| 113 | `supabase/migrations/supabase-migration-qaf-comparison-user-inputs.sql`        | module-schema | 101 (`qaf_comparison`) | none (Spalte erbt _own/_admin) | **KAR-985:** additive Spalte `qaf_comparison.user_inputs JSONB NOT NULL DEFAULT '{}'::jsonb` — Namespace-Bag `{v:1, projection?, calculator?}` für die beiden bislang session-only Editor-Flächen "12. Hochrechnung & Potenzial in €" (Jahres-/Volumen-Tabelle, Δ-Override, Abwehrquote) und den Preis-Kalkulator (die 12 manuellen UnitPriceParams-Felder, KAR-847/848). Gleiches additive-JSONB-Bag-Muster wie `qaf_file.g60_meta` (#105); Merge-Logik in `lib/qaf-differences`'s `comparison-user-inputs.ts` (rein, unit-getestet), Server Actions in der neuen, eigenständigen `app/qaf-differences/qaf-comparison-user-inputs-actions.ts` (das 4750-Zeilen-`actions.ts` blieb tabu). **FAILSOFT ist Pflicht, nicht optional:** dieser PR kann vor dem Operator-Apply deployen — die Action fängt jeden DB-Fehler auf dieser Spalte ab (Save → `ok:false` mit generischer Meldung, Load → `null`/"keine gespeicherten Eingaben"), nie ein Wurf; beide Seiten bleiben bis zum Apply voll nutzbar, nur ohne Persistenz (wie vor diesem PR). Idempotent (`ADD COLUMN IF NOT EXISTS`), `BEGIN/COMMIT`, Verify-Query am Dateiende. **Operator-applied (Service-Key kann kein DDL).** Rollback: `…-user-inputs-rollback.sql`. |

## 7r. Wertstrom-Szenario-Spalten (KAR-878/KAR-986 — 2026-07-21, applied)

| #   | File                                                                              | Class         | Depends on | RLS effect | Notes |
| --: | ------------------------------------------------------------------------------------ | ------------- | ---------- | ---------- | ----- |
| 114 | `supabase/migrations/supabase-migration-wertstrom-scenarios.sql`                     | module-schema | `value_stream_maps` (MO-26) | none (neue Spalten erben die bestehenden `vsm_own`-Policies — die scopen Zeilen, nicht Spalten) | **KAR-986 (Wertstrom P2, Capability-Matrix A9):** additive Spalten `value_stream_maps.parent_value_stream_id uuid NULL` (FK `ON DELETE SET NULL` auf `value_stream_maps.id`, plus `CHECK` gegen Selbst-Referenz und Partial-Index für "alle Szenarien zu Karte X") und `scenario_kind text NOT NULL DEFAULT 'current'` (`CHECK` `current`/`future`/`alternative`, backfillt die Bestandszeilen korrekt als Ist-Karten). Trägt die Szenario-Fähigkeit (Ist/Soll/Alternativen, execution-prompt §9) — die zugehörigen JSONB-Shape-Erweiterungen für A7 (Informationsfluss-Kantentyp, `connection.kind`/`frequency`), A8 (Lager-Untertypen FIFO/Supermarkt/Push, `node.inventoryKind`/`inventoryMaxQuantity`) und A12 (Provenance, `node.provenance`) brauchten bewusst KEIN DDL — reine additive Zod-validierte JSONB-Felder im selben P2-Code-PR (`lib/vsm-types.ts`, `lib/api/schemas.ts`). Idempotent (`ADD COLUMN IF NOT EXISTS`, Constraints erst `DROP IF EXISTS` dann frisch angelegt), `BEGIN/COMMIT`, editor-tauglich (kein `CONCURRENTLY`, 18 Bestandszeilen zum Zeitpunkt der Autorenschaft). **Status: APPLIED to production 2026-07-21 ~23:15 CEST** (operator-applied via SQL editor; live per `list_tables` verifiziert: `parent_value_stream_id` uuid nullable mit FK `value_stream_maps_parent_fkey`, `scenario_kind` text NOT NULL DEFAULT `'current'` mit CHECK `current`/`future`/`alternative`). Dieser Code-PR (Datenmodell + Zod + Typen + Engine-Anbindung + minimale UI) folgt der bereits applied-Migration nach, siehe P2-Brief. Rollback: `…-wertstrom-scenarios-rollback.sql`. |
| 115 | `supabase/migrations/supabase-migration-qaf-rls-project-access-model.sql` | rls-fix | 101 (`supabase-migration-qaf-differences.sql`, 14 Tabellen + `_own`/`_admin`), `supabase-migration-qaf-g60.sql` (`qaf_g60_tab`, `qaf_input_card`), `supabase-migration-qaf-process-mappings-rls-fix.sql`, 109 (`public.is_project_member()`), `public.current_user_role()` | widens read + team/dept write | **Prod-Fehler-Fix (Kais 2026-07-30, Postgres-Log):** QAF-Upload scheiterte mit „new row violates row-level security policy for table `qaf_file`"; dieselbe Fehlerklasse lag am selben Morgen auf `project_members`, `pmo_workstreams`, `project_notes` — kein QAF-Bug, sondern Berechtigungs-Drift. Die 17 `qaf_*`-Policies prüften für JEDE Operation nur Projekt-**Besitz** (`project_id IN (SELECT id FROM projects WHERE user_id = auth.uid())`) + `_admin`, während `projects` längst vier weitere Zugangswege kennt (`projects_dept_visible` inkl. cross-dept, `projects_dept_edit`, `projects_view_granted`, `projects_member_visible`). #109 hat das Modell für die 9 PMO-Tabellen nachgezogen, die `qaf_*`-Tabellen waren nicht in dessen Scope. Ersetzt je Tabelle `_own`/`_admin` durch `<t>_read` (FOR SELECT, `project_id IN (SELECT id FROM public.projects)`) + `<t>_write` (FOR ALL, derselbe Test **UND** neuer SECURITY-DEFINER-Helper `public.can_write_project()` = Besitzer ∪ admin/masteradmin ∪ Team-Mitglied ∪ eigene Abteilung). **Sicherheits-Kern:** der Sichtbarkeits-Subquery steht absichtlich in der Policy (SECURITY INVOKER) und nicht im DEFINER-Helper — nur so greift die RESTRICTIVE `projects_released_scope` (#99/KAR-783) weiter und MO-2/Extern sehen keine QAF-Daten nicht-freigegebener Projekte; Schreiben verlangt beides (fail-closed). Bewusst NICHT geöffnet: Grant-Empfänger (`projects_view_granted` ist SELECT-only) und cross-dept (analog `projects_dept_edit` zählt nur die eigene Abteilung) — beide bleiben lesend. Out of scope: `qaf_uploads` (Alt-Tabelle aus dem Bootstrap-Dump, eigene unverifizierte Policies), `qaf_summary` (kein `project_id`), `qaf_process_mapping` (Singular, 0 Zeilen, toter Vorgänger). Idempotent (`DROP POLICY IF EXISTS` vor jedem `CREATE`, `CREATE OR REPLACE FUNCTION`), `BEGIN/COMMIT`, editor-tauglich (kein `CONCURRENTLY`). Syntax gegen den echten PostgreSQL-Parser (libpg_query via pglast v8.4) validiert: beide Dateien + alle 153 zur Laufzeit generierten Statements + der Funktions-Body, 0 Fehler; `project_id`-Existenz für alle 17 Tabellen gegen die Autoren-Migrationen geprüft. **NICHT** gegen eine laufende PG-17-Instanz verifiziert (kein lokaler Server verfügbar) — der PL/pgSQL-Kontrollfluss ist 1:1 aus #101 übernommen, das in Prod appliedt ist. **Status: APPLIED to production 2026-07-30 ~11:53 CEST** (operator-applied via SQL editor; verifiziert am selben Tag: das vollständige Statement steht im Postgres-Log mit `source: dashboard`, und `public.can_write_project` erscheint im Security-Advisor, der es 1,5 h vorher noch nicht kannte). **Zwei Einschränkungen, ehrlich festgehalten:** (1) Der Diagnose-Query wurde vor dem Apply *nicht* ausgeführt — Teil A (abweichende Live-Policy-Namen, die permissiv mitstapeln würden) und Teil B/C (Zugangsweg des betroffenen Nutzers) sind ungeprüft; Verify-Query 1 aus dem Datei-Ende sollte nachgeholt werden. (2) Ob der ursprüngliche Upload-Fehler damit behoben ist, hängt am Zugangsweg des Nutzers und war zum Zeitpunkt dieses Eintrags noch nicht durch einen Upload-Test bestätigt. **Nebenbefund:** der Security-Advisor meldet `public.can_write_project` als `anon`/`authenticated`-aufrufbare SECURITY-DEFINER-Funktion (`/rest/v1/rpc/can_write_project`). Harmlos im Ergebnis (für `anon` ist `auth.uid()` NULL → alle vier Zweige false → `false`), aber es ist die 25. Funktion derselben bestehenden Klasse (`is_project_member`, `current_user_role`, `current_user_is_release_scoped` u. a.) — ein gruppenweites `REVOKE EXECUTE ... FROM anon` ist ein offener Hygiene-Task, kein Teil dieser Migration. Rollback: `…-qaf-rls-project-access-model-rollback.sql`. |
| 116 | `supabase/migrations/supabase-migration-qaf-summary-label-verification.sql` | module-schema | 101 (`supabase-migration-qaf-differences.sql` — `qaf_summary_metric`, `qaf_summary_diff`) | none (Spalten erben die bestehenden Policies beider Tabellen) | **KAR-996 (QAF-Compare V2, Befund F-02):** additive Spalten für die Label-Verifikation der Summary-Metriken — `qaf_summary_metric.label_file`/`label_verified` (eine Angabe je Datei, Muster wie `source_cell`) und `qaf_summary_diff.label_file_alt`/`label_file_neu`/`label_verified_alt`/`label_verified_neu` (je Vergleichsseite, Muster wie `source_alt`/`source_neu`). **Warum:** Der Vergleich zeigte für den Preisblock ein Label aus der Konfiguration an, das die Datei nicht trägt (Block 7 heißt im realen Template „JIS"). Ursache ist der Zeilen-Prior-Fallback in `summary-metrics.ts`' `locateRow`: bei null Label-Treffern wird die erwartete Zeile trotzdem gelesen (Confidence 0,6) — der Wert dort stimmt, die Beschriftung nicht. Der Parser liefert `labelFile` und `labelVerified` seit PR #379 mit; ohne diese Spalten verfällt die Information beim Persistieren und die falsche Beschriftung bliebe sichtbar. **`boolean` bewusst nullable, kein `NOT NULL DEFAULT false`:** drei Zustände — `true` bestätigt, `false` nicht bestätigt, `NULL` nicht erhoben (Altbestand) oder strukturell nicht prüfbar (die Ausschusszeile trägt im Template kein eigenes Label, `false` wäre dort ein Fehlalarm in jedem Lauf). Ein Default `false` wäre eine inhaltliche Aussage über nie geprüfte Zeilen. **RLS unverändert:** beide Tabellen haben RLS aktiv, neue Spalten unterliegen automatisch den bestehenden Policies, keine Policy angefasst (Zeilen-, nicht Spalten-Scoping). Struktur beider Tabellen vor dem Schreiben gegen den **Live-Stand** geprüft (`list_tables`), nicht gegen den Bootstrap-Dump. Idempotent (`ADD COLUMN IF NOT EXISTS`), `BEGIN/COMMIT`, editor-tauglich (kein `CONCURRENTLY`), Verify-Query am Dateiende. Keine Datenmutation, daher kein Pre-State-`DO`-Block. **Status: APPLIED to production 2026-07-31 ~23:52 CEST** (operator-applied via SQL editor; Kais meldete „SQL: Success. No rows returned". Nicht daraus geschlossen, sondern verifiziert: `list_tables` gegen das Live-Projekt zeigt alle 6 Spalten mit den erwarteten Typen (`label_file` text, `label_verified` boolean, die vier `label_*_alt/neu` entsprechend). Die Nullability ist über dieses Werkzeug nicht abfragbar und folgt aus dem applizierten DDL — `ADD COLUMN` ohne `NOT NULL` ist in Postgres immer nullable.) Rollback: `…-qaf-summary-label-verification-rollback.sql` (destruktiv per `DROP COLUMN`, aber verlustarm — die Spalten tragen ausschließlich abgeleitete Information, die jeder erneute Parse-Lauf wiederherstellt; keine Nutzereingabe betroffen). |

---

## 7r. QAF Summary-Provenienz (Kriterium 2, 2026-08-07, not yet applied)

| #   | File | Class | Depends on | RLS effect | Notes |
| --: | ---- | ----- | ---------- | ---------- | ----- |
| 118 | `supabase/migrations/supabase-migration-qaf-summary-provenance.sql` | module-schema | 101 (`supabase-migration-qaf-differences.sql` — `qaf_summary_metric`) | none (Spalten erben die bestehenden Policies; Zeilen-, nicht Spalten-Scoping) | **Kriterium 2 des Loop-3-Vorbefunds („jede Difference ID besitzt mindestens eine Source Reference"):** drei additive nullable-Spalten `qaf_summary_metric.sheet`/`formula`/`value_state` — die Provenienz-Bestandteile Blatt, Formeltext und Wertzustand (Spec Kap. 6.3), die der persistierte Pfad bisher nicht führte, weshalb der Differenzkatalog dort keine Zellnachweise erzeugen konnte (Weg 4: bewusst leere `cells`, per `validateTraceability` benannt). Ableitung liegt an EINER Stelle (Parser, der die Formel-Grid-Existenz kennt); der `unresolved`-Shared-Formula-Platzhalter wird nie als Formel persistiert. **Bewusst kein Backfill:** die Originaldateien sind nach dem Ingest nicht mehr adressierbar (`qaf_file` führt keinen Storage-Pfad) — Bestandszeilen bleiben `NULL` = „nicht erhoben", ehrlich gemeldet statt erfunden. **Bewusst kein CHECK auf `value_state`:** konsistent mit den `status`-TEXT-Spalten dieses Schemas; die Wertemenge bewacht `cell-state.test.ts` app-seitig, und die Rehydrierung verwirft Fremdwerte an der Boundary. Live-Stand vor dem Schreiben geprüft (`list_tables`: Tabelle existiert mit 1012 Zeilen; kein Table-Rewrite, da nullable ohne DEFAULT). Idempotent (`ADD COLUMN IF NOT EXISTS`), editor-tauglich (kein `CONCURRENTLY`, kein `BEGIN/COMMIT` nötig — ein einzelnes ALTER). **⚠ Apply-Reihenfolge: MUSS vor dem Deploy des zugehörigen Code-Stands appliedt sein** — Insert (`persistence-mapper.ts`) und Select (`[id]/v2/page.tsx`) nutzen die Spalten und brechen gegen eine Tabelle ohne sie (deployment_only_blocker). **Status: APPLIED to production 2026-08-08 ~18:05 CEST** (operator-applied via SQL editor, Sammel-Apply mit #119; live verifiziert: REST-Select auf `sheet,formula,value_state` antwortet HTTP 200 statt „column does not exist"). Rollback: `…-qaf-summary-provenance-rollback.sql` (destruktiv per `DROP COLUMN`, verlustarm für Bestand — die Spalten tragen abgeleitete Information, die jeder erneute Parse-Lauf wiederherstellt; nur zusammen mit Code-Rollback ausführen). |

---

## 7t. QAF Material-Differenzen (Block 2, 2026-08-07, applied 2026-08-08)

| #   | File | Class | Depends on | RLS effect | Notes |
| --: | ---- | ----- | ---------- | ---------- | ----- |
| 119 | `supabase/migrations/supabase-migration-qaf-material-diff.sql` | module-schema | 101 (`qaf_comparison`, `projects`), 115 (`public.can_write_project()` — die write-Policy ruft den Helper) | enables (neue Tabelle mit eigenem read/write-Paar nach exakt dem #115-Muster) | **Block 2 des PO-Programms „QAF-Vergleich komplett" (Kais, 07.08.2026):** Material war der einzige Vergleichsbereich ohne persistierte Differenzen — die DB-gespeiste V2-Seite konnte ihn nicht zeigen. Neue Tabelle `qaf_material_diff`: eine Zeile je Material-Zuordnung des Paarvergleichs (mapping_id, award/current_rows+names als Arrays, match_type, match_rule_id/details, cost_award/current, delta, **effect** + **zero_cost** — die Wirkungsart wird bewusst persistiert: die Anzeige zeigt, was der Vergleich damals entschied, auch wenn sich Klassifikations-Regeln ändern; dasselbe Reproduzierbarkeits-Prinzip wie engine_version). Die Positionsebenen-Reconciliation wird NICHT persistiert (deterministisch beim Rehydrieren aus Zeilen + Summary-Materialkosten-Diff via `classifyMaterialEffects`). **RLS von Geburt an nach dem #115-Muster** — `qaf_material_diff_read` (Projekt-Sichtbarkeit, voller projects-RLS-Kontext inkl. RESTRICTIVE Release-Gate) + `qaf_material_diff_write` (Sichtbarkeit UND `can_write_project()`), Policy-SQL wörtlich aus #115 übernommen, nicht rekonstruiert; die #115-DO-Schleife selbst deckt nur ihre 17 Bestandstabellen. Kein Backfill (Mappen nach Ingest nicht adressierbar; Recompare erzeugt die Zeilen neu). Editor-tauglich (kein CONCURRENTLY — neue leere Tabelle; `CREATE POLICY` mit `DROP IF EXISTS` davor, re-run-fest), idempotent. **⚠ Apply-Reihenfolge: MUSS vor dem Deploy des zugehörigen Code-Stands appliedt sein** (Insert in actions.ts bricht sonst — deployment_only_blocker; gebündelt mit #118 als Sammel-Apply-Paket). **Status: APPLIED to production 2026-08-08 ~18:05 CEST** (operator-applied via SQL editor, Sammel-Apply mit #118; live verifiziert: `list_tables` zeigt `public.qaf_material_diff` mit `rls_enabled: true`, 0 Zeilen). Rollback: `…-qaf-material-diff-rollback.sql` (DROP TABLE; Zeilen per Recompare wiederherstellbar). |

---

## 7s. Internes Zugriffsmodell (Produktentscheidung 2026-08-04, not yet applied)

| #   | File | Class | Depends on | RLS effect | Notes |
| --: | ---- | ----- | ---------- | ---------- | ----- |
| 117 | `supabase/migrations/supabase-migration-internal-open-access-model.sql` | rls | keine (arbeitet über `pg_class`/`pg_policies`, nicht über feste Tabellenlisten) | öffnet Lesen + Schreiben für `authenticated`, schließt `anon` überall aus | **Produktentscheidung Kais, 04.08.2026 (Telegram 9714):** SupplierPulse ist rein intern, jeder authentifizierte Nutzer darf die Fachdaten lesen und bearbeiten (Projekte, QAFs, Wertströme, Analysen, Exporte, Stammdaten). Keine projektbezogene Beschränkung mehr, einzige Grenze ist die Anmeldung. **Ausdrückliche Bedingung, nicht Nebenwirkung:** damit sieht und ändert jeder Login jede Lieferantenkalkulation — vor einer Öffnung des Nutzerkreises (Adesso-Übergabe, BMW-Pilot mit externen Nutzern) muss das Modell neu bewertet werden. **Vorgehen:** je Fachtabelle wird der gesamte Policy-Bestand ersetzt (nicht ergänzt) durch genau eine Policy `<t>_internal_rw` (`FOR ALL TO authenticated USING (true) WITH CHECK (true)`). Ersetzen statt Ergänzen, weil permissive Policies ODER-verknüpft sind: eine vergessene Altregel bliebe sonst unbemerkt wirksam. Damit fallen auch `_own`/`_admin` (#101), `_read`/`_write` + `can_write_project()` (#115), die Team-Policies (#109) und die RESTRICTIVE `projects_released_scope` (#99) — letztere absichtlich, sie wäre sonst als UND-Bedingung weiter aktiv. **Zwei Schutzklassen (Vorgabe Kais, 04.08.2026):** `protected_security_control_plane_readable` bekommt nur eine offene SELECT-Policy dazu, die Schreibregeln bleiben — `user_profiles`, `roles`, `permissions`, `role_permissions`, `role_view_configs`, `fachbereich_users`, `department_visibility`, `project_view_grants`, `audit_log`, `user_audit_log`, `master_data_audit_log`, `app_settings`, **`consultants`** und **`master_data_import_jobs`**. `protected_security_control_plane_closed` bleibt vollständig unangetastet, auch beim Lesen — `email_queue`, `intake_submission_tokens`, `rls_policy_snapshot`, `rls_access_class` und die 13 `cp_*`. **Die verbindliche Liste steht nicht hier, sondern im SQL und nach dem Lauf in `public.rls_access_class`** — diese Aufzählung ist Erläuterung, nicht Quelle. `consultants` gehört dazu, weil `role` per CHECK `admin`/`masteradmin` erlaubt und acht SECURITY-DEFINER-Funktionen die Spalte als Berechtigungsnachweis lesen (u. a. `create_intake_submission_token()`); ohne diesen Schutz hätte #117 die von KAR-777/#85 geschlossene Selbstbedienungs-Lücke wieder geöffnet — gefunden im unabhängigen Security-Review zu diesem PR. **`anon`:** kommt nirgends an Daten, weil alle Policies an `authenticated` gebunden sind und RLS auf jeder Tabelle aktiv bleibt (die Migration setzt `ENABLE ROW LEVEL SECURITY` idempotent auf allen Tabellen). `service_role` ist BYPASSRLS und unberührt. **Nachweis:** CI-Job `rls` fährt zweistufig — erst die 119 Isolationstests gegen den Produktionsstand, dann diese Migration, dann 27 Tests gegen das neue Soll (positiv: fremder Nutzer liest/ändert/legt an, Profile und Protokolle sind lesbar; negativ: anonym liest, schreibt, ändert und löscht nichts, niemand setzt Rollen — weder fremde noch eigene —, niemand trägt sich selbst als Berater mit Adminrolle ein, Import-Protokolle und Einreichungs-Token bleiben zu; strukturell: jede Tabelle klassifiziert, RLS überall aktiv, genau eine offene Policy je Fachtabelle, keine offene Schreibregel auf der Sicherheitsebene, Protokolle ohne UPDATE-/DELETE-Policy), dann fünf absichtliche Verletzungen, die je am zuständigen Test rot werden müssen, danach der Rollback gegen dieselbe Datenbank mit Prüfung, dass die `#115`-Policies zurück sind. Idempotent, `BEGIN/COMMIT`, editor-tauglich (kein `CONCURRENTLY`). **Apply = operator-gated, noch nicht angewendet.** Rollback: `…-internal-open-access-model-rollback.sql` (stellt den in `public.rls_policy_snapshot` gesicherten Bestand wortgleich wieder her; bricht ab, wenn der Snapshot fehlt oder leer ist, statt einen ungeschützten Zustand zu hinterlassen). |

---

## 7. Rollbacks (do **not** apply unless rolling back)

| #  | File                                                                  | Class      | Notes                                                                  |
| -: | --------------------------------------------------------------------- | ---------- | ---------------------------------------------------------------------- |
| R1 | `supabase/migrations/supabase-migration-rls-phase1-cleanup-rollback.sql`                  | rollback   | Reverts the Phase 1 cleanup migration.                                  |
| R2 | `supabase/migrations/supabase-migration-rls-phase1a-cleanup-rollback.sql`                 | rollback   | Reverts the Phase 1a migration.                                         |
| R3 | `supabase/migrations/supabase-migration-rls-phase1b-user-profiles-rollback.sql`           | rollback   | Reverts the Phase 1b user_profiles rewrite (restores self-join policy). |
| R11 | `supabase/migrations/supabase-migration-email-queue-protect-template-data-rollback.sql`   | rollback   | Reverts KAR-771: restores blanket `SELECT` on `email_queue` for `authenticated` (re-exposes `template_data`). |
| R12 | `supabase/migrations/supabase-migration-user-profiles-self-update-guard-rollback.sql`     | rollback   | Reverts KAR-770: drops the `guard_up_self_update` trigger + function (re-opens privileged self-update of `user_profiles`). |
| R13 | `supabase/migrations/supabase-migration-consultants-write-rls-admin-only-rollback.sql`    | rollback   | Reverts KAR-777: restores the permissive `USING(true)` write policies on `consultants` (re-opens the role-escalation hole — emergency only). |
| R14 | `supabase/migrations/supabase-migration-projects-admin-rls-rollback.sql`                  | rollback   | Reverts KAR-777 R37: drops `projects_admin` (admins fall back to own-projects-only). |
| R15 | `supabase/migrations/supabase-migration-consultants-system-dummy-rollback.sql`            | rollback   | Reverts KAR-776 R34: deletes the system dummy + drops the index and `is_system_dummy` column (fails if the dummy is still a project lead — reassign first). |
| R16 | `supabase/migrations/supabase-migration-consultants-employee-number-rollback.sql`         | rollback   | Reverts KAR-775 R4: drops `consultants.employee_number`, its unique index and the sequence. |
| R17 | `supabase/migrations/supabase-migration-department-single-source-mo-codes-rollback.sql`   | rollback   | Reverts KAR-775 dept-single-source: drops `consultants.department_id` + removes the MO seed. Does NOT restore the old generic departments (recover from backup). |
| R18 | `supabase/migrations/supabase-migration-agenda-participant-department-fk-rollback.sql`     | rollback   | Reverts KAR-704 A2: drops `agenda_participant.department_id` + index (text `department` column stays). |
| R19 | `supabase/migrations/supabase-migration-using-true-write-tranche-a-rollback.sql`          | rollback   | Reverts KAR-777 Tranche A: restores the permissive `USING(true)` write policies on the 5 tables (re-opens authenticated write — emergency only). |
| R20 | `supabase/migrations/supabase-migration-using-true-write-tranche-b1-rollback.sql`         | rollback   | Reverts KAR-777 Tranche B-1: restores the permissive `USING(true)` write policies on the 5 tables (emergency only). |
| R21 | `supabase/migrations/supabase-migration-using-true-write-tranche-b2-rollback.sql`         | rollback   | Reverts KAR-777 Tranche B-2: restores permissive write policies on documents + project_consultants (emergency only). |
| R22 | `supabase/migrations/supabase-migration-master-data-suggestions-owner-insert-rollback.sql` | rollback   | Reverts KAR-777 Tranche C: restores permissive INSERT on master_data_suggestions. |
| R23 | `supabase/migrations/supabase-migration-cross-dept-visibility-r40-rollback.sql`          | rollback   | Reverts KAR-784: drops the projects visibility policies + department_visibility/project_view_grants tables (department_id backfill stays). |
| R24 | `supabase/migrations/supabase-migration-consultants-show-in-calendar-rollback.sql`        | rollback   | Reverts KAR-785 D: drops `consultants.show_in_calendar`. |
| R25 | `supabase/migrations/supabase-migration-user-profiles-guard-department-rollback.sql`     | rollback   | Reverts KAR-785 B: restores the KAR-770 guard without the department_id self-change block. |
| R26 | `supabase/migrations/supabase-migration-consultants-unique-email-rollback.sql`           | rollback   | Reverts KAR-775 R5: drops the `consultants_active_email_key` partial UNIQUE index. No data touched. |
| R27 | `supabase/migrations/supabase-migration-projects-released-scope-rollback.sql`            | rollback   | Reverts KAR-783 R42 (#99): drops the release guard trigger+function, the `projects_released_scope` restrictive policy, `current_user_is_release_scoped()`, and the `projects.is_released` column. |
| R28 | `supabase/migrations/supabase-migration-email-queue-pending-unique-rollback.sql`         | rollback   | Reverts KAR-789 (#100): drops the `email_queue_pending_idempotent_key` index. The deduplicated duplicate-pending rows are not restored (redundant, unrecoverable). |
| R29 | `supabase/migrations/supabase-migration-qaf-differences-rollback.sql`                    | rollback   | Reverts KAR-799 (#101): drops the 14 `qaf_*` QAF-Differences tables (CASCADE) + their RLS policies. **Destructive — deletes all QAF-Differences data.** |
| R30 | `supabase/migrations/supabase-migration-assignments-supplier-fk-rollback.sql`            | rollback   | Reverts KAR-704 A1 Stufe 2 (#102): drops `assignments.supplier_id` + FK + index (text `supplier_name` column stays). Nach Code-Deploy gesetzte supplier_id-Werte gehen verloren. |
| R31 | `supabase/migrations/supabase-migration-drop-dead-pmo-tables-rollback.sql`               | rollback   | Reverts #103: rekonstruiert `pmo_measures`, `pmo_log_entries`, `observation_potentials` (Struktur + Indizes + FKs + RLS aus Live-DDL 03.07.). Tabellen waren leer → kein Datenverlust. Setzt voraus, dass `pmo_workstreams`/`project_members`/`project_notes` noch existieren. |
| R32 | `supabase/migrations/supabase-migration-consultants-user-profile-softlink-rollback.sql`  | rollback   | Reverts KAR-704 A3-S3 (#104): drops `consultants.user_profile_id` + FK + index (rein additive Spalte, keine Bestandsdaten). |
| R33 | `supabase/migrations/supabase-migration-qaf-process-mappings-rls-fix-rollback.sql`       | rollback   | Reverts KAR-890 (#108): restores the permissive `qaf_process_mappings_auth_all` policy (`USING (auth.uid() IS NOT NULL)`), drops `_own`/`_admin`. Emergency only — re-opens cross-project read/write on `qaf_process_mappings`. |
| R34 | `supabase/migrations/supabase-migration-pmo-team-write-policies-rollback.sql`            | rollback   | Reverts #109 (FB-48/FB-49a): drops the 9 `<table>_team` + 9 `<table>_admin` policies, `projects_member_visible`, and `public.is_project_member()`. Emergency only — re-breaks team write (only project owners/admins can write PMO rows again). |
| R35 | `supabase/migrations/supabase-migration-offline-sync-updated-at-rollback.sql`            | rollback   | Reverts #110: drops `updated_at` from `process_steps`, `cycle_measurements`, `workshop_actions`. Emergency only — re-breaks offline sync of these tables. `updated_at` is sync metadata; no business data lost. |
| R36 | `supabase/migrations/supabase-migration-value-stream-imports-rollback.sql`               | rollback   | Reverts #111 (KAR-971): drops `create_value_stream_from_qaf` and the `value_stream_imports` table (CASCADE via FK, no separate DROP POLICY needed). Deletes the whole QAF-import audit trail; `value_stream_maps` rows already created via the RPC are NOT deleted (ordinary Wertströme once created). #111 was applied to prod 2026-07-18 (see §7o). **Stand P7 (PR #341, Review-Fix):** the premise „since `qafValueStream` is `false` everywhere, no app code writes to this table" no longer holds — the flag is `true` in `default`+`bmw` since 2026-07-18, so QVS server actions (flag-gated) do write rows to this table while the flag is on for a booted profile; flip the flag off first (`docs/runbooks/qaf-value-stream-rollout.md` §2, flag-only revert, no schema touch) before considering this schema rollback. |
| R37 | `supabase/migrations/supabase-migration-documents-select-fix-rollback.sql`               | rollback   | Reverts #112 (KAR-978, §7p): restores the permissive Alt-Policy `documents_select` (`USING (is_deleted = false)`) exactly as it existed in `supabase-bootstrap-from-prod.sql:4860` — emergency only, re-opens the cross-project SELECT gap on `documents` (any authenticated user reads all non-deleted documents again). Use only if legitimate read paths break post-apply; the recommended follow-up if so is to extend `docs_select` and re-drop `documents_select`, not leave the leak open (see rollback file header). |
| R38 | `supabase/migrations/supabase-migration-qaf-comparison-user-inputs-rollback.sql`         | rollback   | Reverts #113 (KAR-985, §7q): drops `qaf_comparison.user_inputs`. Emergency only — deletes all persisted Vergleichs-Autosave-Eingaben (Hochrechnung + Preis-Kalkulator); both editor surfaces degrade failsoft without the column anyway (qaf-comparison-user-inputs-actions.ts), so this is for when the column itself causes a problem, not to "turn off" autosave. |
| R39 | `supabase/migrations/supabase-migration-wertstrom-scenarios-rollback.sql`                | rollback   | Reverts #114 (KAR-878/KAR-986, §7r): drops `parent_value_stream_id`/`scenario_kind` (+ constraints + partial index) from `value_stream_maps`. **Datenvernichtend for any Soll-/Alternative-Szenario created after the forward migration** — the parent-Verknüpfung and scenario_kind classification are lost (the Wertstrom rows themselves are NOT deleted, they just fall back to being ordinary, unlabeled top-level maps). The 18 pre-existing rows (Stand 21.07.2026) carry only default values, so the rollback is loss-free for them. Emergency only — check for real Soll-/Alternative-Szenarien first (`scenario_kind <> 'current'`). |
| R40 | `supabase/migrations/supabase-migration-internal-open-access-model-rollback.sql`         | rollback   | Reverts #117 (§7s): entfernt die `<t>_internal_rw`/`<t>_internal_read`-Policies und stellt den in `public.rls_policy_snapshot` gesicherten Bestand wortgleich wieder her (Name, PERMISSIVE/RESTRICTIVE, Kommando, Rollen, `USING`, `WITH CHECK`), danach wird der Snapshot geleert. **Bricht ab, wenn der Snapshot fehlt oder leer ist** — ohne ihn gibt es keinen Rückweg, und ein stilles „hat funktioniert" wäre die gefährlichste Antwort; dann helfen nur die historischen Migrationen (#101, #109, #115, #95, #99) erneut. Tabellen, die es zum Rollback-Zeitpunkt nicht mehr gibt, werden übersprungen statt abzubrechen. Im CI-Job `rls` läuft dieser Rollback bei jedem Lauf gegen dieselbe Datenbank und wird geprüft (keine Reste der Umstellung, `qaf_comparison_read`/`_write` aus #115 zurück). |

---

## Known RLS audit notes

The most recent live audit is in [`docs/foundation/rls-audit-2026-04-21.md`](docs/foundation/rls-audit-2026-04-21.md). Highlights:

- The replacement policies for `project_type_assignments`, `project_suppliers`, and `lsc_shift_hours` exist as `supabase/rls/supabase-rls-test-*.sql`. The "test" prefix predates this branch — it does not mean the SQL is invalid, only that it has not been applied yet.
- Some lax `USING (true)` policies on documents, appointments, and metadata tables are documented in the audit as deferred. Do not silently apply them in Azure without revisiting the audit.
- Phase 1 / 1a / 1b cleanup is **applied** in the operator's Supabase as of PR #35 (commit `95b02b7`). Replays in Azure should expect those policies to be in place.

---

## 7u. QAF Deklarierte Vergleichsart (R-03, 2026-08-08, not yet applied)

| #   | File | Class | Depends on | RLS effect | Notes |
| --: | ---- | ----- | ---------- | ---------- | ----- |
| 120 | `supabase/migrations/supabase-migration-qaf-declared-comparison-type.sql` | module-schema | 101 (`supabase-migration-qaf-differences.sql` — `qaf_comparison`) | none (Spalte erbt die bestehenden qaf_comparison-Policies; Zeilen-, nicht Spalten-Scoping) | **R-03 der Spec-Erhebung (Spec Kap. 21, PO-Linie 08.08.2026 „flexibel — deklarierbar, System rät nie"):** eine additive nullable-Spalte `qaf_comparison.declared_comparison_type` — die vom Menschen deklarierte Vergleichsart (temporal_change/supplier_benchmark/site_comparison/variant_comparison/undetermined), die der V2-Kopf bisher hartkodiert als „unbestimmt" zeigte. **Bewusst NICHT `comparison_mode`** — das ist das geschlossene technische Modus-System (comparison-mode.ts, Dispatch-Regeln je Modus); die fachliche Deklaration ist orthogonal, eine Doppelbelegung der Spalte wäre die Drift, die das Modul beerdigt hat. NULL = nie deklariert = „unbestimmt" (ehrlicher Default, kein Backfill — Deklaration ist eine Menschen-Entscheidung, nichts ist ableitbar). Kein CHECK (konsistent mit status/value_state; Wertemenge bewacht `declared-comparison-type.ts` an Lese- UND Schreib-Boundary — Lesen normalisiert Fremdwerte zu undetermined, Schreiben lehnt sie ab). Editor-tauglich (ein ALTER, idempotent, kein CONCURRENTLY, kein Rewrite — nullable ohne DEFAULT; 39 Bestandszeilen zum Autorzeitpunkt). **Apply-Reihenfolge entspannt (KEIN deployment_only_blocker):** der Code liest die Spalte über einen eigenen fehlertoleranten Select (Spalte fehlt → undetermined, Setzen-Aktion meldet „Migration ausstehend") — Merge/Deploy dürfen dem Apply vorausgehen; volle Funktion erst nach Apply. **Status: not yet applied.** Rollback: `…-qaf-declared-comparison-type-rollback.sql` (destruktiv per DROP COLUMN — verliert gesetzte Deklarationen ersatzlos, Menschen-Entscheidungen sind nicht ableitbar; nur mit Code-Rollback). |

## 7v. QAF Operation-Lock (Loop 5 Parallelitäts-Mitigation, 2026-08-12, not yet applied)

| #   | File | Class | Depends on | RLS effect | Notes |
| --: | ---- | ----- | ---------- | ---------- | ----- |
| 121 | `supabase/migrations/supabase-migration-qaf-operation-lock.sql` | module-schema | 101 (`qaf_comparison`, `projects`), 115 (`public.can_write_project()` — die write-Policy ruft den Helper) | enables (neue Tabelle mit eigenem read/write-Paar nach exakt dem #115-Muster) | **Loop-5-Parallelitäts-Mitigation (benannter Befund des Silent-Failure-Reviews 11.08.):** `replaceComparisonFile` patcht die Datei-Zeiger last-write-wins — zwei gleichzeitige Replace/Reparse-Aufrufe derselben Rolle können Zeiger und abgeleitete Zeilen verschränken. Neue Tabelle `qaf_operation_lock` als Mutex-Primitiv: eine Zeile je `(comparison_id, role)` (PK), Erwerb über den PK-Konflikt (`upsert` mit `ignoreDuplicates` → leeres Ergebnis = vergeben), Übernahme verwaister Locks über einen `locked_at`-TTL-CAS (15 min), Freigabe über `delete` mit `locked_by`-Gleichheit (zufällige Operations-ID — derselbe Nutzer in zwei Tabs entsperrt sich nicht gegenseitig). Ein echter Postgres-Advisory-Lock scheidet aus: `pg_advisory_xact_lock` hält nur eine Transaktion (der Flow ist Multi-Roundtrip), Session-Advisory-Locks scheitern am PostgREST-Pooling — Begründung in `docs/qaf-v2/replace-parallelitaet-analyse.md`. **RLS von Geburt an nach dem #115-Muster** (Policy-SQL wörtlich aus #119 übernommen, nicht rekonstruiert); der Lock läuft über den normalen User-Client, kein Service-Role-Register-Eintrag. Live-Stand vor dem Schreiben geprüft (`list_tables`: `qaf_comparison` existiert, `qaf_operation_lock` nicht). Editor-tauglich (kein CONCURRENTLY — neue leere Tabelle; `CREATE POLICY` mit `DROP IF EXISTS` davor), idempotent. **Keine Apply-vor-Deploy-Pflicht — bewusst anders als #118/#119:** der Code erkennt die fehlende Tabelle (42P01/PGRST205), degradiert ehrlich auf das heutige Verhalten (kein Lock, `logger.warn`, getestet) und der Lock wird mit dem Apply wirksam. Kann mit dem offenen Sammel-Apply (#117/#120) gebündelt werden. **Status: not yet applied.** Rollback: `…-qaf-operation-lock-rollback.sql` (DROP TABLE; verlustfrei — Tabelle trägt nur transiente Lock-Zeilen). |

## Vendor handover notes

- Apply order in Azure should mirror sections 1–6 above. Skip rollbacks unless reverting.
- For each `module-schema` file, confirm the table set matches the Azure target — naming should not drift.
- `review-only` files (rows #5, #40, #41, #42) require explicit operator sign-off before they are run in Azure.
- The `cp_tenant_environments.supabase_service_role_key` plaintext column is part of the platform schema. The Azure replacement is a Key-Vault-secret-name reference; that change is **not** in this branch.
- The vendor should plan a clean staging round before any production replay.
