ADR-017: RLS Multi-Tenancy Migration
# ADR-017 — RLS Multi-Tenancy Migration
**Status:** Accepted — CEO Signed 2026-05-11 (Alem Bašić). Phase 2A V17 Flyway PERMISSIVE migration authorized for stage execution. Phase 2C RESTRICTIVE flip remains gated on Securion audit + 30-day soak per §4 schedule.
**Date:** 2026-05-11
**Author:** Bruce Momjian (Database Architecture, CodeCraft)
**Architecture Review:** Petter Graff (CodeCraft)
**Decision-maker:** CEO Alem Bašić — SIGNED 2026-05-11 ("ok adr17 odobreno") via session f73dafab
**Mehanik clearance:** /tmp/mehanik-cleared-100362
**MC Task:** #100362 (Phase 0' ADR Consolidation)
**Promoted from:** ADR-bilko-001 draft (`~/system/specs/bilko-multi-market-architecture-plan/ADR-bilko-001-multi-tenant-architecture.md`)
**Cross-references:**
- ADR-023 (why single DB remains correct — §6 supersession triggers not fired; §2 context)
- ADR-015 (TaxJurisdiction enum drives `country_code` column CHECK values)
- ADR-bilko-001 (ancestor draft, fully absorbed by this ADR — do not reference ancestor)
- ADR-bilko-003 §Layer 3 (versioned CoA data model)
- Plan v3 §4a (Option D not triggered), §4c (RLS timing — PERMISSIVE before Phase 1H merge)
- `~/system/specs/bilko-multi-market-architecture-plan-v3-2026-05-11.md`
---
## 1. Context
### 1.1 Current DB State (tool-verified 2026-05-11)
| Component | State |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Database | `bilko-demo-db`, Cloud SQL PostgreSQL 15, europe-north1 |
| Flyway migrations | V1..V15 applied |
| Row-Level Security | NOT enabled — zero RLS policies on any table |
| Tenant isolation | Application-layer only: `WHERE org_id = :principalOrgId` clauses |
| `organizations.country` | Column exists; values `'RS'`, `'HR'`, `'BA'`; NOT NULL constraint absent |
| Cross-tenant leak | Confirmed: PUT `/api/v1/invoices/{id}` and GET `/api/v1/invoices/{id}/pdf` with cross-tenant JWT return HTTP 500 (test drift memo 2026-05-10, Round 12.1/12.5) |
The current application-layer scoping (ADR-005) is the sole isolation mechanism. A single
missing `WHERE org_id` clause in any new route — or a refactoring that silently drops it — is
a cross-tenant data exposure. This is not theoretical: Round 12 probes confirmed it in two
existing routes.
### 1.2 Why Single Database Remains Correct (ADR-023 §6 Check)
ADR-023 §6 defines the conditions that would trigger migration to Option D (per-country DBs).
All five conditions are unmet as of 2026-05-11 (Plan v3 §4a lines 100–108):
- Paying customers in 2+ markets: 0 — NOT triggered
- Regulatory request for per-country data extract: none received — NOT triggered
- HR-FISK kernel-level coupling: Storecove API path requires no kernel isolation — NOT triggered
- p95 query latency > 500ms from cross-country noise: 0 paying customers — NOT triggered
- 2 customers complain about cross-country data visibility: 0 customers — NOT triggered
Option D costs +$60/month infra and 2–4 weeks engineering per market with no customer-facing
benefit today. **This ADR is explicitly compatible with Option D migration** — RLS policies
are portable to separate databases. If Option D triggers, the same policy DDL applies
to each per-country DB with zero changes.
### 1.3 Why RLS Cannot Wait Until Post-HR GA
Plan v3 §4c (lines 135–145): the cross-tenant 500 leaks are a live security defect.
With 0 paying customers today it is unexploited — but a second registered organization
(required for HR demo) creates an immediately exploitable state.
RLS PERMISSIVE mode (Phase 2A) imposes zero user-facing change and zero risk of service
disruption. The existing `WHERE org_id` middleware still fires, and RLS fires alongside
it. Both must pass for data to be returned. A latent policy gap is caught by the
application layer rather than exposing data to the wrong tenant.
**CEO sign is required before Phase 2A Flyway migrations run on stage** — not before
this ADR document is accepted. The ADR records the decision; the sign unblocks execution.
---
## 2. Decision
**Option C is adopted: Shared codebase, shared deployment, shared database, with
PostgreSQL Row-Level Security enforcing tenant isolation.**
This is the unanimous recommendation from the 5-agent architecture review (ADR-bilko-001 §framing,
line 28–30). One codebase. One Cloud Run deployment. One PostgreSQL instance with RLS.
### 2.1 Binding Constraints
1. `Organization.taxJurisdiction` (`TaxJurisdiction` enum `{HR, RS, BA_FED, BA_RS}` per ADR-015) is
the primary discriminator for jurisdiction-specific behaviour.
2. `Organization.id` (UUID) is the primary tenant discriminator for data isolation.
3. RLS policies enforce data isolation at the database layer. Application code MUST NOT
rely solely on `WHERE org_id = :id` clauses (ADR-005 flaw — being retired by Phase 2C).
4. The `country_code` column on `organizations` is NOT NULL with CHECK constraint
`IN ('HR', 'RS', 'BA_FED', 'BA_RS')` — enforced by Flyway V16 (Phase 1H Task 1H.1).
5. EU data residency: Current `bilko-demo-db` is in Cloud SQL `europe-north1` (Finland).
This IS within EU/EEA — GDPR Article 44 satisfied. Frankfurt migration (eu-central-1)
is not required to unblock HR GA (Plan v3 §4d lines 179–183).
### 2.2 Three-Phase Migration Path
The migration is split into three phases to ensure zero service disruption and a safe
rollback path at each step.
#### Phase 2A — PERMISSIVE RLS (parallel with Phase 1H, target: end of Week 2)
**Goal:** RLS policies created and attached, set to PERMISSIVE. Existing application-layer
scoping continues to operate. Both layers must pass — RLS is a second check, not a
replacement.
**Who signs this off:** CEO Alem Bašić (this ADR signature) — required before any
Phase 2A Flyway migrations run on the stage database.
**DDL — PERMISSIVE policies (Flyway V17):**
```sql
-- V17__rls_permissive.sql
-- ZAKON: CEO sign required before this migration runs on stage.
-- Apply PERMISSIVE RLS on core tables. Application-layer WHERE org_id
-- clauses remain active. Both must pass.
-- Enable RLS on tables
ALTER TABLE organizations ENABLE ROW LEVEL SECURITY;
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
ALTER TABLE invoice_items ENABLE ROW LEVEL SECURITY;
ALTER TABLE expenses ENABLE ROW LEVEL SECURITY;
ALTER TABLE transactions ENABLE ROW LEVEL SECURITY;
ALTER TABLE bank_transactions ENABLE ROW LEVEL SECURITY;
ALTER TABLE bank_accounts ENABLE ROW LEVEL SECURITY;
ALTER TABLE accounts ENABLE ROW LEVEL SECURITY;
ALTER TABLE contacts ENABLE ROW LEVEL SECURITY;
-- PERMISSIVE policy: organization-scoped isolation
-- current_setting() reads the app.current_org_id session variable
-- set by the Ktor connection pool before each query (connection middleware).
CREATE POLICY org_isolation ON invoices
AS PERMISSIVE
FOR ALL
TO bilko_app -- application role (NOT superuser)
USING (org_id = current_setting('app.current_org_id')::uuid);
CREATE POLICY org_isolation ON invoice_items
AS PERMISSIVE
FOR ALL
TO bilko_app
USING (
invoice_id IN (
SELECT id FROM invoices
WHERE org_id = current_setting('app.current_org_id')::uuid
)
);
CREATE POLICY org_isolation ON expenses
AS PERMISSIVE
FOR ALL
TO bilko_app
USING (org_id = current_setting('app.current_org_id')::uuid);
CREATE POLICY org_isolation ON transactions
AS PERMISSIVE
FOR ALL
TO bilko_app
USING (org_id = current_setting('app.current_org_id')::uuid);
CREATE POLICY org_isolation ON bank_transactions
AS PERMISSIVE
FOR ALL
TO bilko_app
USING (
bank_account_id IN (
SELECT id FROM bank_accounts
WHERE org_id = current_setting('app.current_org_id')::uuid
)
);
CREATE POLICY org_isolation ON bank_accounts
AS PERMISSIVE
FOR ALL
TO bilko_app
USING (org_id = current_setting('app.current_org_id')::uuid);
CREATE POLICY org_isolation ON accounts
AS PERMISSIVE
FOR ALL
TO bilko_app
USING (org_id = current_setting('app.current_org_id')::uuid);
CREATE POLICY org_isolation ON contacts
AS PERMISSIVE
FOR ALL
TO bilko_app
USING (org_id = current_setting('app.current_org_id')::uuid);
-- BYPASS for migrations and admin tooling (Flyway runs as bilko_admin)
ALTER TABLE invoices FORCE ROW LEVEL SECURITY;
ALTER TABLE expenses FORCE ROW LEVEL SECURITY;
ALTER TABLE transactions FORCE ROW LEVEL SECURITY;
ALTER TABLE bank_transactions FORCE ROW LEVEL SECURITY;
ALTER TABLE bank_accounts FORCE ROW LEVEL SECURITY;
ALTER TABLE accounts FORCE ROW LEVEL SECURITY;
ALTER TABLE contacts FORCE ROW LEVEL SECURITY;
-- Flyway runs as bilko_admin (superuser bypasses RLS by default).
-- Explicit FORCE is belt-and-suspenders — admin role grants BYPASSRLS if needed.
-- Set connection middleware (Kotlin Exposed / HikariCP):
-- On each connection checkout:
-- SET LOCAL app.current_org_id = '<org_uuid_from_jwt>';
-- On connection return to pool:
-- SET LOCAL app.current_org_id = ''; -- or reset_config('app.current_org_id', true)
```
**Verification after Phase 2A:**
```sql
-- Rogue-role test (Proveo E2E + Securion audit):
SET ROLE bilko_app;
SET LOCAL app.current_org_id = '<hr_org_uuid>';
SELECT count(*) FROM invoices; -- must return only HR org rows
SET LOCAL app.current_org_id = '<rs_org_uuid>';
SELECT count(*) FROM invoices; -- must return only RS org rows
-- Cross-tenant access attempt:
SET LOCAL app.current_org_id = '<hr_org_uuid>';
SELECT * FROM invoices WHERE org_id = '<rs_org_uuid>'; -- must return 0 rows (PERMISSIVE blocks)
```
#### Phase 2B — Audit Log Partitioning (post-HR GA)
**Goal:** Partition the `logged_actions` audit table by `country_code` to enable
per-jurisdiction GDPR data extraction requests and enforce per-jurisdiction retention.
```sql
-- V18__audit_log_partitioning.sql (Phase 2B — post-HR GA)
-- Declarative partitioning by country_code
CREATE TABLE logged_actions_partitioned (
LIKE logged_actions INCLUDING ALL
) PARTITION BY LIST (country_code);
CREATE TABLE logged_actions_hr PARTITION OF logged_actions_partitioned
FOR VALUES IN ('HR');
CREATE TABLE logged_actions_rs PARTITION OF logged_actions_partitioned
FOR VALUES IN ('RS');
CREATE TABLE logged_actions_ba_fed PARTITION OF logged_actions_partitioned
FOR VALUES IN ('BA_FED');
CREATE TABLE logged_actions_ba_rs PARTITION OF logged_actions_partitioned
FOR VALUES IN ('BA_RS');
-- Retention policy enforcement (aligned with CountryPlugin.getRetentionRules()):
-- HR: 11 years (Zakon o računovodstvu NN 78/2015, čl. 10)
-- RS/BA: 10 years
-- Implemented as pg_cron job deleting rows WHERE action_tstamp_tx < now() - interval '11 years'
-- per partition.
-- country_code column backfilled from organizations.country via:
-- UPDATE logged_actions SET country_code = o.country
-- FROM organizations o WHERE o.id = logged_actions.org_id;
```
RLS policy for `logged_actions` (applied in Phase 2B):
```sql
CREATE POLICY org_isolation ON logged_actions_partitioned
AS PERMISSIVE
FOR ALL
TO bilko_app
USING (org_id = current_setting('app.current_org_id')::uuid);
```
#### Phase 2C — RESTRICTIVE + Retire Application-Layer Scoping (post-Securion Audit)
**Goal:** Convert PERMISSIVE policies to RESTRICTIVE. Remove ADR-005 application-layer
`WHERE org_id` middleware. RLS is the sole isolation mechanism.
**Gate conditions (all must be true before Phase 2C begins):**
1. Securion audit of Phase 2A policies completed — no critical findings
2. Automated rogue-role test suite passing in CI (Proveo — see Phase 2A verification above)
3. Zero cross-tenant RLS bypass incidents on stage for 30 consecutive days
4. CEO explicit sign-off for Phase 2C
```sql
-- V19__rls_restrictive.sql (Phase 2C — post Securion audit)
-- Convert PERMISSIVE → RESTRICTIVE on all tables
-- This is the point of no return: application layer WHERE org_id is retired after this.
DROP POLICY org_isolation ON invoices;
CREATE POLICY org_isolation ON invoices
AS RESTRICTIVE
FOR ALL
TO bilko_app
USING (org_id = current_setting('app.current_org_id')::uuid)
WITH CHECK (org_id = current_setting('app.current_org_id')::uuid);
-- Same pattern for expenses, transactions, bank_transactions, bank_accounts,
-- accounts, contacts, invoice_items (repeat for each table).
```
### 2.3 Versioned Chart of Accounts Table
The `chart_of_accounts` table stores jurisdiction-specific CoA entries with time-ranged
validity. This supports:
- Pravilnik revisions without code changes (ADR-bilko-003 §Layer 3, lines 122–143)
- Historical invoice accuracy (rate in force at transaction date, not current rate)
- `CountryPlugin.getChartOfAccountsDefaults()` seeding on org creation (ADR-015 §2.2)
```sql
-- Part of Flyway V17 or separate V17b (Phase 2A / 1H parallel)
CREATE TABLE chart_of_accounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
jurisdiction VARCHAR(8) NOT NULL, -- TaxJurisdiction enum value: 'HR', 'RS', 'BA_FED', 'BA_RS'
code VARCHAR(16) NOT NULL, -- e.g. '1300' (HR Kontni Plan), '204' (RS Pravilnik)
name VARCHAR(256) NOT NULL,
account_type VARCHAR(16) NOT NULL -- ASSET, LIABILITY, EQUITY, INCOME, EXPENSE
CHECK (account_type IN ('ASSET', 'LIABILITY', 'EQUITY', 'INCOME', 'EXPENSE')),
vat_treatment VARCHAR(64), -- e.g. 'STANDARD_RATE', 'EXEMPT', null for non-VAT accounts
valid_from DATE NOT NULL,
valid_to DATE, -- NULL = currently valid
version INT NOT NULL DEFAULT 1, -- increments per Pravilnik revision
notes TEXT, -- statutory reference e.g. "NN 78/2015, čl. 5"
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (jurisdiction, code, valid_from)
);
CREATE INDEX idx_coa_jurisdiction_date
ON chart_of_accounts (jurisdiction, valid_from, valid_to);
-- Query pattern: entries valid on a given transaction date
-- SELECT * FROM chart_of_accounts
-- WHERE jurisdiction = $1
-- AND valid_from <= $2
-- AND (valid_to IS NULL OR valid_to > $2)
-- ORDER BY code;
-- When Croatia raises PDV from 25% to 27% on 2027-01-01:
-- INSERT INTO chart_of_accounts (jurisdiction, code, name, account_type, vat_treatment, valid_from, version)
-- VALUES ('HR', '2400', 'PDV po stopi 27%', 'LIABILITY', 'STANDARD_RATE', '2027-01-01', 2);
-- UPDATE chart_of_accounts SET valid_to = '2026-12-31'
-- WHERE jurisdiction = 'HR' AND code = '2400' AND valid_to IS NULL AND version = 1;
-- No code change required.
```
**Seeding:** `CountryPlugin.getChartOfAccountsDefaults()` returns the list of entries
that Flyway data migrations insert into `chart_of_accounts` for each jurisdiction.
Flyway V18 (Phase 1H — separate from V17 RLS) seeds HR Kontni Plan entries.
### 2.4 Exchange Rate Precision Upgrade
**Current precision (CLAUDE.md database rules):** `NUMERIC(19,4)` for ALL monetary amounts.
**Upgrade required for FX rate columns specifically:**
Exchange rates require higher precision than invoice monetary amounts. Using `NUMERIC(19,4)`
for an exchange rate means EUR/RSD at 117.2350 is representable, but EUR/BAM at
1.95583 is stored as 1.9558 — a systematic rounding error that compounds across large
invoice volumes and cross-currency reconciliation.
**Decision:** FX rate columns upgrade to `NUMERIC(20,10)`. Monetary amount columns
(invoice totals, line amounts, tax amounts) remain `NUMERIC(19,4)`.
```sql
-- V17c__exchange_rate_precision.sql (Phase 2A parallel)
ALTER TABLE exchange_rates
ALTER COLUMN rate TYPE NUMERIC(20,10); -- was NUMERIC(19,4)
-- If an exchange_rate_history or similar snapshot table exists:
-- ALTER TABLE exchange_rate_history
-- ALTER COLUMN rate TYPE NUMERIC(20,10);
-- NEVER change invoice_items.unit_price, invoice_items.line_total,
-- transactions.amount, etc. — those remain NUMERIC(19,4).
-- Only rate/exchange_rate columns receive this upgrade.
```
**Invariant:** All monetary arithmetic (invoice totals, tax calculations, double-entry
postings) remains at `NUMERIC(19,4)`. The precision upgrade is scoped to the FX
rate storage layer only. Rounding when applying FX rates to amounts: round half-even
(banker's rounding) to 4 decimal places after multiplication.
---
## 3. Connection Middleware — Setting `app.current_org_id`
The RLS policies use `current_setting('app.current_org_id')::uuid`. This session
variable must be set on every database connection before any query executes.
**Pattern (Kotlin / Exposed / HikariCP):**
```kotlin
// apps/api/src/main/kotlin/no/alai/bilko/db/OrgContextInterceptor.kt (Phase 2A NEW)
/**
* Sets the PostgreSQL session variable `app.current_org_id` to the authenticated
* org's UUID before any database access.
*
* Called from the Ktor routing pipeline after JWT validation, before the
* database transaction opens.
*
* Must reset after the request completes — use try/finally or Ktor plugin lifecycle.
*/
fun setOrgContext(orgId: UUID) {
transaction {
exec("SET LOCAL app.current_org_id = '${orgId}'")
}
}
fun clearOrgContext() {
transaction {
exec("RESET app.current_org_id")
// or: exec("SET LOCAL app.current_org_id = ''")
}
}
```
**Failure mode:** If `app.current_org_id` is not set, `current_setting('app.current_org_id')`
throws an error in PostgreSQL (by default). To make it return NULL instead (for Flyway
admin connections that do not set the variable):
```sql
-- In V17 migration, set default:
ALTER DATABASE bilko_demo SET app.current_org_id = '';
```
And in the policy, guard against empty string:
```sql
USING (
CASE WHEN current_setting('app.current_org_id', true) = ''
THEN false -- deny if not set
ELSE org_id = current_setting('app.current_org_id', true)::uuid
END
)
```
The `true` parameter to `current_setting()` makes it return NULL rather than throw
when the variable is not set.
---
## 4. Migration Schedule
| Phase | Flyway Version | Target | Blocking |
| ---------- | --------------------------------------------------- | --------------------------- | ------------------------------------------- |
| Phase 1H.1 | V16: `organizations.country` NOT NULL + CHECK | HR enum expansion (ADR-015) | ADR-015 accepted |
| Phase 2A | V17: PERMISSIVE RLS + CoA table + FX rate precision | Stage only | CEO sign (this ADR) |
| Phase 2A | V17 seed: HR Kontni Plan data | Stage only | V17 + PluginHR.getChartOfAccountsDefaults() |
| Phase 2B | V18: audit log partitioning | Post-HR GA | Securion review |
| Phase 2C | V19: RESTRICTIVE + retire ADR-005 app scoping | Post-Securion audit | Securion audit pass + CEO sign |
All migrations use Flyway's expand/contract pattern. No migration modifies data in a way
that cannot be reversed by a subsequent compensating migration. Backward compatibility
is required across all rolling deployments.
---
## 5. Consequences
### 5.1 Positive
1. **Defence in depth.** Even if a developer introduces a missing `WHERE org_id` in a new
route, RLS at the database layer prevents cross-tenant data exposure.
2. **GDPR jurisdiction extraction.** With `country_code` on `logged_actions` (Phase 2B),
a request from Croatian DPA for "all data held on Croatian entities" is a single
partition query, not a full-table scan with a filter.
3. **Audit surface.** Securion can review one set of RLS policies rather than auditing
every application route for correct scoping.
4. **Option D readiness.** If ADR-023 §6 triggers (e.g., first paying HR customer), the
same RLS DDL applies to the per-country databases without change. Migration path is
not blocked by this ADR.
### 5.2 Negative
1. **Connection middleware requirement.** Every DB connection must set `app.current_org_id`
before any query. Forgetting this in a new service or background job will cause all
queries to return 0 rows (PERMISSIVE) or error (RESTRICTIVE). Mitigated by integration
tests that verify the context middleware fires.
2. **Flyway admin bypass.** Flyway and admin tooling must run as a role that bypasses RLS
(`bilko_admin` with BYPASSRLS). This role must be kept tightly restricted — it is
a privilege escalation path.
3. **Phase 2A adds overhead.** Each query now evaluates an additional predicate. At current
scale (0 paying customers) the overhead is immeasurable. Monitor p95 query latency
after Phase 2A migration on stage.
### 5.3 Risks
1. **GDPR data residency.** Croatian entity data in Cloud SQL europe-north1 (Finland) is
legally compliant (EU/EEA). If a future HR DPA contract specifies Frankfurt, a regional
migration is required. This ADR does not block that migration.
2. **RLS policy gap.** An incorrect USING clause (e.g., JOIN condition that broadens
the scope) could expose cross-tenant data. **Mitigation:** Securion audit before
Phase 2C (RESTRICTIVE), automated rogue-role test in CI.
3. **Migration synchronization.** A Flyway migration failure mid-run leaves all markets
degraded. All V17+ migrations must be backward-compatible and use expand/contract
pattern. If V17 fails, rollback is: `DROP POLICY` + `ALTER TABLE ... DISABLE ROW LEVEL SECURITY`.
---
## 6. References
| Reference | Path | Lines |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------- |
| ADR-bilko-001 (ancestor draft, absorbed by this ADR) | `~/system/specs/bilko-multi-market-architecture-plan/ADR-bilko-001-multi-tenant-architecture.md` | 1–162 |
| ADR-bilko-003 §Layer 3 (versioned CoA model) | `~/system/specs/bilko-multi-market-architecture-plan/ADR-bilko-003-market-abstraction-layers.md` | 122–143 |
| ADR-023 §6 (single-DB migration triggers — not fired) | `docs/architecture/ADR-023-TRANSITIONAL-MULTI-MARKET-ROUTING.md` | 166–176 |
| Plan v3 §4a (Option D not triggered — evidence) | `~/system/specs/bilko-multi-market-architecture-plan-v3-2026-05-11.md` | 100–108 |
| Plan v3 §4c (RLS timing — PERMISSIVE before Phase 1H) | `~/system/specs/bilko-multi-market-architecture-plan-v3-2026-05-11.md` | 135–145 |
| Plan v3 §4d (EU data residency does not block HR GA) | `~/system/specs/bilko-multi-market-architecture-plan-v3-2026-05-11.md` | 179–183 |
| ADR-015 §2.1 (TaxJurisdiction enum — `country_code` values) | `docs/architecture/ADR-015-FOUR-JURISDICTION-PLUGIN.md` | §2.1 |
| Test drift memo (cross-tenant 500 leaks, Round 12.1/12.5) | `~/.claude/projects/-Users-makinja/memory/project_bilko_test_strategy_drift_2026-05-10.md` | — |
---
## 7. Approval
**Architecture status:** Accepted (Phase 0' ADR consolidation)
**CEO sign status:** SIGNED 2026-05-11 — Phase 2A V17 Flyway PERMISSIVE migration authorized for stage. Phase 2C RESTRICTIVE flip remains gated on Securion audit + 30-day soak per §4 schedule.
This ADR records the architectural decision. The CEO signature below is the gate for
execution of Phase 2A database migrations. It is not a gate for writing this document
or for Phase 1H code work (CountryPlugin, PluginHR, DI wiring).
| Role | Sign | Date |
| ------------------------------------- | ------------------------------------------------------------- | ---------- |
| Architecture Lead (Petter Graff) | Signed | 2026-05-11 |
| Database Architecture (Bruce Momjian) | Signed | 2026-05-11 |
| CEO (Alem Bašić) | **SIGNED — session f73dafab, transcript "ok adr17 odobreno"** | 2026-05-11 |
---
## 8. Document History
| Date | Author | Change |
| ---------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2026-04-22 | ALAI / ADR-bilko-001 | Initial draft (multi-tenant architecture options analysis) |
| 2026-05-11 | Bruce Momjian / Petter Graff | Promoted from ADR-bilko-001 draft; ID changed to ADR-017; DDL examples added; versioned CoA DDL added; NUMERIC(20,10) FX precision noted; Phase 2B audit log partitioning added; connection middleware pattern added; CEO sign gate formalised. MC #100362. |
| 2026-05-11 | John (AI Director) | CEO Alem Bašić signed ADR-017 via session f73dafab ("ok adr17 odobreno"). Phase 2A V17 Flyway PERMISSIVE migration authorized for stage. Status header + §7 approval table updated. Unblocks Bruce Momjian dispatch for Phase 2A. |
No comments to display
No comments to display