<!-- canonical: https://docs.axelabs.ai/architecture/data -->
<!-- source: content/architecture/data.mdx -->

---
title: 데이터 격리 모델
description: schema-per-entity, customer 격리, audit_log, PII 암호화.
---

# 데이터 격리 모델

AXE Labs 는 **3-tier 데이터 격리**를 사용합니다:

| Tier | 격리 단위 | 구현 |
|---|---|---|
| 1. OS-level | customer | macmini 1대 = customer 1개 |
| 2. Database schema | entity (회사) | PostgreSQL schema-per-entity |
| 3. Row-level (일부) | tenant_id | RLS (magnet 등에서) |

## Tier 1 — OS-level (customer)

각 customer 는 자기 macmini 위에서 frame · blueprint 등 동일 스택을 실행합니다. customer 간 cross-talk 없음.

| customer | macmini | DB | schema 셋 |
|---|---|---|---|
| axe | `axe-macmini` | frame-postgres | shared + axec + axev + axtest |
| realchoice | `realchoice-macmini` (예정) | (별도) | shared + realchoice |

backup 도 macmini 별로 분리되며, customer 간 ring backup 은 **운영자 명시 승인 + 양방향 SSH 합의** 후에만.

## Tier 2 — Schema-per-entity (frame)

frame 의 PostgreSQL 안에는:

```
frame DB
├── shared (cross-entity)
│   ├── entity                ← 등록된 entity 메타 (axec, axev, realchoice)
│   ├── format_profile        ← 파일 포맷 캐시 (vendor + column_signature)
│   ├── account_template      ← 표준 계정과목 (KSME)
│   ├── idempotency_record    ← 멱등 키
│   └── oauth_authorization_codes  ← OAuth proxy (D-ops-15, 현재 dormant)
│
├── axec (개별 entity schema)
│   ├── account, journal, journal_line
│   ├── raw_transaction, bank_account, source_file
│   ├── fiscal_period, open_item
│   ├── evidence, audit_log
│   └── ... (전체 16 개 migration)
│
├── axev (axec 와 동일 구조)
│   └── ...
│
└── axtest (테스트 전용)
    └── ...
```

**Cross-entity 쿼리 불가**: 각 MCP tool 호출이 `SET search_path TO {entity_id}, shared` 로 격리. 사용자가 `axec` 권한만 가지면 `axev` schema 접근 불가.

### Migration 패턴

Alembic 의 dual-env:

| 디렉토리 | 적용 시점 | 대상 |
|---|---|---|
| `alembic/versions/` | `frame migrate` 시 1회 | `shared` schema |
| `alembic/entity_versions/` | `frame register-entity` 시 + 매 migrate | 각 entity schema (axec, axev, ...) |

`frame migrate` 실행 시:
1. `upgrade_shared()` → shared schema 마이그
2. 등록된 모든 entity 에 대해 `upgrade_entity(entity_id)` → entity schema 마이그

## Tier 3 — Row-level (선택적)

`magnet` 같이 multi-tenant 단일 schema 에서 RLS 사용:

```sql
-- magnet sql/050_multi_tenant_baseline.sql
ALTER TABLE campaign_metrics_daily ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON campaign_metrics_daily
  USING (tenant_id = current_setting('app.tenant_id')::bigint);
```

DB 연결 단계에서 `SET app.tenant_id = &lt;id&gt;` 주입. 사용자가 잘못된 tenant_id 로는 read/write 불가.

frame 은 schema-per-entity 가 더 강한 격리 (cross-schema 권한 자체 분리), magnet 은 RLS 가 멀티 brand 통합 운영에 더 적합 → 서비스 별 적절한 격리 모델 선택.

## audit_log — 모든 쓰기 추적

각 entity schema 에 `audit_log` 테이블이 있으며, **모든 쓰기 (insert/update/delete)** 가 자동 기록됩니다.

