server/supabase/ 신규 디렉토리:
config.toml — Supabase CLI 설정
- 프로젝트 ID, DB 포트, auth providers (Google/GitHub/Apple),
edge_runtime, functions.*.verify_jwt 설정
migrations/ (PostgreSQL DDL, 시간순):
- 20260409000001_initial_schema.sql
12개 테이블: profiles, teams, team_members, meetings,
meeting_memos, meeting_documents, transcripts, history,
dictionary, memo_tags, daily_usage, subscriptions
+ 인덱스 + FK CASCADE + transcripts Realtime publication
- 20260409000002_rls_policies.sql
개인 전용(history/dictionary): user_id = auth.uid()
팀 공유(meetings 등): 본인 OR team_members 조회 subquery
daily_usage: 읽기만, 쓰기는 service_role RPC
- 20260409000003_auth_triggers.sql
handle_new_user — auth.users INSERT → profiles+subscriptions 자동 생성
moddatetime — 8개 테이블 updated_at 자동 갱신
increment_daily_usage — service_role 전용 쿼터 RPC
- 20260409000004_storage_buckets.sql
audio/exports/avatars 3개 버킷 + 경로 기반 접근 정책
(파일 경로가 {user_id}/...로 시작해야 쓰기 허용)
functions/ (Deno/TypeScript Edge Functions):
- _shared/cors.ts — CORS 헤더 + preflight 핸들러
- _shared/auth.ts — requireUser (JWT 검증 + User 반환)
- _shared/quota.ts — 티어별 쿼터 체크 + consume + service role client
- stt-proxy/index.ts — Google Cloud STT 래퍼 스캐폴딩
placeholder 응답, 실제 API 호출 코드는 주석으로 포함
- llm-proxy/index.ts — Anthropic Messages API 래퍼 스캐폴딩
티어별 허용 모델 정책 (free=Haiku, pro=Sonnet, team=Opus)
설계 문서:
- docs/v2/phase-V2-2.md — 상세 설계 (스키마/RLS/Edge Functions/Realtime)
- docs/v2/phase-V2-2-setup.md — 사용자 액션 가이드 (Supabase 계정/
OAuth 등록/CLI/배포/검증)
apps/desktop typecheck 통과 (server/는 Deno 런타임이라 별도).
실제 Supabase 프로젝트 배포는 사용자가 phase-V2-2-setup.md 따라 수행.
9.2 KiB
9.2 KiB
Phase V2-2: Supabase 인프라 — 상세 설계
멀티플랫폼(desktop/web/mobile)의 단일 백엔드를 Supabase 기반으로 구축. 로컬 완결 작업(SQL 마이그레이션, Edge Functions)은 이 페이즈에서 전부 작성. 사용자 액션(계정 생성, OAuth 등록, 배포)은
phase-V2-2-setup.md참조.
1. 목표
- 인증: Supabase Auth — Google/GitHub/Apple OAuth + Email
- 데이터 SSOT: PostgreSQL — V1 SQLite 스키마를 멀티테넌시(user_id, team_id)로 포팅
- 보안: Row Level Security — 개인 데이터 격리 + 팀 공유 지원
- AI 프록시: Edge Functions — Google STT, Claude LLM 호출을 서버에서 래핑 (API 키 은닉 + 쿼터 집계)
- 파일 스토리지: Supabase Storage 버킷 3개 (audio, exports, avatars)
- 실시간: 회의 진행 중 전사 세그먼트 실시간 동기화 (Realtime subscription)
2. 디렉토리 구조
server/supabase/
├── config.toml # 프로젝트 설정 (로컬 개발용)
├── seed.sql # 개발 시드 데이터
├── migrations/
│ ├── 20260409000001_initial_schema.sql # 테이블 + 인덱스
│ ├── 20260409000002_rls_policies.sql # RLS policies
│ ├── 20260409000003_auth_triggers.sql # handle_new_user, moddatetime
│ └── 20260409000004_storage_buckets.sql # storage 버킷 + policies
└── functions/
├── _shared/
│ ├── cors.ts # CORS 헤더 헬퍼
│ ├── auth.ts # JWT 검증 + user_id 추출
│ └── quota.ts # 티어별 쿼터 체크
├── stt-proxy/
│ └── index.ts # Google Cloud STT 래퍼
└── llm-proxy/
└── index.ts # Claude API 래퍼
3. 데이터 모델
3.1 테이블 요약
| 테이블 | 설명 | 멀티테넌시 |
|---|---|---|
public.profiles |
auth.users 확장 (name, avatar_url, locale, tier) | id = auth.users.id |
public.meetings |
회의 세션 (V1 meeting_sessions) | user_id, team_id? |
public.meeting_memos |
회의 중 타임스탬프 메모 | meeting_id → meetings |
public.meeting_documents |
LLM 생성 문서 (minutes/report/mindmap) | meeting_id → meetings |
public.transcripts |
전사 세그먼트 (실시간 동기화 대상) | meeting_id → meetings |
public.history |
음성 입력 히스토리 (dictation/translate 등) | user_id |
public.dictionary |
사용자 사전 | user_id |
public.memo_tags |
히스토리 태그 | user_id, history_id → history |
public.daily_usage |
기능별 일일 사용량 (쿼터 집계) | user_id |
public.teams |
팀 | owner_id → auth.users |
public.team_members |
팀 멤버십 | team_id, user_id, role |
public.subscriptions |
Stripe 구독 상태 | user_id |
3.2 V1 SQLite ↔ V2 PostgreSQL 타입 매핑
| V1 (drizzle sqlite-core) | V2 (PostgreSQL) | 비고 |
|---|---|---|
text('id').primaryKey() (nanoid) |
uuid primary key default gen_random_uuid() |
신규는 UUID 사용 |
integer('created_at') (epoch ms) |
timestamptz not null default now() |
Date/Time 네이티브 |
integer('indexed', mode: 'boolean') |
boolean not null default false |
|
real('duration') |
double precision |
|
text('status', {enum: ...}) |
text check (status in (...)) 또는 enum |
enum 타입으로 |
text('embedding') (JSON float[]) |
vector(768) via pgvector |
RAG 전용 |
3.3 공통 컬럼
모든 유저 소유 테이블:
user_id uuid not null references auth.users(id) on delete cascadecreated_at timestamptz not null default now()updated_at timestamptz not null default now()(트리거로 자동 갱신)
회의 관련 테이블만 추가:
team_id uuid references public.teams(id) on delete set null(nullable — 개인 회의는 null)
4. RLS 정책 패턴
4.1 개인 전용 테이블 (history, dictionary, memo_tags, daily_usage)
CREATE POLICY "users_own_rows" ON public.history
FOR ALL
USING (user_id = auth.uid())
WITH CHECK (user_id = auth.uid());
4.2 팀 공유 가능 테이블 (meetings, meeting_memos, meeting_documents, transcripts)
-- SELECT: 본인 소유 OR 소속 팀의 리소스
CREATE POLICY "meetings_read" ON public.meetings
FOR SELECT
USING (
user_id = auth.uid()
OR team_id IN (
SELECT team_id FROM public.team_members WHERE user_id = auth.uid()
)
);
-- INSERT: 본인 user_id 강제
CREATE POLICY "meetings_insert" ON public.meetings
FOR INSERT
WITH CHECK (user_id = auth.uid());
-- UPDATE: 본인 소유 OR 팀 admin 이상
CREATE POLICY "meetings_update" ON public.meetings
FOR UPDATE
USING (
user_id = auth.uid()
OR team_id IN (
SELECT team_id FROM public.team_members
WHERE user_id = auth.uid() AND role IN ('owner', 'admin')
)
);
-- DELETE: 본인 소유만
CREATE POLICY "meetings_delete" ON public.meetings
FOR DELETE
USING (user_id = auth.uid());
4.3 팀 자체 (teams, team_members)
-- 팀 조회: 본인이 소유 OR 멤버
CREATE POLICY "teams_read" ON public.teams
FOR SELECT
USING (
owner_id = auth.uid()
OR id IN (SELECT team_id FROM public.team_members WHERE user_id = auth.uid())
);
-- 팀 생성: 누구나 (owner = 본인)
CREATE POLICY "teams_insert" ON public.teams
FOR INSERT WITH CHECK (owner_id = auth.uid());
-- 팀 수정: owner만
CREATE POLICY "teams_update" ON public.teams
FOR UPDATE USING (owner_id = auth.uid());
5. Auth 트리거
5.1 handle_new_user — auth.users → profiles 자동 생성
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
INSERT INTO public.profiles (id, name, avatar_url, locale)
VALUES (
NEW.id,
COALESCE(NEW.raw_user_meta_data->>'full_name', NEW.raw_user_meta_data->>'name', split_part(NEW.email, '@', 1)),
NEW.raw_user_meta_data->>'avatar_url',
COALESCE(NEW.raw_user_meta_data->>'locale', 'ko')
);
RETURN NEW;
END;
$$;
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
5.2 moddatetime — updated_at 자동 갱신
CREATE OR REPLACE FUNCTION public.moddatetime()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$;
-- 각 테이블마다:
CREATE TRIGGER set_updated_at
BEFORE UPDATE ON public.meetings
FOR EACH ROW EXECUTE FUNCTION public.moddatetime();
6. Edge Functions
6.1 공통: _shared/auth.ts
import { createClient, User } from 'https://esm.sh/@supabase/supabase-js@2'
export async function requireUser(req: Request): Promise<User> {
const authHeader = req.headers.get('Authorization')
if (!authHeader) throw new Response('Unauthorized', { status: 401 })
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!,
{ global: { headers: { Authorization: authHeader } } }
)
const { data: { user }, error } = await supabase.auth.getUser()
if (error || !user) throw new Response('Unauthorized', { status: 401 })
return user
}
6.2 stt-proxy/ — Google Cloud STT 래퍼
- 입력:
multipart/form-data(audio blob + sample_rate + language_code) - 인증: JWT → user 추출
- 쿼터:
daily_usage확인 → 초과 시 429 - 호출: Google Cloud Speech-to-Text API (v2 recognize)
- 응답:
{ transcript, confidence, language_code, duration_seconds } - 사이드 이펙트:
daily_usage.count증가
6.3 llm-proxy/ — Claude API 래퍼
- 입력:
application/json(messages + system + max_tokens + model) - 인증: 동일
- 쿼터: 동일 (
llm_processfeature) - 호출: Anthropic Messages API (스트리밍 지원)
- 응답: SSE 스트림 또는 JSON
- 모델 정책: Free → Haiku 강제, Pro+ → Sonnet 허용
7. Storage 버킷
INSERT INTO storage.buckets (id, name, public) VALUES
('audio', 'audio', false), -- 녹음 원본 (비공개)
('exports', 'exports', false), -- PDF/DOCX 내보내기
('avatars', 'avatars', true); -- 프로필 사진 (공개)
정책: 파일 경로가 {user_id}/...로 시작해야 쓰기 허용.
8. 실시간 구독 전략
- 회의 중
transcriptsINSERT → Realtime 구독으로 팀원에게 전파 - 회의록 편집 중
meeting_documentsUPDATE → CRDT 병합 (V2-7 팀 기능에서 구현)
9. 검증 전략
V2-2는 로컬에서 전체 검증 불가 (Supabase 프로젝트 필요). 이 페이즈 완료 판정 기준:
- ✅ SQL 마이그레이션 파일 작성 완료
- ✅ Edge Functions 스캐폴딩 완료 (TypeScript 컴파일 가능)
- ✅ 설정 가이드 문서 완료
- ⏳ 실제 deploy 검증은 사용자가
supabase db push후 진행 (V2-2b)
10. 다음 페이즈와의 관계
- V2-3 (Web App): Next.js에서
@supabase/supabase-js클라이언트로 직접 연결 - V2-4 (데스크톱 동기화): apps/desktop에 supabase 클라이언트 추가, 로컬 SQLite ↔ Postgres 양방향 sync 레이어
- V2-7 (팀 기능): teams/team_members 테이블 + invite flow