콘텐츠로 이동
Study NoteSupabase

2. 아키텍처 뜯어보기

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

Supabase 아키텍처 — Envoy 게이트웨이 뒤에 PostgREST·Auth·Realtime·Storage·pg_graphql·Edge Functions가 있고 전부 PostgreSQL 위에 얹힌다

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

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

클라이언트 요청이 Envoy와 PostgREST를 거쳐 Postgres에 닿고 RLS로 행이 걸러지는 순서

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

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

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

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

터미널 창
# 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설명
selectGET /rest/v1/<table>조회. 필터·정렬·페이지네이션은 쿼리스트링
insertPOST /rest/v1/<table>본문에 객체 또는 배열
updatePATCH /rest/v1/<table>?filter필터에 걸린 행 수정
deleteDELETE /rest/v1/<table>?filter필터에 걸린 행 삭제
upsertPOST + Prefer: resolution=merge-duplicates있으면 수정, 없으면 삽입
함수 호출POST /rest/v1/rpc/<function>데이터베이스 함수 실행

사용자 정보를 auth 스키마(특히 auth.users)에 저장하고, 로그인에 성공하면 access token(JWT) 과 refresh token을 발급한다. JWT에는 sub(사용자 UUID), role, email, aal(Authenticator Assurance Level — 다단계 인증(MFA)을 몇 단계 거쳤는지), app_metadata 등이 담긴다.

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

역할로 보면 GoTrue는 keycloak 덱의 인증 생태계 계층에서 IdP(Identity Provider) 층에 해당한다 — Supabase 백엔드 전용의 작은 IdP다.

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 connection5432상시 떠 있는 서버, 마이그레이션, pg_dump
Supavisor session 모드5432IPv4만 되는 환경의 상시 서버
Supavisor transaction 모드6543서버리스 / 엣지 함수 — 짧은 연결이 몰릴 때
Dedicated pooler (PgBouncer)6543유료 플랜, 고성능 프로덕션

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

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

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

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

publishable key는 anon 또는 authenticated 역할로 RLS 평가를 받고, secret key는 service_role로 RLS를 우회한다
  • publishable key는 브라우저에 노출해도 된다. 로그인 전에는 anon 역할의 GRANT와 RLS 정책 범위만 허용한다
  • secret key는 절대 클라이언트에 두지 않는다. RLS를 통째로 우회한다
  • 예전 이름인 anon key / service_role key(JWT 형태)는 폐기 예정이다. 신규 프로젝트는 새 키를 쓴다
Data API (PostgREST)직접 연결 (Postgres 프로토콜)Edge Function
클라이언트supabase-js, HTTPPrisma, Drizzle, pgHTTP 호출
인증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 흐름을 익힌다