콘텐츠로 이동

2. 아키텍처 뜯어보기

Envoy가 문지기, PostgREST가 API, GoTrue가 신분증 발급소, Postgres가 금고

flowchart TB
    CL["클라이언트<br/>브라우저 · 모바일 · 서버"]
    CL --> EV["Envoy<br/>API 게이트웨이 · 인증 · 라우팅"]

    EV --> PR["PostgREST<br/>/rest/v1"]
    EV --> GT["Auth (GoTrue)<br/>/auth/v1"]
    EV --> RT["Realtime<br/>/realtime/v1"]
    EV --> ST["Storage API<br/>/storage/v1"]
    EV --> GQ["pg_graphql<br/>/graphql/v1"]
    EV --> EF["Edge Functions<br/>/functions/v1"]

    PR --> PG[("PostgreSQL")]
    GT --> PG
    RT --> PG
    ST --> PG
    GQ --> PG
    EF --> PG

    ST --> S3["오브젝트 스토리지 + CDN"]

    APP["외부 앱 · ORM<br/>Prisma · Drizzle"] --> SV["Supavisor<br/>커넥션 풀러"]
    SV --> PG

    classDef key  fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    classDef warn fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
    class PG key
    class EV,SV warn
    class CL,PR,GT,RT,ST,GQ,EF,S3,APP mute

