데이터 격리 모델
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 실행 시:
upgrade_shared()→ shared schema 마이그- 등록된 모든 entity 에 대해
upgrade_entity(entity_id)→ entity schema 마이그
Tier 3 — Row-level (선택적)
magnet 같이 multi-tenant 단일 schema 에서 RLS 사용:
-- 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 = <id> 주입. 사용자가 잘못된 tenant_id 로는 read/write 불가.
frame 은 schema-per-entity 가 더 강한 격리 (cross-schema 권한 자체 분리), magnet 은 RLS 가 멀티 brand 통합 운영에 더 적합 → 서비스 별 적절한 격리 모델 선택.
audit_log — 모든 쓰기 추적
각 entity schema 에 audit_log 테이블이 있으며, 모든 쓰기 (insert/update/delete) 가 자동 기록됩니다.
-- 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 로 암호화됩니다.
# 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/filesbind 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가지 검사를 수행:
- balance —
SUM(debit) == SUM(credit)per journal - journal — 모든 journal_line 이 valid account 참조
- account — chart-of-accounts 와 journal_line 의 일관성
- fiscal_period — period 의 starting/ending balance 정합
cron 으로 매일 자동 실행 + Slack alert.