```sql
-- entity-side audit_log 의 컬럼
audit_log (
  id              bigserial,
  table_name      text,
  op              text,        -- INSERT | UPDATE | DELETE
  old_data        jsonb,
  new_data        jsonb,
  actor           text,        -- frame.actor session var
  entity_context  text,        -- 'axec'
  ts              timestamptz default now()
)
```

`frame.actor` 는 entity_session context manager 에서 설정되며, MCP tool 호출의 `TokenClaims.sub` 가 자동으로 들어갑니다.

### Append-only 강제

`raw_transaction`, `journal_line` 같은 핵심 테이블에는 DELETE/UPDATE 권한이 DB 사용자 단에서 REVOKE 되어 있습니다. 잘못된 분개는 `reverse_journal` 로 역분 (새 journal 생성), 절대 수정/삭제 안 함.

## PII 암호화 (pgcrypto)

개인정보 (security_holder, audit log 의 일부 raw_data) 는 `pgcrypto.pgp_sym_encrypt` 로 entity 별 별도 passphrase 로 암호화됩니다.

```yaml
# docker-compose.yml env
FRAME_PII_PASSPHRASE_AXEC: ${FRAME_PII_PASSPHRASE_AXEC:-}
FRAME_PII_PASSPHRASE_AXEV: ${FRAME_PII_PASSPHRASE_AXEV:-}
FRAME_PII_PASSPHRASE_REALCHOICE: ${FRAME_PII_PASSPHRASE_REALCHOICE:-}
FRAME_PII_SALT_DIR: /root/.frame
```

각 entity 의 passphrase 는 운영자 Keychain (`security add-generic-password`) 에 저장되고 컨테이너 launch wrapper 가 env 로 주입. **passphrase 분실 = PII 영구 손실** (이는 의도된 설계 — 운영자가 DB dump 만으로는 PII 복원 불가).

## Evidence blob 저장 (content-addressed file store, D-frame-2)

큰 파일 (evidence PDF·통장 xls·세금계산서 등) 은 Postgres bytea 가 아니라 **content-addressed file store** `.local/files/<hash[:2]>/<hash>/<filename>` 에 저장. DB 의 `evidence`/`source_file` row 는 `content_hash` (SHA-256) + **portable** `storage_url` (relative `<hash[:2]>/<hash>/<filename>`) 만 보유.

- **컨테이너 마운트**: host `.local/files` ↔ 컨테이너 `/app/.local/files` bind mount (`FRAME_FILE_STORAGE_PATH`). read 시 `frame.ingest.pipeline.resolve_storage_path()` 가 portable url 을 live store 기준 absolute 로 매핑 → host·컨테이너 어디서 실행해도 같은 blob 해석.
- **금지**: `local://<host 절대경로>` (외부 OneDrive/Downloads 포인터 — 컨테이너 read 불가 + rename/move 시 깨짐), CWD 의존 absolute 경로 (host `/Users/...` vs 컨테이너 `/app/...` 불일치). 모든 ingest write-path (`pipeline`/`card`/`fund`/`hometax`/`entity_meta`/`resolution`/`ops.evidence`) 가 `_store_file_locally` 로 통일.
- **무결성 probe**: `GET /frame/health/storage` 가 모든 entity schema 의 evidence storage_url 을 resolve 해 blob 존재 확인 — bind-mount 회귀로 blob 이 사라지면 (2026-05-14 incident) 503 + missing-list. blob 은 절대 삭제 안 함 (append-only 정신).

## 정합성 검사

`frame integrity-check --entity axec` 가 4가지 검사를 수행:

1. **balance** — `SUM(debit) == SUM(credit)` per journal
2. **journal** — 모든 journal_line 이 valid account 참조
3. **account** — chart-of-accounts 와 journal_line 의 일관성
4. **fiscal_period** — period 의 starting/ending balance 정합

cron 으로 매일 자동 실행 + Slack alert.