프로젝트 URL 하나(https://<ref>.supabase.co)로 전부 접근한다. 경로가 곧 제품 구분이고, 그 뒤에는 전부 같은 Postgres가 있다.

supabase.from('posts').select('*')를 호출했을 때 실제로 벌어지는 일.

sequenceDiagram
    autonumber
    participant C as 클라이언트
    participant E as Envoy
    participant P as PostgREST
    participant D as Postgres

    C->>E: GET /rest/v1/posts?select=*<br/>apikey + Authorization Bearer
    E->>E: apikey 검증 → 프로젝트 식별
    E->>P: 요청 전달
    P->>P: JWT 검증 → role 클레임 추출
    P->>D: set local role = 'authenticated'<br/>set local request.jwt.claims = '...'
    P->>D: SELECT * FROM posts
    Note over D: RLS 정책이 auth.uid() 기준으로 행 필터링
    D-->>P: 허용된 행만
    P-->>C: JSON 응답

5번 줄이 이 문서 전체의 핵심이다. JWT의 내용이 Postgres 세션 변수로 옮겨지고, 그때부터는 데이터베이스가 판정한다.

프로젝트의 모든 HTTP 트래픽이 들어오는 단일 진입점. 경로 기반 라우팅, apikey 헤더 검증, 속도 제한, CORS를 담당한다.

경로 목적지
/rest/v1/* PostgREST
/auth/v1/* Auth (GoTrue)
/realtime/v1/* Realtime
/storage/v1/* Storage API
/functions/v1/* Edge Functions
/graphql/v1 pg_graphql

Supabase 초기 학습에서 가장 중요한 부품이다. Postgres 스키마를 읽어 테이블 · 뷰 · 함수를 자동으로 REST 엔드포인트로 노출한다. 우리가 API 서버를 쓰지 않는 이유가 바로 이것이다.

Terminal window
# supabase-js 없이도 그냥 HTTP다
curl "https://<ref>.supabase.co/rest/v1/posts?select=id,title&status=eq.published&order=created_at.desc&limit=10" \
-H "apikey: sb_publishable_xxx" \
-H "Authorization: Bearer <로그인 JWT>"

supabase.from().select().eq()는 결국 이 URL을 조립하는 빌더다. 노출 대상은 설정된 스키마뿐이다 (기본값 public, graphql_public).

SQL 대상 HTTP 설명
select GET /rest/v1/<table> 조회. 필터·정렬·페이지네이션은 쿼리스트링
insert POST /rest/v1/<table> 본문에 객체 또는 배열
update PATCH /rest/v1/<table>?filter 필터에 걸린 행 수정
delete DELETE /rest/v1/<table>?filter 필터에 걸린 행 삭제
upsert POST + Prefer: resolution=merge-duplicates 있으면 수정, 없으면 삽입
함수 호출 POST /rest/v1/rpc/<function> 데이터베이스 함수 실행

사용자 정보를 auth 스키마(특히 auth.users)에 저장하고, 로그인에 성공하면 access token(JWT)refresh token을 발급한다. JWT에는 sub(사용자 UUID), role, email, aal, app_metadata 등이 담긴다.

이 JWT가 이후 모든 요청에 Authorization: Bearer로 실려 다니고, PostgREST가 그걸 풀어 Postgres 세션 변수에 넣는다.

Realtime — WAL 구독과 브로드캐스트

섹션 제목: “Realtime — WAL 구독과 브로드캐스트”

Elixir/Phoenix로 만들어진 WebSocket 서버. 세 가지 기능을 제공한다.

  • Broadcast — 클라이언트끼리 임의의 메시지를 주고받는다 (커서 위치, 채팅 등)
  • Presence — 채널에 누가 접속해 있는지 상태를 공유한다
  • Postgres Changes — Postgres의 논리 복제(WAL)를 읽어 INSERT/UPDATE/DELETE를 밀어준다

세 기능 모두 채널(channel) 이라는 같은 추상 위에 올라가고, Postgres Changes와 private 채널에는 RLS가 적용된다. (9장)

Storage API — 메타데이터는 Postgres에

섹션 제목: “Storage API — 메타데이터는 Postgres에”

실제 파일 바이트는 오브젝트 스토리지(S3 계열)에, 메타데이터는 Postgres storage 스키마에 있다. storage.buckets, storage.objects 테이블이 실제로 존재한다.

-- 그래서 파일 권한도 결국 테이블 정책이다
create policy "자기 폴더에만 업로드"
on storage.objects for insert to authenticated
with check (
bucket_id = 'avatars'
and (storage.foldername(name))[1] = (select auth.uid())::text
);

별도 권한 시스템을 배울 필요가 없다는 게 이 설계의 이점이다. (8장)

Deno 기반 서버리스 함수. TypeScript를 그대로 실행하고, 전 세계 엣지 로케이션에 배포되어 사용자와 가까운 곳에서 실행된다.

용도는 웹훅 수신, 시크릿이 필요한 외부 API 호출, 이메일 발송, 관리자 작업이다. 함수 안에서는 secret key를 써서 RLS를 우회한 작업도 가능하다.

Vercel의 Route Handler와 역할이 겹친다. 어느 쪽에 무엇을 둘지는 12장에서 정리한다. 한 줄 요약: DB에 붙어 있어야 하는 로직은 Supabase, 프론트엔드와 붙어 있어야 하는 로직은 Vercel.

서버리스 환경에서 반드시 이해해야 하는 부품이다.

Postgres는 연결 하나당 프로세스 하나를 쓴다. 연결 수가 곧 메모리 비용이다. 그런데 서버리스 함수는 요청마다 새로 뜨고 죽는다 → 연결이 폭발한다. Supavisor가 앞단에서 연결을 모아 소수의 실제 연결로 다중화한다.

방식 포트 언제 쓰나
Direct connection 5432 상시 떠 있는 서버, 마이그레이션, pg_dump
Supavisor session 모드 5432 IPv4만 되는 환경의 상시 서버
Supavisor transaction 모드 6543 서버리스 / 엣지 함수 — 짧은 연결이 몰릴 때
Dedicated pooler (PgBouncer) 6543 유료 플랜, 고성능 프로덕션

pg_meta는 테이블 생성, 컬럼 추가, 역할 관리 같은 DDL 작업을 REST로 노출하는 서버이고, Studio(웹 대시보드)의 실제 동작은 pg_meta 호출이다. 즉 대시보드에서 클릭으로 만든 테이블은 결국 SQL DDL이 실행된 것이다.

스키마 내용 API 노출
public 내가 만드는 테이블 ○ (기본 노출)
auth users, sessions, identities, mfa_factors ✕ (헬퍼 함수만 사용)
storage buckets, objects ✕ (Storage API 경유)
realtime messages, 구독 관리
extensions 설치된 확장들
vault 암호화된 시크릿
graphql_public GraphQL 진입점

public이 아닌 스키마(app, private 등)를 만들어 API에 노출하지 않는 내부 테이블을 두는 것이 좋은 습관이다. 노출 스키마는 대시보드의 API Settings에서 관리한다.

이 그림이 Supabase 보안 모델의 전부다.

flowchart LR
    PK["publishable key<br/>sb_publishable_..."] -->|"로그인 안 함"| ANON["role: anon"]
    PK -->|"+ 사용자 JWT"| AUTH["role: authenticated<br/>auth.uid() = 사용자 ID"]
    SK["secret key<br/>sb_secret_..."] --> SR["role: service_role"]

    ANON --> RLS["RLS 정책 평가"]
    AUTH --> RLS
    SR --> BYPASS["RLS 우회 — 전체 접근"]

    classDef ok   fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef bad  fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    classDef key  fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
    class PK ok
    class SK,BYPASS bad
    class RLS key
    class ANON,AUTH,SR mute
  • publishable key는 브라우저에 노출해도 된다. 그 자체로는 아무 권한이 없다
  • secret key는 절대 클라이언트에 두지 않는다. RLS를 통째로 우회한다
  • 예전 이름인 anon key / service_role key(JWT 형태)는 폐기 예정이다. 신규 프로젝트는 새 키를 쓴다
Data API (PostgREST) 직접 연결 (Postgres 프로토콜) Edge Function
클라이언트 supabase-js, HTTP Prisma, Drizzle, pg HTTP 호출
인증 JWT + API 키 DB 사용자/비밀번호 JWT 또는 커스텀
RLS 적용 ○ 자동 연결 역할에 따름 (보통 우회) 어떤 키를 쓰느냐에 따름
브라우저에서 직접
커넥션 풀 필요 ✕ (HTTP) ○ (Supavisor) 상황에 따라
적합한 곳 사용자 데이터 CRUD 관리자 도구, 배치, 복잡한 트랜잭션 시크릿이 필요한 로직
  1. Postgres가 단일 진실 공급원이다 — 사용자도, 파일 메타데이터도, 권한 정책도 전부 한 DB 안에 있다
  2. 보안의 무게중심이 DB로 내려간다 — “API 서버에서 권한 체크”가 아니라 “DB가 행 단위로 판정”이다
  3. 프론트엔드가 DB와 직접 대화한다 — 중간 계층이 없으니 빠르지만, RLS를 틀리면 곧바로 데이터 유출이다
  4. 부품을 갈아끼울 수 있다 — Data API 대신 직접 연결, Edge Function 대신 Vercel 함수. 선택지가 열려 있다
  • Envoy가 문지기, PostgREST가 API, GoTrue가 신분증 발급소, Postgres가 금고
  • 모든 부품이 하나의 Postgres를 공유한다 — 통합성과 위험성이 같은 곳에서 나온다
  • publishable key(공개 가능) / secret key(서버 전용) 두 개만 구분하면 된다
  • 서버리스에서 직접 연결을 쓸 거면 Supavisor transaction 모드(6543) 를 기억한다
  • 대시보드 클릭 변경은 마이그레이션으로 남지 않는다 — 처음부터 CLI 흐름을 익힌다