diff --git a/ordinis-auth/src/main/java/cloud/nstart/terravault/ordinis/auth/OrgContext.java b/ordinis-auth/src/main/java/cloud/nstart/terravault/ordinis/auth/OrgContext.java
new file mode 100644
index 0000000..a609a0e
--- /dev/null
+++ b/ordinis-auth/src/main/java/cloud/nstart/terravault/ordinis/auth/OrgContext.java
@@ -0,0 +1,113 @@
+package cloud.nstart.terravault.ordinis.auth;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
+import org.springframework.stereotype.Component;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * Phase 3 cheap-prep skeleton — resolver «текущая организация юзера».
+ *
+ *
Сейчас всегда возвращает empty — Ordinis single-tenant, никакая
+ * логика не использует org-scoping. Bean существует чтобы при старте Phase 3:
+ *
+ * - Callsites могут уже сейчас depend on {@code OrgContext} без падений
+ * (всегда получат "нет org" — поведение совпадает с текущим).
+ * - Замена внутри методов на реальный JWT claim parser — изолированная,
+ * без необходимости менять весь call graph.
+ * - Phase 3 startup стоит ~30 минут вместо ~2 дней (зависит от того
+ * сколько мест уже подключилось).
+ *
+ *
+ * Когда Phase 3 стартует, имплементация поменяется на чтение JWT claim
+ * {@code organization} (altum auth-service кладёт туда array of org UUIDs).
+ * Параллельно появится:
+ *
+ * - Liquibase migration 0029+ — backfill {@code org_id} в data-таблицах
+ * (колонки уже добавлены nullable в migration 0028, см. её комментарий).
+ * - {@code OrgScopeFilter} — applies {@code WHERE org_id IN (...)} к
+ * repository queries.
+ * - {@code X-Org-Id} header / {@code ?org=} query — переключение между
+ * orgs когда юзер в нескольких.
+ *
+ *
+ * Не путать со {@link ScopeContext}: scope = data sensitivity tier
+ * (PUBLIC/INTERNAL/RESTRICTED), org = data tenant. Они ортогональны:
+ * Customer-X RESTRICTED data ≠ Customer-Y RESTRICTED data.
+ */
+@Component
+public class OrgContext {
+
+ private static final Logger log = LoggerFactory.getLogger(OrgContext.class);
+ private static final String ORG_CLAIM = "organization";
+
+ /**
+ * Все организации текущего юзера. Пустой Set если:
+ *
+ * - JWT отсутствует (anonymous request).
+ * - JWT не содержит {@code organization} claim.
+ * - Phase 3 ещё не активен (текущее состояние).
+ *
+ *
+ * Реализация ниже уже умеет читать claim — это сделано чтобы при
+ * включении Phase 3 не пришлось трогать сам метод, только downstream
+ * consumers. Сейчас downstream'ов нет → выход пустой → ничего не делает.
+ */
+ public Set currentOrgs() {
+ Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+ if (!(auth instanceof JwtAuthenticationToken jwtAuth)) return Set.of();
+ return readOrgClaim(jwtAuth.getToken());
+ }
+
+ /**
+ * Single «active» org для current-request scope. Когда юзер в одной org —
+ * она. Когда в нескольких — нужен явный select (header / query param),
+ * пока не реализовано → возвращает первую (детерминистично, sorted), либо
+ * empty если нет ни одной.
+ *
+ * В Phase 3 этот метод будет принимать requested org из request и
+ * валидировать что юзер in её состав. Сейчас просто placeholder.
+ */
+ public Optional activeOrg() {
+ Set orgs = currentOrgs();
+ if (orgs.isEmpty()) return Optional.empty();
+ return orgs.stream().sorted().findFirst();
+ }
+
+ /**
+ * Парсит {@code organization} claim из JWT. Поддерживает оба формата
+ * которые встречаются у altum auth-service:
+ *
+ * - Массив строк: {@code "organization": ["nstart", "customer-x"]}.
+ * - Одна строка (legacy): {@code "organization": "nstart"}.
+ *
+ * Null/missing claim → empty set.
+ */
+ private static Set readOrgClaim(Jwt token) {
+ Object raw = token.getClaim(ORG_CLAIM);
+ if (raw == null) return Set.of();
+ Set orgs = new HashSet<>();
+ if (raw instanceof Collection> col) {
+ for (Object o : col) {
+ if (o == null) continue;
+ String s = o.toString().trim();
+ if (!s.isEmpty()) orgs.add(s);
+ }
+ } else {
+ String s = raw.toString().trim();
+ if (!s.isEmpty()) orgs.add(s);
+ }
+ if (log.isDebugEnabled() && !orgs.isEmpty()) {
+ log.debug("OrgContext: JWT organization claim resolved → {}", orgs);
+ }
+ return Set.copyOf(orgs);
+ }
+}
diff --git a/ordinis-migrations/src/main/resources/db/changelog/changes/0028-phase3-prep-org-id.xml b/ordinis-migrations/src/main/resources/db/changelog/changes/0028-phase3-prep-org-id.xml
new file mode 100644
index 0000000..30c70c3
--- /dev/null
+++ b/ordinis-migrations/src/main/resources/db/changelog/changes/0028-phase3-prep-org-id.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+ Phase 3 prep: nullable org_id на dictionary_definitions
+
+
+
+
+
+
+
+
+ Phase 3 prep: nullable org_id на dictionary_records
+
+
+
+
+
+
+
+
+ Phase 3 prep: nullable org_id на audit_log
+
+
+
+
+
+
+
diff --git a/ordinis-migrations/src/main/resources/db/changelog/master.xml b/ordinis-migrations/src/main/resources/db/changelog/master.xml
index e9bfddf..ba6ef33 100644
--- a/ordinis-migrations/src/main/resources/db/changelog/master.xml
+++ b/ordinis-migrations/src/main/resources/db/changelog/master.xml
@@ -37,5 +37,6 @@
+