<PonsLink/>
작업
글
주제
시리즈
카테고리
연락
⌘K 검색
About
코드와 제품으로

코드와 제품으로

연결되는 세계

연결되는 세계

PonsLink는 만남 뒤 요청을 세션으로 잇고, PonsWarp는 서버 보관 없이 파일을 직접 전송합니다. 이 블로그는 기능 소개보다 설계 판단과 운영 증거를 남깁니다.

연락처

[email protected]github.com/DeclanJeon

운영

콘텐츠는 WordPress headless CMS, 공개 UI는 Next.js입니다.

초안 미리보기
© 2026 PonsLink Blog|Next.js · WordPress REST · Framer Motion
Local-first · Backup-before-deploy

[PonsLink] 데이터 모델 설계 회고 — Postgres 두 개 DB, 37개 테이블

Declan Jeon·2026년 8월 7일

  • 개발 회고

서비스를 오래 굴리면 스키마가 제품 결정을 배신한다. PonsLink도 예외가 아니었다. 한때는 &quot;users 하나면 되겠지&quot;로 시작했다가, 지금은 같은 Postgres 인스턴스 안에 두 개의 DB를 둔다. `ponslink`(22테이블)와 `ponswarp`(15테이블). 이 글은 2026-08-07 실측 덤프를 기준으로 그 구조를 해설한다. 왜 DB를 둘로 나눴는가 | DB | 도메인 | 대표 테이블 | |&#8212;|&#8212;|&#8212;| | `ponslink` | 룸·세션·미디어·웹훅·플랜·테넌트 | rooms, sessions, &#8230; Read more

서비스를 오래 굴리면 스키마가 제품 결정을 배신한다. PonsLink도 예외가 아니었다. 한때는 "users 하나면 되겠지"로 시작했다가, 지금은 같은 Postgres 인스턴스 안에 두 개의 DB를 둔다. `ponslink`(22테이블)와 `ponswarp`(15테이블). 이 글은 2026-08-07 실측 덤프를 기준으로 그 구조를 해설한다.

ERD 개요

왜 DB를 둘로 나눴는가

| DB | 도메인 | 대표 테이블 |
|---|---|---|
| `ponslink` | 룸·세션·미디어·웹훅·플랜·테넌트 | rooms, sessions, participants, media_tracks, webhook_endpoints, idempotency_keys |
| `ponswarp` | 계정·결제·클라우드 공유 | users, auth_sessions, subscriptions, cloud_shares, drop_passes, *_events |

세션/미디어 부하는 짧고 빈번하다. 계정/결제/공유는 상대적으로 드물지만 무결성 요구가 세다. 물리 분리는 백업·마이그레이션·장애 반경을 가르기 위한 선택이다. API 런타임 env는 `DATABASE_URL=…/ponslink`를 주 DB로 쓴다.

ponswarp — 계정·결제·공유 코어

users: `email` UNIQUE, `plan` default `free`, `stripe_customer_id`/`google_sub` UNIQUE. 타임스탬프는 bigint epoch ms.

auth_sessions: 토큰 원문은 저장하지 않는다. `token_hash` UNIQUE, `expires_at`, `revoked_at`. 부분 인덱스 `WHERE revoked_at IS NULL`로 활성 세션만 스캔한다.

subscriptions: Stripe/PayPal/LemonSqueezy 식별자를 컬럼으로 나란히 둔다. 공급자별 UNIQUE 제약으로 중복 구독을 막는다.

cloud_shares / cloud_share_files / cloud_download_sessions / cloud_usage_events: 공유 링크 → 파일 목록(R2 `object_key`) → 다운로드 세션(`token_hash`) → 사용 이벤트(bytes). `plan_snapshot`(jsonb)으로 "공유 생성 시점의 요금제 조건"을 고정한다. 요금제가 나중에 바뀌어도 이미 만든 공유의 규칙은 흔들리지 않는다.

drop_passes: 1회성 유료 공유권. `max_total_bytes`, `max_file_bytes`, `retention_seconds`, `remaining_uses`. Stripe/PayPal/LemonSqueezy 결제 식별자를 UNIQUE로 잡는다.

결제 이벤트 원장: `stripe_events` / `paypal_events` / `lemonsqueezy_events` — 공급자 이벤트 id를 PK로 둔다. 웹훅이 두 번 와도 두 번 적용되지 않는다.

admin_members / admin_audit_logs: 역할·상태·감사(actor/action/target/ip/ua/request_id).

ponslink — 세션·미디어·웹훅

`tenants → projects → rooms → sessions/participants/media_tracks/speech_transcripts/session_tokens`. 협업 문서는 `collaboration_resources → resource_documents`. 개발자 플랫폼 면은 `api_keys`, `provider_credentials`, `provider_usage`, `webhook_endpoints → webhook_deliveries`, `idempotency_keys`.

웹훅 전달과 결제 멱등성은 같은 패턴이다. 원장에 먼저 쓰고, 상태를 바꾼다.

설계 원칙 다섯 줄

1. 타임스탬프는 bigint epoch ms — JS 직렬화가 단순하다.

2. 비밀은 해시만 저장한다 (`token_hash`, `password_hash`).

3. 결제/웹훅은 이벤트 원장 + 멱등성 키로 중복을 죽인다.

4. 요금제 조건은 스냅샷한다 (`plan_snapshot`).

5. 소프트 삭제·부분 인덱스로 활성 행 스캔을 싸게 유지한다.

스키마는 제품의 기억이다. 다음 글에서는 이 테이블들이 요청→세션→아카이브 라이프사이클에서 어떻게 채워지는지 본다.

Related

관련 운영 노트

같은 주제에서 이어서 읽기 좋은 글입니다.

  • [Blueprint L3] Pons 포트폴리오 — 의존성 0, 단일 index.html이 오래 가는 이유

    의존성 0, 단일 index.html 41KB. 디자인 토큰 SSOT와 13개 제품 쇼케이스로 만든 정적 포트폴리오가 오래 가는 이유.

  • [Blueprint L2] PonsLink Control Tower — Threads에 dry-run으로 쏘는 이유

    Python Threads 컨트롤타워. cardnews 재사용→생성 폴백, n8n 패치, dry-run 기본 게이트로 안전하게 쏘는 자동발행.

  • [Blueprint L1] PonsLink P2P Room — WebRTC 화상회의를 BFF로 감싼 이유

    React SPA와 Express BFF가 Socket.IO로 시그널링하고, 미디어는 mesh로 가는 2티어. TURN 그랜트와 presence lease로 룸을 만든 화상회의.

댓글

아직 댓글이 없습니다.

현재 이 글의 댓글은 닫혀 있습니다.