프로젝트

일반

사용자정보

Actions

이슈 #254

진행중
CP JL

8-2. 데이터베이스 스키마 설계 (사용자. 앱. 일정)

이슈 #254: 8-2. 데이터베이스 스키마 설계 (사용자. 앱. 일정)

Chris Park이(가) 약 5시간 전에 추가함.

상태:
진행
우선순위:
높음
담당자:
목표버전:
시작일:
2026/04/28
완료일:
2026/04/29 (약 6달 지연)
진척도:

0%

추정시간:

설명

PostgreSQL/MongoDB 스키마


연결된 일감 6 (6 건 진행 중 — 모두 미완료)

다음 일감과 관련됨: 이슈 #432: Engine + ConnectNode 포팅 — Synth / Dispatcher / Scheduler / VNodeCreditor + bidi 스트림 완성해결James Lee2026/04/172026/04/17

Actions
다음 일감과 관련됨: 이슈 #433: VfsService 포팅 — per-account 가상 파일시스템 (Read/Write/List/Mkdir/Delete/Stat/Exists/Lock/Unlock)해결Henry Park2026/05/11

Actions
다음 일감과 관련됨: 이슈 #434: [Go backport] Supabase GoTrue 인증 전환 — Phase 1: 인프라 + 인증 코어해결Henry Park2026/05/11

Actions
다음 일감과 관련됨: 이슈 #435: [Go backport] AGX 회계 user_id 종속 전환 — Phase 2해결Henry Park2026/05/122026/05/12

Actions
다음 일감과 관련됨: 이슈 #437: [Go backport] FriendsService user_id 전환 — Phase 4해결Henry Park2026/05/142026/05/14

Actions
다음 일감과 관련됨: 이슈 #438: [Go backport] Legacy wallet_address 컬럼 제거 — Phase 5 (final cleanup)해결Henry Park2026/05/152026/05/15

Actions

CP Chris Park이(가) 7달 전에 변경 Actions #1

  • 프로젝트을(를) 14에서 Alphea App(으)로 변경되었습니다.
  • 목표버전을(를) 6월 중간 버전(으)로 지정되었습니다.

JL James Lee이(가) 7달 전에 변경 Actions #2

  • 상태을(를) 신규에서 진행(으)로 변경되었습니다.

JL James Lee이(가) 7달 전에 변경 Actions #3

초기 스키마 도입 (accounts, wallets, nodes)

사용자 인증(#256) 사전 작업으로 PostgreSQL 초기 스키마를 생성했습니다.

  • Repository: alphea-ai-edge
  • Commit: commit:server|2451853
  • Migration: migrations/0001_initial_schema.sql
  • Runtime: PostgreSQL 17 (docker-compose, postgres:17-alpine)
  • Migrator: sqlx::migrate! 매크로 — 서버 기동 시 자동 적용, 버전은 _sqlx_migrations 테이블이 추적

테이블 1. accounts

사용자 계정. 지갑 서명 인증과 (선택적) 이메일/비밀번호 로그인을 모두 지원.

Column Type Null Notes
id BIGSERIAL NOT NULL PK
wallet_address TEXT NOT NULL UNIQUE, 인증 키
email TEXT NULL UNIQUE, 이메일 복구용
username TEXT NULL 표시명
password_hash TEXT NULL Argon2 해시 (향후 단계)
profile_icon TEXT NULL 아이콘 URL
referral_code TEXT NOT NULL UNIQUE, 친구 초대 코드
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()

Indexes: idx_accounts_wallet_address, idx_accounts_email, idx_accounts_referral_code

테이블 2. wallets

계정별 AGX 잔액 및 누적 수익. accounts.id 와 1:1.

Column Type Null Notes
id BIGSERIAL NOT NULL PK
account_id BIGINT NOT NULL FK → accounts.id ON DELETE CASCADE, UNIQUE
balance DOUBLE PRECISION NOT NULL DEFAULT 0
total_earned DOUBLE PRECISION NOT NULL DEFAULT 0
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()

Indexes: idx_wallets_account_id

테이블 3. nodes

엣지 / 모바일 / 가상 컴퓨팅 노드 레지스트리. Verify RPC 에서 device_id + device_meta 가 주어지면 자동으로 모바일 노드 행을 생성할 예정.

Column Type Null Default Notes
id BIGSERIAL NOT NULL PK
node_id TEXT NOT NULL UNIQUE
name TEXT NOT NULL ''
node_type INTEGER NOT NULL 1=EDGE, 2=MOBILE, 3=VIRTUAL
owner_wallet_address TEXT NOT NULL 소유자 지갑
auth_mode INTEGER NOT NULL 2 1=INTERNAL, 2=SIGNATURE
status INTEGER NOT NULL 1 1=INACTIVE, 2=ACTIVE, 3=BUSY, 4=DRAINING, 5=BANNED
tier INTEGER NOT NULL 1 1=LOW, 2=MID, 3=HIGH
role INTEGER NOT NULL 1 1=WORKER, 2=COMMITTEE
max_concurrent_jobs INTEGER NOT NULL 5
throughput_hint INTEGER NOT NULL 100
running_jobs INTEGER NOT NULL 0
contributed_storage_gb INTEGER NOT NULL 0
contributed_cpu_cores INTEGER NOT NULL 0
network_bandwidth_mbps INTEGER NOT NULL 0
score INTEGER NOT NULL 50 디스패처 점수
success_rate_1m DOUBLE PRECISION NOT NULL 1.0 최근 1분 성공률
avg_exec_ms DOUBLE PRECISION NOT NULL 0
p95_exec_ms DOUBLE PRECISION NOT NULL 0
timeouts_1m INTEGER NOT NULL 0
total_jobs_completed BIGINT NOT NULL 0
total_jobs_failed BIGINT NOT NULL 0
device_id TEXT NULL 모바일/엣지 디바이스 식별자
device_meta JSONB NULL DeviceMeta proto 직렬화 (platform, model 등)
device_platform TEXT NULL ios / android / edge 등
pool_group TEXT NULL 풀 그룹핑용
last_heartbeat_at TIMESTAMPTZ NULL
last_assigned_at TIMESTAMPTZ NULL
created_at TIMESTAMPTZ NOT NULL NOW()
updated_at TIMESTAMPTZ NOT NULL NOW()

Indexes: idx_nodes_owner_wallet, idx_nodes_status, idx_nodes_node_type, idx_nodes_device_id

외래 키 관계

accounts (id) ──┐
                └── wallets.account_id  (ON DELETE CASCADE, UNIQUE)

nodes.owner_wallet_address  ────> accounts.wallet_address  (논리적 참조 — FK 제약은 걸지 않음)

nodes.owner_wallet_address 는 FK 제약을 걸지 않았습니다. 지갑 주소 기반 조회가 빈번하고, 아직 계정이 생성되지 않은 상태에서도 노드 사전 등록이 가능해야 하기 때문입니다.

레거시 프로젝트와의 차이점

  • 레거시 Go 프로젝트의 수동 마이그레이션 관리(_meta.schema_version) 대신 sqlx 표준 마이그레이션 테이블(_sqlx_migrations)을 사용
  • enum 값은 레거시와 동일한 숫자 매핑 유지 (proto 값과 1:1 대응)
  • device_meta 는 JSONB 로 저장 → 향후 device platform/model/timezone 등 유연한 쿼리 가능

JL James Lee이(가) 7달 전에 변경 Actions #4

Step B 관련 참조 (DB 스키마 변경 없음)

  • Commit: commit:server|430589e
  • 관련 이슈: #256 (사용자 인증 포팅)

이번 커밋은 순수 도메인 로직(src/domain/auth/)만 추가했으며 DB 스키마 변경은 없습니다. 다만 Step A(#254 의 초기 스키마 — commit:server|2451853)의 테이블들과 이번에 추가된 타입 간 매핑을 기록해 둡니다.

스키마 ↔ 도메인 타입 매핑

테이블 / 컬럼 도메인 타입 / 로직
accounts.wallet_address Keypair::wallet_address — 0x + keccak256(pubkey[1..])[12..] hex
accounts.password_hash crypto::hash_password(password, PASSWORD_SALT) — SHA256(pw+salt) hex
accounts.email derive_keypair_from_credentials(email, password, AUTH_SALT) 의 입력
nodes.device_id, nodes.device_meta (Step C 에서 Verify RPC 가 모바일 노드 자동 등록 시 기록)

다음 변경 예정

Step C 에서 AuthService 핸들러를 구현할 때 nodes.device_id + nodes.device_meta 컬럼이 처음으로 쓰기 경로에 연결됩니다. 그 시점에 마이그레이션은 필요 없지만, 실제 JSONB 페이로드 포맷(DeviceMeta proto 직렬화)은 이 이슈에 다시 기록할 예정입니다.

JL James Lee이(가) 7달 전에 변경 Actions #5

Step C 관련 DB 작업 (스키마 변경 없음, 쿼리 코드 추가)

  • Commit: commit:server|9b7ffb3
  • 관련 이슈: #256 (AuthService 구현)
  • 마이그레이션 변경: 없음 — Step A 에서 도입한 accounts / wallets / nodes 테이블을 그대로 사용

이번 커밋은 Step A 에서 만들어 둔 스키마에 대해 처음으로 쓰기 경로를 연결한 변경입니다. 새로 추가된 쿼리 코드와 각 테이블에 실제로 어떻게 데이터가 들어가는지 정리합니다.

새로 추가된 store 모듈

  • src/store/accounts.rs
  • src/store/wallets.rs
  • src/store/nodes.rs
  • src/store/mod.rs 를 submodule 구조로 재편성, 기존 Database 래퍼는 src/store/database.rs 로 이동

전부 sqlx 의 query_as::<_, T>(sql).bind(...) runtime API 를 사용합니다. sqlx::query!/query_as! 매크로는 빌드 타임에 DB 접속을 요구하므로 현재는 쓰지 않으며, #[derive(FromRow)] 만 macros feature 로 활성화했습니다.

각 테이블에 쓰기 경로가 생긴 쿼리

1. accounts — src/store/accounts.rs

함수 SQL
get_by_wallet(pool, wallet) SELECT ... FROM accounts WHERE wallet_address = $1
create(pool, wallet, email, referral_code) INSERT INTO accounts (wallet_address, email, referral_code) VALUES ($1, $2, $3) RETURNING *
touch_updated_at(pool, wallet) UPDATE accounts SET updated_at = NOW() WHERE wallet_address = $1
  • referral_code 는 UUID v4 앞 8자를 사용 (uuid::Uuid::new_v4().simple().to_string()[..8])
  • 새 지갑으로 Verify 를 호출하면 자동으로 create 가 실행되어 계정이 생성됨
  • updated_at 은 Verify 성공 시 fire-and-forget 으로 갱신 (레거시의 "last active" 트래킹)

2. wallets — src/store/wallets.rs

함수 SQL
ensure(pool, account_id) INSERT INTO wallets (account_id, balance, total_earned) VALUES ($1, 0, 0) ON CONFLICT (account_id) DO NOTHING
get_balance(pool, wallet) SELECT w.balance, w.total_earned FROM wallets w JOIN accounts a ON a.id = w.account_id WHERE a.wallet_address = $1
  • ensure 는 멱등적. 계정 생성 직후 항상 호출됨
  • get_balance 는 지갑 행이 없으면 WalletBalance::default() (0, 0) 반환

3. nodes — src/store/nodes.rs

함수 설명
node_id_for(wallet, device_id) rn_{short}_{device_id} 포맷 (레거시와 동일). short = wallet[2:10]
device_meta_to_json(meta) 프로토 DeviceMeta → serde_json::Value (빈 필드 제외, 모두 비면 None)
ensure_mobile_node(pool, wallet, device_id, device_meta) 디바이스가 있을 때만 mobile 노드 upsert. 없으면 빈 문자열 반환

ensure_mobile_node 의 upsert SQL:

INSERT INTO nodes (
    node_id, name, node_type, owner_wallet_address, auth_mode, status, tier, role,
    device_id, device_meta, device_platform
)
VALUES ($1, $2, $3, $4, 1, $5, $6, 1, $7, $8, $9)
ON CONFLICT (node_id) DO UPDATE SET
    device_meta = EXCLUDED.device_meta,
    device_platform = EXCLUDED.device_platform,
    status = $5,
    updated_at = NOW()

바인딩 값:

  • $3 node_type = NodeType::Mobile as i32 = 2
  • $5 status = NodeStatus::Active as i32 = 2
  • $6 tier = NodeTier::Low as i32 = 1
  • auth_mode = 1 (INTERNAL), role = 1 (WORKER) 고정

Rust 의 proto enum 값은 Step A 에서 정의한 integer 컬럼과 1:1 매칭됩니다.

nodes.device_meta JSONB 직렬화 스펙

DeviceMeta proto 필드가 비어있지 않은 것만 포함되며, 키 네이밍은 레거시 (camelCase) 와 동일합니다.

proto 필드 JSON 키
platform platform
model model
screen_resolution screenResolution
app_version appVersion
timezone timezone

모든 필드가 비어 있으면 NULL 이 저장됩니다.

End-to-End 검증으로 확인된 실제 영속화 결과

examples/auth_roundtrip.rs 로 Challenge → Verify 전체를 돌린 직후의 DB 상태:

            wallet_address             | referral_code
----------------------------------------+---------------
 0x7a72e61aaadd48917e5845d41f0f7575831c56af | fb9f95c2

          node_id           | node_type |            owner_wallet_address            |   device_id    |                       device_meta                       | device_platform
----------------------------+-----------+--------------------------------------------+----------------+----------------------------------------------------------+-----------------
 rn_7a72e61a_demo-device-42 |         2 | 0x7a72e61aaadd48917e5845d41f0f7575831c56af | demo-device-42 | {"model":"iPhone 15 Pro","platform":"ios", ...}          | ios
  • node_id 포맷 레거시 완전 매칭
  • node_type = 2 → MOBILE
  • device_meta JSONB 에 5개 필드 모두 직렬화 확인

다음 변경 예정

  • SetPassword / Login / EmailRecover 구현 시 accounts.password_hash 및 accounts.email 컬럼 쓰기 경로 활성화 예정 (현재 스키마로 충족)
  • JWT 인터셉터 구현 시 DB 변경 없음
  • 향후 실제 노드 등록 (엣지 노드 직접 등록) 을 구현하면 nodes.auth_mode = 2 (SIGNATURE) 사용 경로가 추가됨

이 이슈 (#254) 에는 위 후속 작업에서 스키마가 바뀔 때마다 계속 기록해 나갑니다.

JL James Lee이(가) 7달 전에 변경 Actions #6

Section 17 Step 1 관련 DB 작업 (스키마 변경 없음, 읽기 쿼리 추가)

  • Commit: commit:server|7828037
  • 관련 이슈: #310 (17-1. 노드 목록 조회), #311 (17-2. 노드 상세 정보)
  • 마이그레이션 변경: 없음 — Step A 에서 정의한 nodes 테이블 그대로 사용

nodes 테이블에 대한 읽기 쿼리 경로를 처음으로 연결한 변경입니다. 모든 컬럼이 한꺼번에 노출되므로, DB 구조와 proto NodeInfo 매핑을 이 이슈에 확정 기록합니다.

새로 추가된 쿼리

1. get_by_id(pool, node_id) → Option<NodeRow>

SELECT {모든 컬럼} FROM nodes WHERE node_id = $1

2. list(pool, node_type?, status?, tier?, limit, offset) → Vec<NodeRow>

SELECT {모든 컬럼} FROM nodes
WHERE ($1::int IS NULL OR node_type = $1)
  AND ($2::int IS NULL OR status    = $2)
  AND ($3::int IS NULL OR tier      = $3)
ORDER BY created_at DESC
LIMIT $4 OFFSET $5
  • 세 필터 모두 optional. NULL 이면 해당 조건 비활성화
  • 핸들러에서 0 (UNSPECIFIED) enum 값도 None 으로 정규화 후 전달

3. count_filtered(pool, node_type?, status?, tier?) → i64

동일한 WHERE 절로 COUNT(*). ListNodes 응답의 PageInfo.total 을 채우기 위함.

NodeRow ↔ nodes 테이블 완전 매핑

모든 30개 컬럼이 #[derive(FromRow)] 로 1:1 매핑됩니다.

컬럼 Rust 타입 비고
id i64 PK
node_id String UNIQUE
name String
node_type i32 proto NodeType enum 값
owner_wallet_address String
auth_mode i32 proto AuthMode enum 값
status i32 proto NodeStatus enum 값
tier i32 proto NodeTier enum 값
role i32 proto NodeRole enum 값
max_concurrent_jobs i32
throughput_hint i32
running_jobs i32
contributed_storage_gb i32
contributed_cpu_cores i32
network_bandwidth_mbps i32
score i32
success_rate_1m f64
avg_exec_ms f64
p95_exec_ms f64
timeouts_1m i32
total_jobs_completed i64
total_jobs_failed i64
device_id Option<String>
device_meta Option<serde_json::Value> JSONB
device_platform Option<String>
pool_group Option<String>
last_heartbeat_at Option<DateTime<Utc>> TIMESTAMPTZ
last_assigned_at Option<DateTime<Utc>> TIMESTAMPTZ
created_at DateTime<Utc> TIMESTAMPTZ
updated_at DateTime<Utc> TIMESTAMPTZ

JSONB device_meta ↔ proto DeviceMeta 왕복 검증

Step C 에서 쓰기 방향 (device_meta_to_json) 이 구현됐고, 이번에 읽기 방향 (device_meta_from_json) 이 추가되어 왕복이 완성되었습니다.

이전 세션에서 AuthService.Verify 로 저장된 실제 데이터를 ListNodes / GetNode 로 조회했을 때:

"deviceMeta": {
  "platform": "ios",
  "model": "iPhone 15 Pro",
  "screenResolution": "1290x2796",
  "appVersion": "0.1.0",
  "timezone": "Asia/Seoul"
}

5개 필드 전부 손실 없이 proto 로 복원됨을 확인했습니다.

다음 변경 예정 (Section 17)

  • #312 (17-3) — RegisterNode 구현 시 nodes 테이블 쓰기 경로가 모바일 외 타입 (EDGE / 가상) 으로 확장됨. 스키마 변경 없음 예상
  • #313 (17-4) — 노드 삭제 구현 시 soft-delete 를 위한 archived_at TIMESTAMPTZ 컬럼 추가 마이그레이션 도입 예정. 이 시점에 이 이슈에 다시 기록
  • #314 (17-5) — 수익 계산은 job_receipts / aggregated_earnings 테이블이 필요. 이 테이블들은 아직 스키마가 없으므로 새 마이그레이션 필요. 이 시점에 이 이슈에 상세 기록

JL James Lee이(가) 7달 전에 변경 Actions #7

Section 17 Step 2 관련 DB 작업 (스키마 변경 없음, upsert 쿼리 추가)

  • Commit: commit:server|920cd5c
  • 관련 이슈: #312 (17-3. 노드 추가 API)
  • 마이그레이션 변경: 없음 — Step A 에서 정의한 nodes 테이블 그대로 사용

nodes 테이블에 대한 두 번째 쓰기 경로가 추가되었습니다. Step C 의 ensure_mobile_node (AuthService/Verify 자동 생성) 에 이어, 이번에는 owner-signed 명시적 등록용 upsert 가 붙었습니다.

새로 추가된 쿼리

upsert_registered(pool, node_id, owner_wallet, node_type, tier, device_meta) → NodeRow

INSERT INTO nodes (
    node_id, name, node_type, owner_wallet_address, auth_mode, status, tier, role,
    device_meta, device_platform
)
VALUES ($1, $2, $3, $4, 2, $5, $6, 1, $7, $8)
ON CONFLICT (node_id) DO UPDATE SET
    device_meta = EXCLUDED.device_meta,
    device_platform = EXCLUDED.device_platform,
    status = $5,
    updated_at = NOW()
RETURNING {SELECT_COLUMNS}

바인딩:

  • $3 node_type = 요청 (EDGE=1, MOBILE=2, VIRTUAL=3)
  • $5 status = NodeStatus::Active as i32 = 2
  • $6 tier = 요청 (LOW=1, MID=2, HIGH=3)
  • auth_mode = 2 (SIGNATURE) 고정 — 명시적 등록이므로 mobile 자동 등록과 구분됨
  • role = 1 (WORKER) 고정

ensure_mobile_node vs upsert_registered 비교

항목 ensure_mobile_node (Step C) upsert_registered (이번 단계)
호출 주체 AuthService.Verify — 모바일 앱 로그인 플로우 NodeService.RegisterNode — 사용자 직접 노드 추가
node_id 생성 rn_{short_wallet}_{device_id} 자동 생성 클라이언트 제공 (자유 문자열)
node_type 항상 MOBILE (2) 요청에 따라 EDGE (1) / MOBILE (2) / VIRTUAL (3)
tier 항상 LOW (1) 요청에 따라 LOW/MID/HIGH
auth_mode INTERNAL (1) — 앱 토큰으로 노드를 대신 대리 인증 SIGNATURE (2) — 지갑 소유자가 직접 서명
서명 검증 없음 (지갑 JWT 로 간접 인증) 실제 ECDSA recovery 로 직접 검증
소유권 체크 해당 없음 (항상 본인) 기존 행의 owner_wallet_address 와 요청 비교
재등록 시 변경 device_meta, status, updated_at 동일 (node_type/tier 는 건드리지 않음)
ON CONFLICT 행위 RETURNING 없음 — node_id 만 반환 RETURNING * 으로 최신 NodeRow 반환

디자인 원칙: 한 nodes.node_id 는 한 가지 진입 경로를 통해서만 생성되어야 합니다. 모바일 자동 등록은 rn_ 접두사가 붙어서 충돌 가능성이 낮고, 사용자 직접 등록은 사용자가 원하는 식별자를 쓸 수 있습니다.

실제 저장 결과 검증

Section 17 Step 2 E2E 예제 (examples/node_register.rs) 실행 후:

    node_id    | node_type | tier | status | auth_mode |            owner_wallet_address            | device_platform
---------------+-----------+------+--------+-----------+--------------------------------------------+-----------------
 edge-9a778fea |         1 |    2 |      2 |         2 | 0x217d4acd078cad11ab7ca93da7a4368b1393383d | edge
  • node_type = 1 → EDGE ✓
  • tier = 2 → MID ✓
  • status = 2 → ACTIVE ✓
  • auth_mode = 2 → SIGNATURE ✓
  • device_platform = 'edge' ✓

다음 변경 예정 (Section 17 Step 3, #313)

  • Soft delete: archived_at TIMESTAMPTZ 컬럼 추가 마이그레이션 도입 예정. 노드 삭제 시 물리 삭제가 아닌 archived_at 세팅
  • ListNodes / GetNode 쿼리: archived_at IS NULL 조건을 암묵적으로 추가해서 archived 노드 숨김
  • 마이그레이션 번호: 0002_nodes_archived_at.sql (예정)
  • 이 시점에 이 이슈에 상세 기록 예정

JL James Lee이(가) 7달 전에 변경 Actions #8

Section 17 Step 3 — 마이그레이션 0002: nodes.archived_at 추가

  • Commit: commit:server|d9668e6
  • 관련 이슈: #313 (DeleteNode RPC), #310/#311 (read 쿼리 필터), #312 (upsert 업데이트)
  • 마이그레이션 파일: migrations/0002_nodes_archived_at.sql

Step A (0001) 이후 첫 번째 스키마 확장입니다. 노드 soft delete 를 지원하기 위한 변경입니다.

마이그레이션 SQL

ALTER TABLE nodes
    ADD COLUMN IF NOT EXISTS archived_at TIMESTAMPTZ;

-- Partial index: only the archived rows are indexed, so the index stays
-- tiny while the hot "active nodes" scans use the existing indexes.
CREATE INDEX IF NOT EXISTS idx_nodes_archived_at
    ON nodes (archived_at)
    WHERE archived_at IS NOT NULL;

결정 포인트

  1. Nullable: archived_at 은 NULL 허용. NULL = 활성, NOT NULL = 삭제됨. 기본값은 명시적으로 설정하지 않음 (ADD COLUMN 시 자동으로 NULL)
  2. Partial index: 일반 index 가 아닌 WHERE archived_at IS NOT NULL 부분 인덱스 사용. 대부분의 행은 활성 상태 (archived_at IS NULL) 이므로 전체 index 는 낭비. archived 행만 색인 → 인덱스 크기 최소화
  3. 기존 데이터: ADD COLUMN 은 모든 기존 행을 NULL 로 채움. 기존 노드는 자동으로 "활성" 상태 유지

코드 레벨 변경

NodeRow 구조체

#[derive(Debug, Clone, FromRow)]
pub struct NodeRow {
    // ... 30 existing columns ...
    pub archived_at: Option<DateTime<Utc>>,  // NEW
}

SELECT_COLUMNS 상수에도 archived_at 추가 → 30 → 31개 컬럼

읽기 쿼리 필터 추가 (3개 함수 모두)

// get_by_id
WHERE node_id = $1 AND archived_at IS NULL

// list + count_filtered
WHERE archived_at IS NULL
  AND ($1::int IS NULL OR node_type = $1)
  AND ($2::int IS NULL OR status    = $2)
  AND ($3::int IS NULL OR tier      = $3)

새 함수

/// Retrieve a node row regardless of archived state.
pub async fn get_by_id_including_archived(pool, node_id) -> Option<NodeRow>;

/// Soft-delete a node by setting archived_at = NOW().
pub async fn archive(pool, node_id) -> bool;

upsert_registered 업데이트 — ON CONFLICT DO UPDATE 에 archived_at = NULL 리셋 추가

ON CONFLICT (node_id) DO UPDATE SET
    device_meta = EXCLUDED.device_meta,
    device_platform = EXCLUDED.device_platform,
    status = $5,
    archived_at = NULL,       -- NEW: un-archive on re-registration
    updated_at = NOW()

nodes 테이블 현재 스키마 (31개 컬럼)

그룹 컬럼
식별 id, node_id, name
분류 (enum) node_type, auth_mode, status, tier, role
소유권 owner_wallet_address
용량 max_concurrent_jobs, throughput_hint, running_jobs
리소스 contributed_storage_gb, contributed_cpu_cores, network_bandwidth_mbps
성능 score, success_rate_1m, avg_exec_ms, p95_exec_ms, timeouts_1m
누적 total_jobs_completed, total_jobs_failed
디바이스 device_id, device_meta (JSONB), device_platform
기타 pool_group
타임스탬프 last_heartbeat_at, last_assigned_at, created_at, updated_at, archived_at (신규)

인덱스 현황

인덱스 타입 용도
nodes_pkey PK (btree) id
nodes_node_id_key UNIQUE (btree) node_id
idx_nodes_owner_wallet btree owner 조회
idx_nodes_status btree status 필터
idx_nodes_node_type btree node_type 필터
idx_nodes_device_id btree device_id 조회
idx_nodes_archived_at partial btree (WHERE archived_at IS NOT NULL) archived 행 조회 (신규)

향후 예정

  • Section 17 Step 4 (#314) — 첫 새 테이블 도입 예정. WalletService 수익 관련 RPC 는 job_receipts / aggregated_earnings 테이블을 요구하므로 마이그레이션 0003_earnings_schema.sql 이 추가될 예정. 이 시점에 이 이슈에 상세 기록
  • 이후 job 처리 서비스 포팅 시 job_history 테이블 추가 예정

JL James Lee이(가) 7달 전에 변경 Actions #9

마이그레이션 0003: job_receipts 테이블 (신규)

  • Commit: commit:server|c56e3d6
  • 관련 이슈: #314 (WalletService 수익 RPC)
  • 마이그레이션 파일: migrations/0003_job_receipts.sql
  • Alphea-ai-edge 첫 번째 신규 테이블 (기존 0001/0002 는 accounts/wallets/nodes 및 nodes 확장)

마이그레이션 SQL

CREATE TABLE IF NOT EXISTS job_receipts (
    id              BIGSERIAL PRIMARY KEY,
    job_id          TEXT        NOT NULL,
    node_id         TEXT        NOT NULL,
    wallet_address  TEXT        NOT NULL,
    job_type        INTEGER     NOT NULL,
    tier            INTEGER     NOT NULL,
    exec_time_ms    INTEGER     NOT NULL,
    status          TEXT        NOT NULL DEFAULT 'COMPLETED',
    reward_agx      DOUBLE PRECISION NOT NULL,
    cpu_seconds     DOUBLE PRECISION NOT NULL DEFAULT 0,
    network_bytes   BIGINT      NOT NULL DEFAULT 0,
    storage_gb      DOUBLE PRECISION NOT NULL DEFAULT 0,
    completed_at    TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_job_receipts_wallet_time
    ON job_receipts (wallet_address, completed_at DESC);

CREATE INDEX IF NOT EXISTS idx_job_receipts_node_time
    ON job_receipts (node_id, completed_at DESC);

설계 결정

1. FK 제약 없음

wallet_address 와 node_id 는 각각 accounts / nodes 를 논리적으로 참조하지만 외래 키 제약은 걸지 않았습니다:

  • 수익 영수증은 감사 레코드이므로 계정/노드가 사라져도 과거 수익 기록은 보존되어야 함
  • 레거시와 동일한 느슨한 연결

2. 정규화 선택 — wallet_address 직접 저장

nodes → wallet_address 간접 조회 대신 영수증에 지갑 주소를 직접 중복 저장합니다:

  • GetReceipts / GetEarningsSummary 의 핫 경로에서 JOIN 제거
  • 노드 소유권 이전 가능성 (향후 기능) 시에도 과거 수익 귀속 불변 유지

3. 두 개의 인덱스

인덱스 용도
(wallet_address, completed_at DESC) GetReceipts 페이지네이션 + GetEarningsSummary 세 개 파티션(전체/7일/오늘) 스캔
(node_id, completed_at DESC) 향후 노드별 수익 집계 RPC

두 인덱스 모두 completed_at DESC 접미사를 포함하여 "최근부터" 조회를 B-tree 순방향 스캔으로 커버합니다.

4. aggregated_earnings 테이블은 의도적으로 제외

레거시는 aggregated_earnings 를 별도 테이블로 두고 일간 집계를 캐시했으나, 이번 포팅에서는:

  • job_receipts 직접 집계로도 대시보드 부하 커버 가능
  • 단일 SQL 문(아래) 으로 3개 파티션 모두 한 번에 계산
  • 복잡도 감소 + 동기화 이슈 제거
SELECT
    COALESCE(SUM(reward_agx), 0) AS all_time,
    COALESCE(SUM(reward_agx) FILTER (WHERE completed_at >= NOW() - INTERVAL '7 days'), 0) AS week,
    COALESCE(SUM(reward_agx) FILTER (WHERE completed_at >= DATE_TRUNC('day', NOW())), 0) AS today
FROM job_receipts
WHERE wallet_address = $1

캐시 테이블은 부하 임계치가 실제로 문제가 될 때 별도 이슈로 도입합니다.

store::receipts 모듈

src/store/receipts.rs 신설. 주요 함수:

함수 용도
list_by_wallet(pool, wallet, limit, offset) 페이지네이션 조회
count_by_wallet(pool, wallet) PageInfo.total
summary_for_wallet(pool, wallet) (all_time, week, today) 한 번에
insert(pool, ...) 쓰기 경로 (현재는 E2E 예제에서 시드용, 향후 스케줄러에서 사용)
row_to_proto(row) ReceiptRow → proto ReceiptRecord

현재 전체 스키마 요약

버전 파일 내용
0001 0001_initial_schema.sql accounts, wallets, nodes (30 컬럼)
0002 0002_nodes_archived_at.sql nodes.archived_at 추가 + partial index
0003 0003_job_receipts.sql job_receipts 테이블 + 2 인덱스 (신규)

향후 예정

  • job_history 테이블: 스케줄러/dispatcher 포팅 시. 현재 진행 중/실패한 작업 추적
  • aggregated_earnings 테이블: 부하 이슈 발생 시 (당분간 불필요)
  • virtual_node_* 테이블군: 가상 노드 포팅 시 (WalletService.ClaimVirtualNodes + VNodeCreditor 엔진)
  • referral_* / friends 테이블: FriendsService 포팅 시 (Section 16)

테이블이 새로 추가될 때마다 이 이슈에 기록 예정.

JL James Lee이(가) 6달 전에 변경 Actions #10

스키마 변경 — commit:server|304d9a1 (refs #390)

migrations/0004_accounts_last_active_and_friends.sql

accounts 테이블 컬럼 추가

ALTER TABLE accounts
    ADD COLUMN IF NOT EXISTS last_active_at TIMESTAMPTZ;
  • AuthService.Verify 성공 시 touch_last_active() 로 스탬핑
  • UserService.GetUser 의 is_active 계산에 사용 (24h 이내 → true)

friends 테이블 신규 생성

CREATE TABLE IF NOT EXISTS friends (
    id          BIGSERIAL   PRIMARY KEY,
    inviter_id  BIGINT      NOT NULL REFERENCES accounts (id) ON DELETE CASCADE,
    invitee_id  BIGINT      NOT NULL REFERENCES accounts (id) ON DELETE CASCADE,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE (inviter_id, invitee_id)
);
CREATE INDEX IF NOT EXISTS idx_friends_inviter ON friends (inviter_id);
CREATE INDEX IF NOT EXISTS idx_friends_invitee ON friends (invitee_id);
  • UserService.GetUser 의 friend_count / grade 계산에 사용
  • 향후 FriendsService 포팅 시 이 테이블을 기반으로 확장 예정

JL James Lee이(가) 6달 전에 변경 Actions #11

스키마 추가 — commit:server|ef3f2ba

migration 0005_job_history_and_aggregated_earnings.sql

job_history (신규)

id, job_id (UNIQUE), type, status, assigned_node_id,
actual_exec_ms, final_reward_agx, completed_at, created_at

인덱스: idx_job_history_node_id, idx_job_history_completed_at

aggregated_earnings (신규)

id, wallet_address, period_date, period_type, total_jobs,
completed_jobs, failed_jobs, total_agx, cpu_agx, network_agx,
storage_agx, created_at, updated_at
UNIQUE (wallet_address, period_date, period_type)

인덱스: idx_aggregated_earnings_wallet

JL James Lee이(가) 6달 전에 변경 Actions #12

스키마 변경 — Supabase Auth 전환 (commit:server|8f52241)

마이그레이션 파일: migrations/0006_supabase_auth_schema.sql

변경 내용

accounts 테이블 재설계

  • id BIGSERIAL → user_id UUID PRIMARY KEY (GoTrue sub)
  • username, password_hash, wallet_address 등 기존 컬럼 제거
  • email TEXT, display_name TEXT, profile_icon TEXT 유지/추가

wallets 테이블 신설

CREATE TABLE wallets (
    id BIGSERIAL PRIMARY KEY,
    user_id UUID NOT NULL REFERENCES accounts(user_id),
    wallet_address TEXT NOT NULL,
    ...
);

사용자와 보상 지갑의 1:N 관계. wallet은 identity가 아닌 reward destination.

nodes 테이블 컬럼 추가

ALTER TABLE nodes ADD COLUMN IF NOT EXISTS owner_user_id UUID REFERENCES accounts(user_id);

friends 테이블 DROP (wallet identity 기반 referral 시스템 → 폐기)

영향

  • 기존 accounts 데이터 전체 DROP 후 재생성 (마이그레이션 포함 DROP TABLE CASCADE)
  • nodes.owner_wallet_address는 유지 (노드 서명 검증용), owner_user_id가 소유자 추적에 사용됨

JL James Lee이(가) 6달 전에 변경 Actions #13

DB 스키마 변경 — 0007_agx_account_scope.sql

commit:server|657e7fa

AGX 회계의 키를 wallet_address → user_id (UUID) 로 통일. 데이터 보존 없이 클린 리라이트.

스키마 변경 요약

DROP (wallet 기반 테이블 폐기)

  • job_receipts
  • aggregated_earnings

ALTER (dead column 제거)

  • wallets: balance_agx, total_earned_agx 컬럼 삭제 (코드에서 한 번도 read/write 된 적 없음)

CREATE (user_id 기반 신규 테이블)

CREATE TABLE job_receipts (
    id              BIGSERIAL PRIMARY KEY,
    job_id          TEXT        NOT NULL,
    node_id         TEXT        NOT NULL,
    user_id         UUID        NOT NULL,        -- 신규 키 (이전엔 wallet_address)
    job_type        INTEGER     NOT NULL,
    tier            INTEGER     NOT NULL,
    exec_time_ms    INTEGER     NOT NULL,
    status          TEXT        NOT NULL DEFAULT 'COMPLETED',
    reward_agx      DOUBLE PRECISION NOT NULL,
    cpu_seconds     DOUBLE PRECISION NOT NULL DEFAULT 0,
    network_bytes   BIGINT      NOT NULL DEFAULT 0,
    storage_gb      DOUBLE PRECISION NOT NULL DEFAULT 0,
    completed_at    TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_job_receipts_user_time ON job_receipts (user_id, completed_at DESC);
CREATE INDEX idx_job_receipts_node_time ON job_receipts (node_id, completed_at DESC);
CREATE TABLE aggregated_earnings (
    id             BIGSERIAL PRIMARY KEY,
    user_id        UUID        NOT NULL,        -- 신규 키
    period_date    TEXT        NOT NULL,
    period_type    TEXT        NOT NULL DEFAULT 'daily',
    total_jobs     INT         NOT NULL DEFAULT 0,
    completed_jobs INT         NOT NULL DEFAULT 0,
    failed_jobs    INT         NOT NULL DEFAULT 0,
    total_agx      DOUBLE PRECISION NOT NULL DEFAULT 0,
    cpu_agx        DOUBLE PRECISION NOT NULL DEFAULT 0,
    network_agx    DOUBLE PRECISION NOT NULL DEFAULT 0,
    storage_agx    DOUBLE PRECISION NOT NULL DEFAULT 0,
    created_at     TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at     TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE (user_id, period_date, period_type)
);
CREATE INDEX idx_aggregated_earnings_user ON aggregated_earnings (user_id, period_date DESC);
-- 신규 ledger: redeem code, admin credit/debit, AI 차감 등 receipt 외의 모든 AGX 변동
CREATE TABLE wallet_adjustments (
    id           BIGSERIAL PRIMARY KEY,
    user_id      UUID        NOT NULL,
    delta_agx    DOUBLE PRECISION NOT NULL,         -- 음수 = 차감, 양수 = 적립
    reason       TEXT        NOT NULL,               -- 'redeem' | 'admin_credit' | ...
    ref_code     TEXT,                                -- redeem_codes.code 등 외부 식별자
    note         TEXT,
    created_by   UUID,
    created_at   TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_wallet_adjustments_user_time ON wallet_adjustments (user_id, created_at DESC);

잔액 계산식 변경

balance_agx       = Σ job_receipts.reward_agx (status='COMPLETED', user_id=$1)
                  + Σ wallet_adjustments.delta_agx (user_id=$1)
total_earned_agx  = Σ job_receipts.reward_agx (status='COMPLETED', user_id=$1)

영향

  • 기존 job_receipts / aggregated_earnings 데이터 모두 폐기 (마이그레이션 자동 적용 시)
  • wallets 테이블은 payout destination 목록 용도로만 남음 (1 user → N wallet)

JL James Lee이(가) 6달 전에 변경 Actions #14

DB 스키마 변경 — 0008_redeem_codes.sql

commit:server|a2df7c8

Redeem Code 시스템 (#420) Phase 1 — 사용권 발급/리딤/관리를 위한 3개 테이블 신설.

신규 테이블

CREATE TABLE redeem_codes (
    code              TEXT        PRIMARY KEY,
    batch_id          UUID,                          -- NULL = 단일 발급
    reward_type       SMALLINT    NOT NULL,          -- 1:AGX_POINTS, 2:AI_MONTHLY, 3:AI_YEARLY, 4:CUSTOM
    agx_amount        DOUBLE PRECISION NOT NULL DEFAULT 0,
    ai_service_days   INTEGER     NOT NULL DEFAULT 0,
    note              TEXT,
    max_redemptions   INTEGER     NOT NULL DEFAULT 1,
    redemption_count  INTEGER     NOT NULL DEFAULT 0,
    expires_at        TIMESTAMPTZ,                    -- NULL = 무기한
    created_by        UUID,                            -- 발급한 admin user_id
    created_at        TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    revoked_at        TIMESTAMPTZ,
    CHECK (max_redemptions >= 1),
    CHECK (redemption_count >= 0),
    CHECK (agx_amount >= 0),
    CHECK (ai_service_days >= 0)
);
CREATE INDEX idx_redeem_codes_batch       ON redeem_codes (batch_id) WHERE batch_id IS NOT NULL;
CREATE INDEX idx_redeem_codes_created_at  ON redeem_codes (created_at DESC);
CREATE INDEX idx_redeem_codes_reward_type ON redeem_codes (reward_type);
CREATE TABLE redeem_history (
    id              BIGSERIAL PRIMARY KEY,
    code            TEXT        NOT NULL REFERENCES redeem_codes(code) ON DELETE CASCADE,
    user_id         UUID        NOT NULL,
    redeemed_at     TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    agx_credited    DOUBLE PRECISION NOT NULL DEFAULT 0,
    ai_days_added   INTEGER     NOT NULL DEFAULT 0,
    UNIQUE (code, user_id)                          -- 동일 사용자의 중복 리딤 차단
);
CREATE INDEX idx_redeem_history_user_time ON redeem_history (user_id, redeemed_at DESC);
CREATE INDEX idx_redeem_history_code      ON redeem_history (code);
-- AI Service 사용권 — 단일 만료일 누적 연장 방식
CREATE TABLE ai_service_entitlements (
    user_id      UUID        PRIMARY KEY,
    expires_at   TIMESTAMPTZ NOT NULL,
    updated_at   TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

키 정책 / 외래키

  • redeem_history.user_id 는 GoTrue UUID — accounts(user_id) FK 는 걸지 않음 (admin 발급은 사용자 가입 전이라도 가능해야 하므로)
  • redeem_history.code 는 redeem_codes(code) ON DELETE CASCADE — 코드 자체가 삭제되면 이력도 삭제
  • redeem_codes.created_by 는 admin user_id — FK 없음 (audit 로그성)

AGX 적립 경로

redeem() 호출 시 agx_amount > 0 이면 wallet_adjustments 에 reason='redeem', ref_code=<code> 로 ledger 기록 (별도 컬럼 추가 없음).

→ AGX 잔액 조회는 기존 wallets::get_balance(user_id) 가 자동으로 receipts + adjustments 합산 (Phase 0 #421 에서 도입).

JL James Lee이(가) 6달 전에 변경 Actions #15

DB 부트스트랩 — GoTrue 용 auth schema + pgcrypto

commit:server|028596e

GoTrue (supabase/gotrue:v2.158.1) 는 깨끗한 Postgres 에서 자체 마이그레이션을 위해 다음 두 가지가 사전에 필요:

CREATE EXTENSION IF NOT EXISTS pgcrypto;       -- gen_random_uuid()
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";    -- 보조 UUID 함수
CREATE SCHEMA IF NOT EXISTS auth;              -- GoTrue 의 모든 테이블 위치
GRANT ALL ON SCHEMA auth TO alphea;            -- GoTrue 가 alphea 사용자로 접속

이 SQL 을 db/init/01-gotrue-schema.sql 에 두고 docker-compose 의 postgres 서비스에 ./db/init:/docker-entrypoint-initdb.d:ro 로 마운트했다. postgres entrypoint 가 데이터 디렉토리 첫 부팅 시 자동 실행한다.

기존 데이터가 있는 환경에서는 init script 가 트리거되지 않으므로 동일 SQL 을 수동 실행해야 한다 (#424 노트 참고).

영향

  • alphea 의 기존 public 스키마 테이블에는 영향 없음
  • GoTrue 는 auth 스키마 안에 자체 테이블 (auth.users, auth.refresh_tokens 등) 생성

JL James Lee이(가) 6달 전에 변경 Actions #16

후속 — Supabase 표준 role 세트 추가

commit:server|392fb8a

GoTrue v2.140+ RLS 마이그레이션이 표준 Supabase role 5개 (postgres, anon, authenticated, service_role, supabase_auth_admin) 의 존재 + GRANT 가능성을 가정. db/init/01-gotrue-schema.sql 에 NOLOGIN role 5개 생성 + alphea 에게 멤버십 부여 (PostgreSQL 16+ 의 grantor 멤버십 요구사항).

JL James Lee이(가) 6달 전에 변경 Actions #17

DB 스키마 변경 — Redeem USD-only refactor

commit:server|1ce5955

migrations/0009_redeem_codes_usd_only.sql — Redeem 정책 변경(#420)에 따른 클린 리라이트.

삭제

  • ai_service_entitlements 테이블 (AI Service 기능 폐기)
  • redeem_codes.agx_amount, redeem_codes.ai_service_days
  • redeem_history.agx_credited, redeem_history.ai_days_added

추가/변경

CREATE TABLE redeem_codes (
    code              TEXT        PRIMARY KEY,
    batch_id          UUID,
    reward_type       SMALLINT    NOT NULL,    -- 1=USD_200, 2=USD_1000, 3=USD_2000
    usd_amount        INTEGER     NOT NULL,    -- 200 / 1000 / 2000
    note              TEXT,
    max_redemptions   INTEGER     NOT NULL DEFAULT 1,
    redemption_count  INTEGER     NOT NULL DEFAULT 0,
    expires_at        TIMESTAMPTZ,
    created_by        UUID,
    created_at        TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    revoked_at        TIMESTAMPTZ,
    CHECK (reward_type IN (1, 2, 3)),
    CHECK (usd_amount > 0),
    CHECK (max_redemptions >= 1),
    CHECK (redemption_count >= 0)
);

CREATE TABLE redeem_history (
    id              BIGSERIAL PRIMARY KEY,
    code            TEXT        NOT NULL REFERENCES redeem_codes(code) ON DELETE CASCADE,
    user_id         UUID        NOT NULL,
    redeemed_at     TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    usd_credited    INTEGER     NOT NULL DEFAULT 0,
    UNIQUE (code, user_id)
);

기존 redeem_* 데이터는 폐기 (Phase 0 / 0007 의 클린 리라이트 패턴 일관 적용).

Redeem 적립 경로 변경

  • 이전: redeem() 가 wallet_adjustments 에 AGX 적립 + ai_service_entitlements upsert
  • 이후: 둘 다 안 함. redeem_history.usd_credited 에 USD 가치만 기록.
  • wallet_adjustments 테이블 자체는 유지 (admin manual credit 등 향후 용도).

JL James Lee이(가) 6달 전에 변경 Actions #18

DB 스키마 변경 — redeem_codes.qr_png BYTEA 추가

commit:server|3a456f2

migrations/0010_redeem_codes_qr_png.sql — additive 마이그레이션.

ALTER TABLE redeem_codes ADD COLUMN IF NOT EXISTS qr_png BYTEA;

목적

발급된 redeem 코드의 QR PNG 이미지를 DB 에 함께 보관. 어드민 UI 가 외부 JS 라이브러리 없이 <img src="/api/.../qr.png"> 로 직접 표시하고, 외부 시스템(인쇄 자동화 등) 도 같은 endpoint 로 가져다 쓸 수 있다.

정책

  • NULLABLE — 기존 row 폐기 없이 적용 가능. PostgreSQL TOAST 가 BYTEA 를 자동으로 out-of-line 저장하므로 redeem_codes 일반 쿼리 성능 영향 없음.
  • 신규 INSERT 는 store 레이어에서 항상 PNG 채움.
  • 기존 row (NULL) 는 admin endpoint 가 첫 호출 시 on-the-fly 생성.

사이즈

  • 256×256 흑백 PNG, "ALPHEA-XXXX-XXXX-XXXX" 코드 하나당 약 200-400 byte. 1만 row 기준 ~3MB 미만.

JL James Lee이(가) 6달 전에 변경 Actions #19

DB 스키마 변경 — redeem_codes 에 phone + last_sms_sent_at

commit:server|b021104

migrations/0011_redeem_codes_phone.sql — additive (기존 데이터 보존).

ALTER TABLE redeem_codes
    ADD COLUMN IF NOT EXISTS phone             TEXT,
    ADD COLUMN IF NOT EXISTS last_sms_sent_at  TIMESTAMPTZ;

용도

  • phone — redeem 코드 발급 시 수령자 전화번호 (optional). Detail 패널에서 추후 편집 가능.
  • last_sms_sent_at — MMS 발송 시각 stamp. UI 의 "최근 발송" 표시 + 중복 발송 방지 판단 근거 (현재는 표시만).

정책

  • 둘 다 NULLABLE — 기존 row 자동 적용 가능, 신규 row 도 phone 미지정 시 NULL.
  • last_sms_sent_at 는 redeem::stamp_sms_sent() 가 발송 성공 후에만 갱신.

향후 (Phase B real provider 추가 시) 추가 컬럼 가능성

  • sms_provider TEXT — 어느 provider 가 발송했는지
  • sms_provider_message_id TEXT — provider 의 외부 ID (delivery 추적용)
  • 별도 sms_log 테이블 — 발송 이력 (성공/실패 상세, 비용 등)

지금 단계에서는 굳이 추가하지 않음 — Phase B 에서 실제 provider 요구에 맞춰 결정.

JL James Lee이(가) 6달 전에 변경 Actions #20

DB 스키마 변경 — redeem_codes 에 email + last_email_sent_at

commit:server|49543d4

migrations/0012_redeem_codes_email.sql — additive (기존 데이터 보존).

ALTER TABLE redeem_codes
    ADD COLUMN IF NOT EXISTS email               TEXT,
    ADD COLUMN IF NOT EXISTS last_email_sent_at  TIMESTAMPTZ;

용도

  • email — redeem 코드의 기본 발송 채널 수령자 주소 (#420/#426)
  • last_email_sent_at — 마지막 SES 발송 시각 stamp (UI "최근 발송" 표시)

정책

  • 둘 다 NULLABLE — 기존 row 자동 적용 가능
  • last_email_sent_at 는 redeem::stamp_email_sent() 가 발송 성공 후 갱신
  • email 은 #426 의 default delivery channel. phone (last_sms_sent_at) 은 secondary.

향후 추가 가능성 (Phase B)

  • redeem_code_deliveries 별도 감사 테이블 — channel, status, error_message, provider_msg_id, retry_count 등 상세 추적 (현재는 last_*_sent_at stamp 만)

JL James Lee이(가) 6달 전에 변경 Actions #21

  • 다음 일감과 관련됨:에 이슈 #432: Engine + ConnectNode 포팅 — Synth / Dispatcher / Scheduler / VNodeCreditor + bidi 스트림 완성이(가) 추가되었습니다.

JL James Lee이(가) 6달 전에 변경 Actions #22

마이그레이션 추가 — 0013_virtual_nodes.sql (Phase 7, 가상 노드 적립)

commit:server|d4d49f9

#432 Phase 7 (VirtualNodeCreditor) 와 함께 추가된 새 테이블 3개.

virtual_node_packages

구매 가능한 가상 노드 패키지 카탈로그. 정보용이며 assignment 가 패키지 없이도 존재 가능 (admin 발급 / redeem-bound).

CREATE TABLE virtual_node_packages (
    id              BIGSERIAL PRIMARY KEY,
    name            TEXT        NOT NULL UNIQUE,
    node_count      INTEGER     NOT NULL,
    jobs_per_second INTEGER     NOT NULL DEFAULT 10,
    daily_job_cap   INTEGER     NOT NULL DEFAULT 100000,
    reward_per_job  DOUBLE PRECISION NOT NULL DEFAULT 0.01,
    duration_days   INTEGER     NOT NULL DEFAULT 30,
    price_usd       DOUBLE PRECISION NOT NULL DEFAULT 0,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

virtual_node_assignments

user 별 가상 노드 grant. status 전이: active → expired (creditor 가 end_time < NOW() 발견) | active → revoked (admin).

CREATE TABLE virtual_node_assignments (
    id              BIGSERIAL PRIMARY KEY,
    user_id         UUID        NOT NULL,
    package_id      BIGINT      REFERENCES virtual_node_packages(id),
    node_count      INTEGER     NOT NULL,
    jobs_per_second INTEGER     NOT NULL DEFAULT 10,
    daily_job_cap   INTEGER     NOT NULL DEFAULT 100000,
    reward_per_job  DOUBLE PRECISION NOT NULL DEFAULT 0.01,
    status          TEXT        NOT NULL DEFAULT 'active',
    start_time      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    end_time        TIMESTAMPTZ,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_vna_user   ON virtual_node_assignments (user_id);
CREATE INDEX idx_vna_status ON virtual_node_assignments (status);

virtual_node_credits

(assignment, period) 단위 적립 기록. 한 row 당 한 tick 기간의 누적 결과. 마지막 row 의 period_end 가 다음 tick 의 cursor.

CREATE TABLE virtual_node_credits (
    id              BIGSERIAL PRIMARY KEY,
    assignment_id   BIGINT      NOT NULL REFERENCES virtual_node_assignments(id) ON DELETE CASCADE,
    user_id         UUID        NOT NULL,
    period_start    TIMESTAMPTZ NOT NULL,
    period_end      TIMESTAMPTZ NOT NULL,
    node_count      INTEGER     NOT NULL,
    completed_jobs  BIGINT      NOT NULL,
    agx_earned      DOUBLE PRECISION NOT NULL,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_vnc_user        ON virtual_node_credits (user_id, created_at DESC);
CREATE INDEX idx_vnc_assignment  ON virtual_node_credits (assignment_id, period_end DESC);

잔액 통합

store::wallets::get_balance 가 virtual_node_credits.agx_earned 도 합산하도록 동시 수정:

  • total_earned_agx = SUM(job_receipts.reward_agx WHERE COMPLETED) + SUM(virtual_node_credits.agx_earned)
  • balance_agx = total_earned_agx + SUM(wallet_adjustments.delta_agx)

→ 가상 노드 적립이 별도 회계 경로 없이 사용자 잔액에 자동 반영.

모든 테이블 user_id (UUID) keyed

#421 의 account-scope 결정과 정합. wallet_address 키는 사용하지 않음.

JL James Lee이(가) 6달 전에 변경 Actions #23

  • 다음 일감과 관련됨:에 이슈 #433: VfsService 포팅 — per-account 가상 파일시스템 (Read/Write/List/Mkdir/Delete/Stat/Exists/Lock/Unlock)이(가) 추가되었습니다.

JL James Lee이(가) 6달 전에 변경 Actions #24

마이그레이션 추가 — 0014_vfs_nodes.sql (#433, VfsService)

commit:server|fed4ea0

#433 (VfsService 포팅) 과 함께 추가된 새 테이블 1개.

vfs_nodes

per-account 가상 파일시스템 노드 (file 또는 directory). account-scope (user_id UUID, post-#421). default tree 16 entry 는 첫 접근 시 store::vfs::ensure_init 가 트랜잭션으로 seed.

CREATE TABLE vfs_nodes (
    id            BIGSERIAL PRIMARY KEY,
    user_id       UUID        NOT NULL,
    path          TEXT        NOT NULL,
    name          TEXT        NOT NULL,
    node_type     TEXT        NOT NULL,        -- 'file' | 'directory'
    content       TEXT,
    mime_type     TEXT,
    size          BIGINT      NOT NULL DEFAULT 0,
    readonly      BOOLEAN     NOT NULL DEFAULT FALSE,
    created_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    modified_at   TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE (user_id, path)
);

CREATE INDEX idx_vfs_user_path ON vfs_nodes (user_id, path);

Schema 결정

  • TIMESTAMPTZ (vs legacy BIGINT unix-seconds) — Rust 측 다른 테이블과 통일. proto wire 에서는 .timestamp() 로 변환.
  • (user_id, path) UNIQUE — 동일 사용자의 동일 경로 중복 방지. write 가 ON CONFLICT UPDATE 로 idempotent.
  • content TEXT — 작은 텍스트 파일만 지원 (legacy 동등). 큰 binary 는 별도 blob storage 가 필요할 때 추가 컬럼 또는 별도 테이블로 확장.
  • readonly — /modules, /sys 시스템 경로 보호. 핸들러가 prefix 체크로 한 번 더 거름.

post-#421 정합성

user_id UUID 키, wallet_address 미사용. proto 의 wallet_address 필드는 wire 호환을 위해 유지하되 핸들러가 무시.

JL James Lee이(가) 6달 전에 변경 Actions #25

  • 다음 일감과 관련됨:에 이슈 #434: [Go backport] Supabase GoTrue 인증 전환 — Phase 1: 인프라 + 인증 코어이(가) 추가되었습니다.

JL James Lee이(가) 6달 전에 변경 Actions #26

DB 스키마 v12 — GoTrue 인증 도입 (Phase 1)

commit:server|03c6c64 (이슈 #434)

internal/store/migrations.go 에 v12 추가. clean rewrite 정책에 따라 식별 관련 테이블은 운영 데이터를 보존하지 않는다 (scripts/ec2/reset-db.sh 과 호환).

스키마 변경 내역

-- TRUNCATE (clean rewrite)
TRUNCATE wallet_comments, rewards_log, referral_claims, friends, wallets CASCADE;
TRUNCATE accounts RESTART IDENTITY CASCADE;
TRUNCATE nodes RESTART IDENTITY CASCADE;

-- accounts — GoTrue user_id 를 새 식별 컬럼으로
ALTER TABLE accounts ADD COLUMN IF NOT EXISTS user_id UUID;
ALTER TABLE accounts ADD COLUMN IF NOT EXISTS display_name TEXT;
CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_user_id ON accounts(user_id);
ALTER TABLE accounts ALTER COLUMN wallet_address DROP NOT NULL;
ALTER TABLE accounts ALTER COLUMN account_type   DROP NOT NULL;

-- nodes — owner_user_id 도입
ALTER TABLE nodes ADD COLUMN IF NOT EXISTS owner_user_id UUID;
CREATE INDEX IF NOT EXISTS idx_nodes_owner_user_id ON nodes(owner_user_id);
ALTER TABLE nodes ALTER COLUMN owner_wallet_address DROP NOT NULL;

GoTrue 자체의 auth 스키마는 Postgres 첫 기동 시 alphea-rpc/db/init/01-gotrue-schema.sql 이 pgcrypto / uuid-ossp 확장 및 Supabase role 5종을 생성한 뒤 GoTrue 마이그레이션이 알아서 채워넣는다.

Phase 2 에서 추가 예정인 스키마 재설계 (별도 이슈)

  • wallets → (user_id, wallet_address, label, created_at, UNIQUE(user_id, wallet_address)) 1:N 구조로 교체
  • job_receipts / aggregated_earnings 에 user_id UUID 추가 + 인덱스
  • wallet_adjustments ledger 테이블 신규

JL James Lee이(가) 6달 전에 변경 Actions #27

  • 다음 일감과 관련됨:에 이슈 #435: [Go backport] AGX 회계 user_id 종속 전환 — Phase 2이(가) 추가되었습니다.

JL James Lee이(가) 6달 전에 변경 Actions #28

DB 스키마 v13 — AGX 회계 user_id 종속 (Phase 2)

commit:server|61f09e9 (이슈 #435)

internal/store/migrations.go 에 v13 추가. clean rewrite 정책에 따라 wallets / job_receipts / aggregated_earnings 의 dev 데이터는 보존하지 않는다.

스키마 변경 내역

-- 1) wallets 재설계 — 1:N user→wallet
DROP TABLE IF EXISTS wallets CASCADE;
CREATE TABLE wallets (
    id              BIGSERIAL PRIMARY KEY,
    user_id         UUID NOT NULL,
    wallet_address  TEXT NOT NULL,
    label           TEXT,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE (user_id, wallet_address)
);
CREATE INDEX idx_wallets_user_id        ON wallets(user_id);
CREATE INDEX idx_wallets_wallet_address ON wallets(wallet_address);

-- 2) job_receipts.user_id
TRUNCATE job_receipts;
ALTER TABLE job_receipts ADD COLUMN IF NOT EXISTS user_id UUID;
CREATE INDEX IF NOT EXISTS idx_job_receipts_user_id
    ON job_receipts(user_id, completed_at DESC);

-- 3) aggregated_earnings.user_id + 고유 인덱스
TRUNCATE aggregated_earnings;
ALTER TABLE aggregated_earnings ADD COLUMN IF NOT EXISTS user_id UUID;
CREATE UNIQUE INDEX IF NOT EXISTS idx_aggregated_earnings_user_period
    ON aggregated_earnings(user_id, period_date, period_type);
CREATE INDEX IF NOT EXISTS idx_aggregated_earnings_user_date
    ON aggregated_earnings(user_id, period_date);

-- 4) wallet_adjustments — account-scope ledger
CREATE TABLE IF NOT EXISTS wallet_adjustments (
    id          BIGSERIAL PRIMARY KEY,
    user_id     UUID NOT NULL,
    delta_agx   DOUBLE PRECISION NOT NULL,
    reason      TEXT NOT NULL,
    ref_code    TEXT,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_wallet_adjustments_user_id
    ON wallet_adjustments(user_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_wallet_adjustments_ref_code
    ON wallet_adjustments(ref_code) WHERE ref_code IS NOT NULL;

잔액 공식

balance = Σ job_receipts.reward_agx WHERE user_id=$1 AND status='completed' + Σ wallet_adjustments.delta_agx WHERE user_id=$1

Phase 3 에서 추가 예정

  • friends.inviter_id/invitee_id → user_id
  • referral_claims.account_id / rewards_log.account_id → user_id
  • nodes.owner_wallet_address 제거
  • virtual_node_* 테이블의 user_id 컴럼 추가

JL James Lee이(가) 6달 전에 변경 Actions #29

  • 다음 일감과 관련됨:에 이슈 #437: [Go backport] FriendsService user_id 전환 — Phase 4이(가) 추가되었습니다.

JL James Lee이(가) 6달 전에 변경 Actions #30

DB 스키마 v14 — FriendsService user_id 전환 (Phase 4)

commit:server|de617cf (이슈 #437)

internal/store/migrations.go 에 v14 추가. clean rewrite 정책에 따라 세 테이블 dev 데이터는 보존하지 않는다.

스키마 변경 내역

-- friends 재설계 — (inviter_user_id, invitee_user_id) 1:N
DROP TABLE IF EXISTS friends CASCADE;
CREATE TABLE friends (
    id                BIGSERIAL PRIMARY KEY,
    inviter_user_id   UUID NOT NULL,
    invitee_user_id   UUID NOT NULL,
    created_at        TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE (inviter_user_id, invitee_user_id)
);
CREATE INDEX idx_friends_inviter_user_id ON friends(inviter_user_id);
CREATE INDEX idx_friends_invitee_user_id ON friends(invitee_user_id);

-- referral_claims 재설계 — user_id 키
DROP TABLE IF EXISTS referral_claims CASCADE;
CREATE TABLE referral_claims (
    id             BIGSERIAL PRIMARY KEY,
    user_id        UUID NOT NULL,
    reward_type    TEXT NOT NULL,
    reward_amount  DOUBLE PRECISION NOT NULL,
    claimed_at     TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_referral_claims_user_id
    ON referral_claims(user_id, claimed_at DESC);

-- rewards_log 재설계 — user_id 키
DROP TABLE IF EXISTS rewards_log CASCADE;
CREATE TABLE rewards_log (
    id          BIGSERIAL PRIMARY KEY,
    user_id     UUID NOT NULL,
    job_id      TEXT NOT NULL,
    amount      DOUBLE PRECISION NOT NULL,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_rewards_log_user_id
    ON rewards_log(user_id, created_at DESC);

유지

  • accounts.referral_code UNIQUE 컴럼 유지 (referral code 발급 주체는 account)

Phase 5+ 에서 추가 예정

  • nodes.owner_wallet_address 제거
  • virtual_node_assignments / virtual_node_credits / virtual_node_packages user_id 직접 키로 전환

JL James Lee이(가) 6달 전에 변경 Actions #31

  • 다음 일감과 관련됨:에 이슈 #438: [Go backport] Legacy wallet_address 컬럼 제거 — Phase 5 (final cleanup)이(가) 추가되었습니다.

JL James Lee이(가) 6달 전에 변경 Actions #32

DB 스키마 v15 — 레거시 wallet_address 제거 (Phase 5)

commit:server|dc8ad6e (이슈 #438)

internal/store/migrations.go 에 v15 추가. clean rewrite 정책.

스키마 변경

-- 1) nodes 소유권 컬럼 제거
ALTER TABLE nodes DROP COLUMN IF EXISTS owner_wallet_address;

-- 2) virtual_node_* 재설계
DROP TABLE IF EXISTS virtual_node_credits CASCADE;
DROP TABLE IF EXISTS virtual_node_packages CASCADE;
DROP TABLE IF EXISTS virtual_node_assignments CASCADE;
DROP TABLE IF EXISTS virtual_node_claim_events CASCADE;

CREATE TABLE virtual_node_assignments (
    id BIGSERIAL PRIMARY KEY,
    user_id UUID NOT NULL,
    node_count INTEGER NOT NULL DEFAULT 1,
    jobs_per_second BIGINT NOT NULL DEFAULT 0,
    daily_job_cap BIGINT NOT NULL DEFAULT 0,
    reward_per_job DOUBLE PRECISION NOT NULL DEFAULT 0,
    agx_rate DOUBLE PRECISION NOT NULL DEFAULT 1.0,
    start_time TIMESTAMPTZ NOT NULL,
    end_time TIMESTAMPTZ,
    status TEXT NOT NULL DEFAULT 'active',
    memo TEXT NOT NULL DEFAULT '',
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_vna_user_status ON virtual_node_assignments(user_id, status);

CREATE TABLE virtual_node_credits (
    id BIGSERIAL PRIMARY KEY,
    assignment_id BIGINT NOT NULL REFERENCES virtual_node_assignments(id),
    user_id UUID NOT NULL,
    period_start TIMESTAMPTZ NOT NULL,
    period_end TIMESTAMPTZ NOT NULL,
    node_count INTEGER NOT NULL,
    agx_earned DOUBLE PRECISION NOT NULL,
    credited_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_vnc_user_id ON virtual_node_credits(user_id);
CREATE INDEX idx_vnc_assignment ON virtual_node_credits(assignment_id, period_end DESC);

CREATE TABLE virtual_node_packages (
    id BIGSERIAL PRIMARY KEY,
    claim_code TEXT NOT NULL UNIQUE,
    node_count INTEGER NOT NULL CHECK (node_count > 0 AND node_count % 20 = 0),
    agx_rate DOUBLE PRECISION NOT NULL DEFAULT 1.0,
    valid_years INTEGER NOT NULL DEFAULT 0,
    jobs_per_second BIGINT NOT NULL DEFAULT 0,
    daily_job_cap BIGINT NOT NULL DEFAULT 0,
    reward_per_job DOUBLE PRECISION NOT NULL DEFAULT 0,
    status TEXT NOT NULL DEFAULT 'active',
    claimed_user_id UUID,
    claimed_assignment_id BIGINT REFERENCES virtual_node_assignments(id),
    claimed_at TIMESTAMPTZ,
    expires_at TIMESTAMPTZ,
    memo TEXT NOT NULL DEFAULT '',
    created_by TEXT NOT NULL DEFAULT '',
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_vnp_status ON virtual_node_packages(status, created_at DESC);
CREATE INDEX idx_vnp_claimed_user ON virtual_node_packages(claimed_user_id);

CREATE TABLE virtual_node_claim_events (
    id BIGSERIAL PRIMARY KEY,
    package_id BIGINT NOT NULL REFERENCES virtual_node_packages(id),
    event_type TEXT NOT NULL,
    user_id UUID,
    details TEXT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_vnce_package ON virtual_node_claim_events(package_id, created_at DESC);

남은 wallet_address 사용자

소유권/identity 경로에서는 완전히 사라졌고, 다음 테이블만 텍스트 컬럼으로 유지:

  • wallets — 레거시 registry (1:N user→wallet)
  • job_receipts.wallet_address — 원공 receipt metadata
  • aggregated_earnings.wallet_address — metadata
  • vfs_nodes.wallet_address — VFS 콘테이너 소유 key
  • access_logs.wallet_address — audit log metadata
  • wallet_comments — admin 메모

Phase 종결

Go 원본 GoTrue 백포트가 v15 으로 완결. 이후 스키마 변경은 새 기능/추가 자어 구축 단계에서 진행.

JL James Lee이(가) 6달 전에 변경 Actions #33

Deploy 운영 문서에 v15 / GoTrue init 반영

commit:server|ac96cc2

docs/10-deployment.md + docs/11-ec2-deployment-runbook.md 에 v15 스키마까지의 흐름 반영:

  • 테이블 _meta.schema_version = v15
  • alphea-rpc/db/init/01-gotrue-schema.sql 마운트 (pgcrypto, uuid-ossp, auth schema, Supabase role 5종)
  • v11 이하에서 v15로 올릴 때 reset 이 가장 단순한 경로임을 명시, 데이터 보존 경로도 병기
  • post-reset smoke check에 [migrate] done version=15 관찰 포함

스키마 변경 없이 운영 관점 보완이므로 이슈 상태는 그대로.

HP Henry Park이(가) 6달 전에 변경 Actions #34

v19 — vfs_nodes drop (Phase 1C 동반)

commit:server2|a2f34ff (refs #446)

스키마 변경

  • migrateV19DropVfsNodes: DROP TABLE IF EXISTS vfs_nodes
  • v1 baseline 의 CREATE TABLE vfs_nodes ... 는 immutable-migrations 규칙대로 그대로 유지 — fresh DB 는 v1 에서 만들고 v19 에서 즉시 drop. 기존 dev/prod DB 는 v19 마이그레이션이 적용되면서 테이블이 사라짐
  • DROP CASCADE 필요 없음 — vfs_nodes 에 다른 테이블의 FK 없음 (독립 테이블)

이유

Phase 1C 에서 VfsService 가 완전히 폐기됨 — proto/handler/store/domain 전부 제거. vfs_nodes 의 reader/writer 가 하나도 없으므로 도르만트 상태가 아닌 완전 drop 이 맞음. DfsService (Phase 1B) 가 별도 스키마 (dfs_namespaces / dfs_files / dfs_file_versions, v17/v18) 로 이미 대체 완료

운영 영향

  • fresh DB: 자동 적용. 기존 운영 절차와 동일
  • 기존 dev/qa DB (vfs_nodes 에 데이터 있음): v19 적용 시 테이블 전체 삭제됨. VfsService 가 이미 폐기되었으니 수집할 데이터가 없음 — 별도 백업/마이그레이션 불필요
  • production: 해당 없음 (next 브랜치에 포함되지 않음, feat/dfs-poc 에서만 적용). Phase 1 완성 후 monitor 통합 시점에 next 로 일괄 merge 예정

현재 스키마 버전

_meta.schema_version = 19 (runMigrations 가 len(migrations) 로 자동 산출)

HP Henry Park이(가) 6달 전에 변경 Actions #35

v20 — delta_apps (Phase 2 Delta Registry)

commit:server2|48b17a0 (refs #446)

스키마 신규

CREATE TABLE IF NOT EXISTS delta_apps (
  id                    BIGSERIAL PRIMARY KEY,
  namespace_id          BIGINT NOT NULL REFERENCES dfs_namespaces(id) ON DELETE CASCADE,
  app_id                TEXT NOT NULL,
  app_path              TEXT NOT NULL CHECK (app_path LIKE '/%'),
  manifest_path         TEXT NOT NULL CHECK (manifest_path LIKE '/%'),
  runtime_type          TEXT NOT NULL,
  entry                 TEXT,
  enabled               BOOLEAN NOT NULL DEFAULT TRUE,
  current_version       TEXT,
  digest                TEXT,
  owner_principal_type  TEXT NOT NULL
      CHECK (owner_principal_type IN ('user','wallet')),
  owner_principal_id    TEXT NOT NULL,
  created_at            TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at            TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  UNIQUE (namespace_id, app_id)
);

CREATE INDEX idx_delta_apps_namespace_enabled
  ON delta_apps(namespace_id, enabled, app_id);
CREATE INDEX idx_delta_apps_owner_principal
  ON delta_apps(owner_principal_type, owner_principal_id);

설계 구현 이유

  • namespace_id FK + ON DELETE CASCADE: namespace 가 삭제되면 등록된 앱도 자동 제거. orphan delta_apps 불가능
  • UNIQUE(namespace_id, app_id): 등록 아이델티티. RegisterDeltaApp 는 ON CONFLICT 으로 UPSERT — 같은 앱 재등록 은 새 행 대신 기존 행 update
  • owner_principal_type CHECK in ('user','wallet'): Phase 0.7 Principal 모델 일관. token namespace 경우 wallet 동일은 보류 (token namespace 자체가 owner principal NULL이라 현재 쓰기 흐름 X)
  • app_path / manifest_path CHECK LIKE '/%': Phase 1 dFS path normalization 철학 일관 (NormalizeDFSPath 는 쓰기 경로에서 호출)
  • 인덱스 2개: namespace 당 enabled 앱 나열 조회 (idx_delta_apps_namespace_enabled), owner principal 기준 모든 namespace 가로지르는 조회 (idx_delta_apps_owner_principal, Phase 4 이후 admin 대시보드 용)

연관 테이블

  • dfs_files / dfs_file_versions: 변경 없음. 다만 RegisterDeltaApp 이 단일 트랜잭션 안에서 namespace 의 /manifest.json 에 새 dfs_file_versions row 추가 (system-managed write path). 파일 API 의미론은 그대로 — /manifest.json 은 readonly 여전히 공개 PutFile 로 쓰기 불가

운영 영향

  • fresh DB: 자동 적용, 운영 절차 없음
  • 기존 dev/qa DB: v20 적용 시 테이블 생성 만 — 기존 데이터 영향 없음
  • production: 해당 없음 (next 브랜치 미적용, feat/dfs-poc 전용)

현재 스키마 버전

_meta.schema_version = 20

Actions

내보내기 PDF Atom