백엔드 플랫폼
Supabase를 한 문장으로 말하면 PostgreSQL 데이터베이스를 웹·모바일·AI 앱에서 바로 쓰도록 주변 기능을 묶은 플랫폼입니다. 데이터는 PostgreSQL에 저장하고, PostgREST는 테이블·뷰·함수를 HTTP API로 노출합니다. Auth는 로그인과 토큰을 담당하고, Storage와 Realtime은 파일과 실시간 연결을 맡습니다.
이 저장소에서 배울 핵심은 데이터베이스를 만드는 법보다, 데이터베이스 주변의 도구들을 일관된 개발 경험으로 연결하는 법입니다.
apps/studio에는 관리 화면이, packages/pg-meta에는 데이터베이스 구조를 읽는 SQL 조립기가, docker에는 서비스들을 연결하는 설정이 있습니다.
데이터베이스는 기록을 보관하는 건물입니다. Auth는 신분 확인 창구, RLS는 방마다 붙은 출입 규칙, PostgREST는 서류 접수 창구, Realtime은 변경 사항을 알리는 방송입니다.
Studio는 관리실입니다. 관리실에서 장부를 살펴보는 동선과, 일반 방문자가 자기 서류를 읽는 동선은 서로 다릅니다. 이 차이가 뒤의 아키텍처를 이해하는 출발점입니다.
| 구분 | 이 저장소에서 확인하는 것 | 별도로 살펴볼 것 |
|---|---|---|
| 관리 경험 | Studio 화면, 조회 훅, 서버 API, 공유 UI | 호스팅 플랫폼의 모든 운영 내부 구현 |
| 데이터베이스 | 카탈로그 조회 SQL, 마이그레이션, Docker 연결 | PostgreSQL 엔진과 확장 자체의 구현 |
| 인증·실시간·저장소 | 서비스 이미지, 환경 변수, 예제와 문서 | Auth·Realtime·Storage 각각의 별도 저장소 |
| 클라이언트 SDK | SDK를 사용하는 앱과 예제 | supabase/supabase-js의 SDK 내부 |
README.md는 제품 전체의 기능을 소개합니다. 반면 루트 package.json의 작업 대상은 pnpm과 Turborepo로 관리하는 앱·패키지입니다.
따라서 GitHub의 주 언어가 TypeScript라는 사실을 PostgreSQL이나 모든 서비스의 구현 언어로 확대하면 안 됩니다.
2026-10-23 GitHub Trending Daily에서 6위에 선정됐고, 같은 날짜 기준 스타는 113,641개입니다. 이 수치는 관심의 크기를 보여주지만 그날의 순위 상승 원인을 설명하지는 않습니다. 여기서는 저장소에 포함된 앱과 코드에서 읽을 수 있는 장점을 살펴봅니다.
커밋 2f548a69에는 Knowledge Base 앱이 포함돼 있습니다. 오류 해결과 다른 플랫폼에서의 이전 안내를 모은 지식 기반입니다.
apps/kb/package.json에는 Astro 의존성과 콘텐츠 생성 스크립트가 있습니다. 이 앱의 존재만으로 트렌딩 순위의 원인을 설명할 수는 없습니다.
Knowledge Base 구현과 콘텐츠.
테이블, 외래 키, 인덱스, 트랜잭션을 직접 배웁니다. 관리 화면이 대신 처리하는 동작도 결국 SQL과 PostgreSQL 카탈로그로 내려가므로, 화면 사용법에서 데이터베이스 원리로 학습을 확장할 수 있습니다.
docker/docker-compose.yml은 PostgREST·Auth·Storage 등을 이미 연결합니다. CRUD마다 라우트를 새로 만드는 부담이 줄어드는 대신, 노출할 스키마·역할·행 정책을 올바르게 설정하는 능력이 중요해집니다.
이미지 태그, 볼륨, 상태 검사, 프록시 경로를 읽을 수 있습니다. 자체 호스팅은 인프라 통제권을 주지만, 백업·업데이트·비밀 값 관리도 운영자의 책임이 됩니다. 클라우드의 모든 기능이 동일하게 재현된다는 뜻은 아닙니다.
| 선택지 | 출발하는 모델 | Supabase에서 배울 차이 |
|---|---|---|
| Supabase | PostgreSQL 테이블·SQL·역할 | 관계형 모델, 자동 API, 데이터베이스 권한을 한 흐름으로 연결 |
| Cloud Firestore | 문서와 컬렉션을 중심으로 한 NoSQL 모델 | 문서 중첩과 테이블 관계의 설계 차이를 비교하기 좋음 |
| 직접 SQL 연결 | 연결 문자열과 SQL로 데이터베이스에 접근 | HTTP API와 인증 계층이 어느 위치에 추가되는지 비교 |
| 직접 만든 API 서버 | 팀이 라우트·쿼리·권한 경계를 직접 구현 | 자동화할 공통 기능과 직접 작성할 업무 로직의 경계를 판단 |
비교 범위는 데이터 접근 모델입니다. Firebase 전체를 문서형 데이터베이스 하나로 규정하지 않습니다. Firestore의 문서·컬렉션 모델은 저장소의 Firebase 비교 문서, SQL 연결 구성은 docker/docker-compose.yml을 기준으로 설명합니다. 성능·가격의 우열을 측정한 비교는 아닙니다.
Supabase의 장점은 SQL을 감추는 데 있지 않고, SQL과 권한 모델을 유지하면서 앱 개발에 필요한 연결 작업을 줄이는 데 있습니다.
버전의 기준은 커밋 2f548a69입니다. package.json의 catalog:는 중앙 버전표를 가리키며, 실제 범위는 pnpm-workspace.yaml에서 찾습니다.
설치 해석 결과는 pnpm-lock.yaml의 catalogs.default에 기록돼 있습니다.
^8.0.16은 허용할 버전 범위이고 8.2.1은 잠금 파일에 선택된 구체 버전입니다. 여러 앱이 같은 중앙 표를 참조하면 중복 설정을 줄이고 함께 업데이트하기 쉽습니다.| 기술 | 선언 / 잠금 버전 | 역할과 근거 |
|---|---|---|
| React | ^19.2.6 / 19.2.6 | 화면 구성. 중앙 catalog와 Studio의 react 의존성 |
| Next.js | ^16.3.6 / 16.3.6 | Studio 기본 실행 경로와 Docs. apps/studio/scripts/dispatch.js |
| TanStack Start | ^1.169.2 / 1.169.2 | Studio의 병행 전환 경로. Router는 ^1.170.44 / 1.170.44 |
| TanStack Query | Studio 선언 ~5.104.3 | 서버 조회·캐시·무효화. apps/studio/package.json |
| Tailwind CSS | ^4.2.4 / 4.2.4 | 공유 UI와 화면 스타일. 중앙 catalog |
| Valtio · Zod | ^2.3.2 / 2.3.2 · 3.25.76 | 공유 상태와 데이터 구조 정의. Studio 의존성과 catalog |
| Vite | ^8.0.16 / 8.2.1 | TanStack 실행 경로 등. 선언과 잠금 결과의 차이가 보이는 예 |
| Astro | KB 선언 ^7.2.6 | 지식 기반 사이트. apps/kb/package.json |
apps/lite-studio는 별도의 경량 앱입니다. apps/lite-studio/package.json에는 react-router: ^7.17.0과 vite: ^8.0.0이 선언돼 있습니다.
같은 모노레포라고 모든 앱이 Next.js로 실행되는 것은 아닙니다.
다음은 docker/docker-compose.yml의 이미지 태그입니다. 패키지 버전이나 제품 전체 릴리스 번호와 다른 축입니다.
각 엔진의 내부 소스가 이 저장소에 전부 들어 있다는 뜻도 아닙니다.
| 서비스 | 실제 이미지 | 담당하는 일 |
|---|---|---|
| PostgreSQL | supabase/postgres:17.6.1.136 | 관계형 데이터, 역할, 정책, 확장 |
| API 게이트웨이 | envoyproxy/envoy:v1.39.1 | 외부 경로를 내부 서비스로 연결 |
| Auth | supabase/gotrue:v2.197.0 | 회원가입·로그인·JWT 발급 |
| REST API | postgrest/postgrest:v14.18 | 데이터베이스 객체를 HTTP 자원으로 제공 |
| Realtime | supabase/realtime:v2.143.3 | 실시간 구독과 통신 |
| Storage | supabase/storage-api:v1.81.0 | 파일 API와 접근 제어 연결 |
| postgres-meta | supabase/postgres-meta:v0.100.0 | 데이터베이스 구조와 관리 쿼리 API |
| Edge Runtime | supabase/edge-runtime:v1.77.4 | Deno 기반 함수 실행 환경 |
| Supavisor | supabase/supavisor:2.9.13 | 데이터베이스 연결 풀링 |
| imgproxy | darthsim/imgproxy:v3.31.4 | 이미지 변환 |
Storage의 기본 설정은 STORAGE_BACKEND: file, 파일 크기 제한은 52428800바이트, 즉 50 MiB입니다.
README의 S3 중심 설명과 달리 기본 Compose는 로컬 파일 저장을 사용하며, S3 계열 구성은 별도 덮어쓰기 파일로 선택합니다. 이런 차이가 설정 파일을 읽어야 하는 이유입니다.
docker/README.md의 구성 소개에는 Logflare와 Vector도 등장합니다. 실제 기본 Compose에는 없으며 docker/docker-compose.logs.yml로 추가합니다.
이때 Logflare 이미지 태그는 1.50.15이고 Studio의 ENABLED_FEATURES_LOGS_ALL이 true로 바뀝니다.
| 항목 | 설정값 | 읽을 파일 |
|---|---|---|
| Node.js | engines.node: >=22.13, .nvmrc: 22 | 루트 package.json · .nvmrc |
| pnpm | packageManager: pnpm@11.13.1 | 루트 package.json |
| Turborepo | 2.9.14, 빌드 선행 작업 ^build | package.json · turbo.jsonc |
| TypeScript 도구 | typescript: ~6.0.2, @typescript/native: npm:typescript@~7.0.2 | 중앙 catalog. API 호환 패키지와 네이티브 컴파일러를 나눔 |
| Vitest | ^5.0.0 / 5.0.0 | catalog · Studio의 test:ci |
| Docker 빌드 | node:22-slim, turbo prune studio --docker | apps/studio/Dockerfile |
Dockerfile은 의존성 가지치기, 설치, 프레임워크별 빌드, 실행 산출물 복사를 단계로 나눕니다. 최종 서버 포트는 3000입니다.
STUDIO_FRAMEWORK 선택은 이미지 빌드 시점에 적용됩니다. 실행 중인 이미지에서 환경 변수만 바꿔 두 프레임워크를 즉시 전환하는 구조가 아닙니다.
.github/workflows/typecheck.yml은 잠금 파일 고정 설치 후 같은 작업 안에서 타입 검사와 린트를 병렬 단계로 수행합니다.
.github/workflows/studio-unit-tests.yml은 관련 경로 변경을 필터링하고, .github/workflows/studio-lint-ratchet.yml은 선택한 규칙의 위반 수 증가를 막습니다. 기존 경고를 한 번에 없애기 어려운 큰 프로젝트의 점진적 품질 관리 사례입니다.
examples/ai/image_search/pyproject.toml은 Python ^3.11, sentence-transformers ^2.7.0, vecs ^0.2.4를 선언합니다.
examples/edge-functions/supabase/functions/wasm-modules/add-wasm/Cargo.toml은 Rust 2021 edition, cdylib, wasm-bindgen 0.2를 사용합니다. 둘 다 예제의 의존성입니다. 추적 파일에서 go.mod는 발견되지 않았습니다.
기술 스택을 읽을 때는 웹앱의 패키지, 서비스의 이미지 태그, 예제의 도구 버전을 각각의 범위 안에서 해석해야 합니다.
아래 도식은 docker/docker-compose.yml과 게이트웨이 설정을 바탕으로 단순화한 자체 호스팅 구조입니다.
각 선이 별도의 네트워크 호출이나 데이터 접근 경계를 뜻합니다. 클라우드의 모든 내부 제어 계층을 묘사한 그림은 아닙니다.
GraphQL 경로는 게이트웨이에서 /rpc/graphql로 바뀌어 PostgREST로 전달됩니다.
docker/volumes/api/envoy/lds.template.yaml의 graphql-v1-protected 경로에서 이 연결을 읽을 수 있습니다. GraphQL 전용 서버가 무조건 하나 더 떠 있는 구조로 상상하지 않아도 됩니다.
API 게이트웨이 패턴은 외부 주소 체계와 내부 서비스 배치를 분리합니다.
docker/volumes/api/envoy/lds.template.yaml에는 경로 조건, 대상 cluster, 접두 경로 변환이 있고, docker/volumes/api/envoy/cds.yaml에는 대상 호스트·포트·연결 제한·상태 검사가 있습니다.
# docker/volumes/api/envoy/lds.template.yaml 중 경로 구조 발췌
match:
prefix: /rest/v1/
route:
cluster: rest
prefix_rewrite: /
# docker/volumes/api/envoy/cds.yaml의 rest 대상: rest:3000
# 외부 /rest/v1/todos 요청은 내부 /todos로 전달된다.
Envoy의 경로 설정은 WebSocket 업그레이드를 허용하고 Realtime 경로를 /socket/ 쪽으로 보냅니다.
프록시가 단순히 문자열만 바꾸는 것이 아니라 각 서비스의 통신 방식까지 연결한다는 점을 배울 수 있습니다.
apps/studio/data/sql/execute-sql-mutation.ts의 executeSql()은 공통 조회 전송 경계입니다.
프로젝트 식별자를 검사하고, SQL 크기를 0.98 * MB와 비교하며, 연결 정보와 요청 옵션을 붙입니다.
자체 호스팅 서버 진입점은 apps/studio/pages/api/platform/pg-meta/[ref]/query/index.ts의 handlePost()입니다.
이어지는 apps/studio/lib/api/self-hosted/query.ts의 executeQuery()는 assertSelfHosted()를 호출하고, 연결 문자열을 암호화한 헤더와 함께 pg-meta로 보냅니다.
apps/studio/data/fetchers.ts의 createClient<paths>()는 OpenAPI 타입을 사용합니다.
constructHeaders()는 요청 ID와 인증 헤더를 구성하고, pgMetaGuard()는 플랫폼 환경에서 필요한 연결 헤더를 검사합니다. 호출마다 이 처리를 복제하지 않도록 한곳에 모은 설계입니다.
자체 호스팅 Compose는 pg-meta의 DB 사용자에 postgres를 설정합니다. 관리자 경로와 앱 사용자의 RLS 경로는 권한이 다릅니다. 관리 화면에서 보인다는 이유만으로 일반 사용자가 같은 행을 읽을 수 있다고 판단하면 안 됩니다.
apps/studio/data/database/schemas-query.ts에는 getSchemas(), useSchemasQuery(), invalidateSchemasQuery(), prefetchSchemas()가 함께 있습니다.
하나의 자원을 가져오는 방법을 정의하고, 화면·사전 조회·갱신이 그 정의를 재사용합니다.
// apps/studio/data/database/keys.ts의 키 구조
const key = ['projects', projectRef, 'schemas'] as const
// apps/studio/data/database/schemas-query.ts의 핵심 옵션을 줄여 쓴 학습용 예
useQuery({
queryKey: databaseKeys.schemas(projectRef),
queryFn: ({ signal }) =>
getSchemas({ projectRef, connectionString }, signal),
enabled: enabled && typeof projectRef !== 'undefined',
})
키에 projectRef가 있으므로 서로 다른 프로젝트의 스키마 목록이 같은 캐시 자리에 들어가지 않습니다.
AbortSignal을 전달하는 흐름은 필요 없어진 요청을 취소할 통로를 만들고, enabled는 프로젝트가 정해지기 전의 조회를 막습니다.
캐시 키는 서류 보관함의 주소입니다. 서랍 이름이 모두 ‘스키마’라면 어느 프로젝트 서류인지 헷갈립니다. ‘프로젝트 → 식별자 → 스키마’로 주소를 만들면 올바른 서랍을 읽고 비울 수 있습니다.
여기서 한 단계 더 읽어야 합니다. Schema와 SchemasData는 Zod 스키마로부터 타입을 추론하지만, 이 파일의 getSchemas()는 응답에 zod.parse()를 호출하지 않습니다.
실제 처리는 Array.isArray(result) 검사입니다. 타입 선언이 있다는 사실과 모든 응답 필드가 런타임에 검증된다는 주장은 다릅니다.
packages/pg-meta/src/pg-format/index.ts는 일반 문자열과 안전하게 조립할 SQL 조각을 구분합니다.
SafeSqlFragment는 문자열에 식별용 타입 표식을 더한 브랜드 타입입니다. 표식은 컴파일 시점의 규칙이며, 별도의 암호학적 검증 값이 아닙니다.
// 타입 구조와 사용법을 보여주는 학습용 예
type SafeSqlFragment = string & {
readonly __safeSqlFragmentBrand: never
}
const table = ident('user notes')
const owner = literal("O'Reilly")
const sql = safeSql`select * from ${table} where owner = ${owner}`
// 결과: select * from "user notes" where owner = 'O''Reilly'
// safeSql`select * from ${userInput}`
// 일반 string을 그대로 끼우면 타입 규칙에 어긋난다.
| 함수 | 입력의 의미 | 설계 의도 |
|---|---|---|
ident() | 테이블·열·스키마 같은 이름 | SQL 식별자의 인용 규칙을 적용 |
literal() | 문자열·숫자·NULL 같은 값 | 값에 맞는 SQL 표현을 생성 |
safeSql | 이미 안전한 조각들 | 삽입 인수를 SafeSqlFragment로 제한하여 조합 |
untrustedSql() | 외부 내용의 영향을 받은 SQL | 표시는 허용하되 자동 실행과 구분 |
acceptUntrustedSql() | 명시적으로 승인된 편집기 SQL | 사용자 실행 동작에서 신뢰 경계를 넘김 |
실제 safeSql() 구현은 문자열을 이어 붙입니다. 이 함수 자체가 모든 입력을 정제하는 것은 아닙니다.
ident()·literal()로 의미에 맞게 변환한 뒤 조합하는 규칙이 중요하며, rawSql()이나 강제 타입 단언을 남용하면 보호를 우회할 수 있습니다.
packages/pg-meta/README.md는 SQL 조각을 매개변수화됐다고 소개하지만, 여기서 본 조립기는 주로 값을 이스케이프한 SQL 문자열을 만듭니다.
일반적인 $1 자리표시자와 값 배열을 전송하는 준비된 쿼리와 동일한 구현이라고 설명하면 부정확합니다. 서버 executeQuery()가 별도 parameters 옵션을 지원하는 점과도 구분해야 합니다.
packages/pg-meta/src/sql/studio/table-editor/table.ts의 getTableEditorSql({ id })는 테이블 정보를 여러 CTE로 묶습니다.
base_table_info는 c.oid, primary_keys와 index_cols는 i.indrelid를 요청한 테이블 ID로 제한합니다.
-- getTableEditorSql()의 필터 구조를 줄인 예
with base_table_info as (
select c.oid, c.relname
from pg_class c
where c.oid = 12345
), primary_keys as (
select i.indrelid
from pg_index i
where i.indisprimary and i.indrelid = 12345
)
select * from base_table_info;
테이블 하나를 열 때마다 전체 데이터베이스의 인덱스·제약 조건을 모두 훑으면, 사용자 테이블 수가 많아질수록 관리 화면도 느려집니다. 그래서 결과 값만 맞는지 검사하는 테스트에 더해 어떤 경로로 읽는지를 검사합니다.
packages/pg-meta/test/db/plan-guard.ts의 explainAnalyze()는 EXPLAIN (ANALYZE, FORMAT JSON)을 사용합니다.
collectSeqScans()는 계획 트리의 Seq Scan 노드별 관계 이름을 모으고, assertPlanWithinBudget()는 허용 목록·노드 수·실행 시간을 검사합니다. 기본 시간 예산은 1000ms입니다.
packages/pg-meta/test/sql/studio/catalog-plan-guard.test.ts는 합성 카탈로그를 만들고 이 규칙을 적용합니다.
예를 들어 테이블 편집기 예산은 역방향 외래 키 조회의 구조적 이유를 적고 pg_constraint의 Seq Scan 노드를 최대 2개 허용합니다. ‘순차 탐색은 전부 나쁘다’가 아니라, 불가피한 탐색의 이유와 한도를 코드에 남깁니다.
기능이 맞는지, 권한이 맞는지, 규모가 커져도 접근 범위가 맞는지는 서로 다른 질문입니다. 성능 문제를 고친 뒤 실행 계획 규칙을 테스트에 넣으면, 다음 변경에서 같은 문제가 돌아오는지 확인할 수 있습니다.
Studio의 executeSql()에도 선택적 preflightCheck가 있습니다. 예상 비용이 200_000 이상이면 차단합니다. 사전 검사 응답의 오류 필드는 무시하지만, 네트워크 예외 등을 잡는 처리는 없습니다.
PostgreSQL의 예상 비용은 밀리초가 아니며, 이 선택적 장치를 데이터베이스 전체의 보안·부하 보장으로 해석해서는 안 됩니다.
좋은 추상화는 세부 사항을 지우는 것이 아니라, 요청·권한·SQL·성능의 경계를 읽을 수 있는 위치에 모아 둡니다.
| 학습하고 싶은 것 | 첫 번째 파일 | 다음 이동 |
|---|---|---|
| 목록 조회와 캐시 | apps/studio/data/database/schemas-query.ts | apps/studio/data/database/keys.ts → apps/studio/data/sql/execute-sql-mutation.ts |
| SQL 조립 안전성 | packages/pg-meta/src/pg-format/index.ts | packages/pg-meta/src/pg-meta-schemas.ts → SQL 생성 함수 |
| 테이블 편집기 원리 | packages/pg-meta/src/sql/studio/table-editor/table.ts | packages/pg-meta/test/sql/studio/catalog-plan-guard.test.ts |
| 서비스 연결 | docker/docker-compose.yml | docker/volumes/api/envoy/lds.template.yaml → docker/volumes/api/envoy/cds.yaml |
| 함수 실행 제한 | docker/volumes/functions/main/index.ts | Compose의 functions 서비스 |
| 단계적 프레임워크 전환 | apps/studio/scripts/dispatch.js | apps/studio/TANSTACK_MIGRATION.md → apps/studio/pages/와 apps/studio/routes/ |
루트의 supabase/는 Supabase 제품 전체의 모든 서버 코드라는 뜻이 아닙니다. 이 모노레포에서 사용하는 로컬 프로젝트 설정, 함수, 마이그레이션이 모여 있습니다.
한편 docker/는 배포 구성이고, packages/pg-meta는 SQL 생성 라이브러리입니다. 폴더가 다루는 책임을 먼저 구분하면 탐색 범위를 크게 줄일 수 있습니다.
한 화면의 조회 훅을 출발점으로 삼아 ‘데이터를 요청하는 곳 → 전송 경계 → 서버 처리 → SQL 생성’으로 이동하세요. 모노레포 전체를 첫 파일부터 순서대로 읽으려 하면 사용자 동작과 코드의 연결을 놓치기 쉽습니다.
SafeSqlFragment와 UntrustedSqlFragment는 같은 문자열도 출처와 허용 동작에 따라 다르게 취급하는 예입니다.
또 apps/studio/data/query-sources/query-source-registry.ts는 _tag: 'database' | 'logs'로 데이터 원본을 구분합니다. 각 SQL 방언과 전송 경계를 잘못 섞지 않도록 유니온 타입을 사용합니다.
연습: 문자열 하나로 표현하던 값을 ‘사용자가 작성한 SQL’과 ‘외부에서 들어온 SQL’로 나누어 종이에 타입을 설계해 보세요. 어떤 함수가 표시만 허용하고 어떤 함수가 실행을 허용해야 하는지 화살표로 표시하면 타입의 목적을 이해할 수 있습니다.
스키마 목록은 서버에서 받은 상태이고, 펼쳐 둔 메뉴는 화면 상태입니다. 둘을 같은 방식으로 관리할 필요는 없습니다.
useSchemasQuery()는 조회 키·활성 조건·취소 신호를 묶어 서버 상태를 다룹니다. getSchemas()를 별도 함수로 둬 React 바깥의 사전 조회에서도 재사용합니다.
연습: ‘프로젝트 A의 테이블 목록’과 ‘프로젝트 B의 테이블 목록’의 캐시 키를 각각 써 보세요. 테이블을 만든 뒤 어느 접두 키를 무효화해야 하는지 설명하면, 단순한 데이터 가져오기를 넘어 캐시 설계를 연습하게 됩니다.
examples/todo-list/nextjs-todo-list/supabase/migrations/20230712094349_init.sql은 todos.user_id를 auth.users에 연결하고 RLS를 켭니다.
조회 정책은 auth.uid() = user_id입니다. ‘로그인했는가’와 ‘이 행의 주인인가’를 다른 질문으로 다루는 기초 예제입니다.
GRANT는 테이블 접근 권한, RLS 정책은 행 접근 조건을 담당하므로 둘을 함께 생각해야 합니다.연습: 사용자 A와 B가 각각 한 행을 가진 표를 만들고, A의 요청에 정책 조건을 대입해 보세요.
일반 역할에서는 정책이 적용되지만 슈퍼유저와 BYPASSRLS 역할은 우회하며, 소유자도 보통 예외입니다. 이는 PostgreSQL 공식 행 보안 문서의 규칙입니다.
docker/volumes/functions/main/index.ts는 VERIFY_JWT가 true이고 OPTIONS가 아닌 요청의 JWT를 검증합니다. 함수별 디렉토리를 찾은 뒤 EdgeRuntime.userWorkers.create()로 작업자를 생성합니다.
isValidHybridJWT()는 토큰 헤더의 알고리즘에 따라 HS256과 ES256·RS256의 검증 경로를 나눕니다. 헤더를 읽는 것 자체는 검증이 아니며, 실제 서명 검사는 jose.jwtVerify()에서 수행합니다.
이 함수 라우터의 제한값은 memoryLimitMb = 150, workerTimeoutMs = 400_000, 요청 부재 제한 60_000ms입니다.
Compose는 요청 유휴 제한을 150000ms로 설정합니다. 각 값이 제한하는 대상이 다르며, 호스팅 상품 전체에 일괄 적용되는 보장으로 읽으면 안 됩니다.
연습: resolveRuntimeError()의 오류 종류를 ‘시작 실패 / 자원 부족 / 유휴 시간 초과 / 실행 오류’로 분류해 보세요.
재시도는 WorkerAlreadyRetired에만 적용되고 최초 시도 외 최대 3회입니다. 이미 실행된 임의 요청을 모두 재시도하는 정책과 무엇이 다른지 설명하세요.
examples/ai/edge-functions/supabase/migrations/20240408072601_embeddings.sql은 vector(384) 열과 hnsw 인덱스, vector_ip_ops 연산자 클래스를 정의합니다.
이어지는 examples/ai/edge-functions/supabase/migrations/20240410031515_vector-search.sql의 query_embeddings()는 <#> 연산자로 음의 내적을 계산하고 정렬합니다.
연습: 왜 임계값에 음수가 붙는지, 왜 모델이 만드는 벡터의 차원이 열의 384와 맞아야 하는지 설명해 보세요.
예제 주석의 코사인 유사도와 내적의 결과 동등성은 벡터가 정규화됐다는 전제를 갖습니다. 모든 모델 출력에 무조건 성립하는 설명으로 외우지 마세요.
.github/workflows/pg-meta-tests.yml은 SQL 패키지 변경 시 테스트를 실행합니다.
.github/workflows/self-host-tests-smoke.yml은 default, logs, kong, rustfs, kong-rustfs, pgbouncer 조합을 행렬로 검사합니다. 한 가지 기본 구성만 통과해서는 선택 구성을 보장할 수 없다는 판단이 드러납니다.
연습: 함수 하나의 결과 검사, SQL 실행 계획 검사, 여러 서비스의 상태 검사가 각각 잡을 수 있는 오류를 한 개씩 적어 보세요. 비용이 다른 검사를 같은 수준으로 반복하기보다 위험이 있는 경계에 맞추어 배치하는 연습입니다.
기술을 배웠는지 확인하는 가장 좋은 질문은 ‘이 도구가 어떤 상태와 권한의 경계를 책임지는가’를 자기 말로 설명할 수 있는가입니다.
공식 자체 호스팅 안내의 수치는 apps/docs/content/guides/self-hosting/docker.mdx의 시스템 요구사항 표에서 확인할 수 있습니다.
전체 구성요소를 실행할 때의 기준이며, 특정 앱의 동시 접속자 수나 응답 시간을 보증하는 수치는 아닙니다.
| 자원 | 공식 최소 | 공식 권장 |
|---|---|---|
| 메모리 | 4 GB | 8 GB 이상 |
| CPU | 2코어 | 4코어 이상 |
| 디스크 | 40 GB SSD | 80 GB 이상 SSD |
같은 안내는 불필요한 서비스를 제거하면 자원 요구량을 줄일 수 있고, 로그·분석 서비스를 추가하면 더 많은 자원이 필요하다고 설명합니다. 데이터량·인덱스·업로드 파일·백업은 계속 디스크를 차지하므로, 초기 컨테이너가 실행되는 것과 장기 운영에 충분한 것은 다릅니다.
| 목적 | 필요한 준비 | 구분할 점 |
|---|---|---|
| 소스와 도식 읽기 | 텍스트 편집기, Git, 검색 도구 | Node·Docker·GPU 없이 코드의 흐름을 분석할 수 있음 |
| Studio 개발 환경 | Node >=22.13, pnpm 11.13.1, API 대상 | 화면 서버만 켜도 데이터 서비스가 자동으로 모두 생기지는 않음 |
| 전체 자체 호스팅 | Docker·Compose와 위의 공식 자원 기준 | 호스트와 컨테이너에 할당된 실제 메모리를 함께 확인 |
| AI 예제 확장 | 예제별 Python·모델·추론 환경 | 임베딩 생성 비용과 PostgreSQL의 벡터 저장 비용을 구분 |
개발 PC의 여유 메모리로는 16 GB 이상을 잡으면 브라우저·편집기·빌드·컨테이너를 함께 다루기 편할 수 있습니다. 이는 공식 최소 사양이 아닌 학습 환경을 위한 여유치 제안입니다. 실제 CI도 Studio 단위 테스트에 약 3 GB, 타입 검사와 일부 빌드에 약 4 GB의 Node 힙 상한을 둡니다. 힙 상한은 전체 프로세스 메모리와 같지 않습니다.
8082, Studio Docker 이미지의 서버는 3000입니다.8000입니다. 내부 서비스의 같은 숫자 포트와 구분하세요.5432와 6543에 연결합니다. 세션·트랜잭션 풀링의 용도를 따로 읽어야 합니다.기본 Compose에 GPU 장치 요청은 없습니다. 벡터를 데이터베이스에 저장하고 검색하는 일과 신경망 모델로 임베딩을 만드는 일을 분리하면, 어떤 단계에 별도 추론 자원이 필요한지 판단하기 쉽습니다.
시스템 요구사항은 제품 이름 하나에 붙는 숫자가 아니라, 실제로 실행할 서비스와 보관할 데이터의 합으로 결정됩니다.
아래 과제는 서버를 설치하거나 저장소 코드를 실행하지 않아도 진행할 수 있습니다. 명령은 저장소 루트를 기준으로 파일을 검색하거나 읽습니다. 완성 기준은 ‘파일을 열었다’가 아니라 입력·출력·권한·실패 조건을 설명할 결과물입니다.
/rest/v1/todos가 어느 서비스로 가는지, Studio의 스키마 조회는 어떤 경로를 지나는지 각각 화살표 다섯 개 안팎으로 그립니다.
완성 기준: 일반 앱 경로에는 PostgREST, 관리 경로에는 Studio 서버와 postgres-meta가 나타나야 합니다. 두 경로의 인증·권한 주체가 같다고 가정하지 않아야 합니다.
# 경로와 호출 위치만 찾아 읽는다.
rg -n 'prefix: /rest/v1/|cluster: rest|prefix_rewrite:' \
docker/volumes/api/envoy/lds.template.yaml
rg -n 'getSchemas|useSchemasQuery|executeSql' \
apps/studio/data/database/schemas-query.ts
sed -n '1,110p' \
'apps/studio/pages/api/platform/pg-meta/[ref]/query/index.ts'
ident()와 literal() 구현을 읽고 아래 세 입력의 결과를 손으로 적습니다. 그 뒤 packages/pg-meta/test/pg-format.test.ts의 관련 검증과 비교합니다.
완성 기준: 큰따옴표와 작은따옴표의 역할, 따옴표를 겹쳐 쓰는 이유, 일반 문자열을 safeSql에 바로 넣지 않는 이유를 설명합니다.
| 입력 | 예상 SQL 조각 | 확인할 이유 |
|---|---|---|
ident('user notes') | "user notes" | 공백이 있는 이름의 인용 |
literal("O'Reilly") | 'O''Reilly' | 문자열 내부 작은따옴표 처리 |
literal(null) | NULL | 문자열 'NULL'과 다른 의미 |
심화 질문: 테이블 이름을 literal()로 처리하면 왜 원하는 쿼리가 되지 않을까요?
이름과 값을 안전하게 표현하는 방법은 서로 대체할 수 없습니다. 안전성뿐 아니라 SQL 문법상 의미도 맞아야 합니다.
할 일 예제의 SELECT·INSERT·UPDATE 정책을 읽습니다. A와 B의 행이 하나씩 있다고 가정하고, A가 할 수 있는 동작을 표로 만듭니다.
완성 기준: A가 자기 행을 읽는 경우와 B의 행을 읽는 경우를 구분하고, 로그인하지 않은 요청과 관리자 역할의 차이를 적습니다. 테이블 권한과 행 정책이 모두 관련된다는 설명을 포함합니다.
-- 원본 예제를 설명하기 위한 별도의 정책 설계 예
-- 테이블 접근 권한이 있다는 전제에서 정책을 판정한다.
alter table public.todos enable row level security;
create policy own_todos_select on public.todos
for select to authenticated
using ((select auth.uid()) = user_id);
create policy own_todos_update on public.todos
for update to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
여기서 USING은 수정 전의 행을 고르고, WITH CHECK는 수정 후의 행을 검사합니다.
원본 예제의 UPDATE 정책은 USING만 적습니다. WITH CHECK를 생략하면 USING 조건을 수정 후 행의 검사에도 재사용하므로 곧바로 취약점이라고 단정하면 안 됩니다. 위 예는 두 의도를 명시적으로 보여줍니다.
추가 질문: A가 자기 행의 user_id를 B로 바꾸려 하면 어떤 조건이 막을까요?
UPDATE에는 행을 볼 수 있는 SELECT 정책도 함께 고려해야 합니다. 관리자 역할로 확인한 결과는 일반 사용자 정책의 검증을 대신하지 못합니다.
databaseKeys를 읽고 A·B 프로젝트의 스키마 목록과 같은 프로젝트의 두 테이블 열 목록을 키 배열로 적습니다. A의 스키마 생성 뒤 갱신할 키를 골라 봅니다.
완성 기준: 프로젝트를 누락한 키가 왜 충돌하는지, 한 자원만 무효화할 때와 프로젝트 접두 키를 무효화할 때의 범위 차이를 설명합니다.
['projects', 'A', 'schemas']
['projects', 'B', 'schemas']
['projects', 'A', 'table-columns', 'public', 'todos']
['projects', 'A', 'table-columns', 'public', 'profiles']
// A의 스키마 조회만 갱신할 때의 기준 키는 무엇일까?
getTableEditorSql()의 CTE별 대상 카탈로그와 ID 필터를 표로 만듭니다. 필터 하나가 사라졌을 때 어떤 테이블 전체 탐색이 늘어날지 예상합니다.
완성 기준: collectSeqScans()가 모으는 정보, allowedSeqScans의 한도, 시간 예산을 함께 설명하고 새 SQL 하나에 적용할 예산 초안을 작성합니다.
// 실행하지 않고 검토하는 테스트 예산 설계 예
{
allowedSeqScans: {
pg_constraint: {
max: 2,
reason: '역방향 외래 키 탐색에 필요한 구조적 스캔',
},
},
maxExecutionTimeMs: 1000,
}
EXPLAIN ANALYZE는 쿼리를 실제 실행한다는 점도 답안에 포함하세요.
합성 카탈로그의 기본 테이블 수는 packages/pg-meta/test/db/stress-catalog.ts에서 2000입니다. 이 크기에서 만든 예산은 모든 운영 환경의 성능 보증과 다릅니다.
실습의 목표는 큰 서비스를 켜는 데서 끝나지 않고, 요청 한 건의 데이터·권한·캐시·비용을 끝까지 설명하는 것입니다.
HTML·JavaScript의 기본 문법을 알고 있다는 전제로 6주 흐름을 제안합니다. 모든 패키지를 같은 깊이로 공부하기보다, 작은 요청 한 건을 이해한 뒤 그 주변으로 범위를 넓혀 가세요. 각 주의 결과물은 다음 주의 질문을 만드는 재료입니다.
| 주차 | 배울 개념 | 읽을 소스 | 결과물 |
|---|---|---|---|
| 1주차 | 관계형 모델, 기본 키·외래 키, CRUD | 할 일 예제의 examples/todo-list/nextjs-todo-list/supabase/migrations/20230712094349_init.sql | 테이블 관계도와 제약 조건 설명 |
| 2주차 | HTTP 경로, 프록시, 상태 검사 | docker/docker-compose.yml과 Envoy 설정 | REST 요청과 관리 요청의 여행 지도 |
| 3주차 | JWT, 역할, GRANT, RLS | 할 일 정책과 함수 라우터의 isValidHybridJWT() | 사용자 A·B·비로그인·관리자의 접근 판정표 |
| 4주차 | React 서버 상태, 캐시 키, 취소·갱신 | apps/studio/data/database/schemas-query.ts → apps/studio/data/database/keys.ts → executeSql() | 조회부터 무효화까지의 상태 흐름도 |
| 5주차 | 브랜드 타입, SQL 조립, 런타임 검증 | packages/pg-meta/src/pg-format/index.ts와 packages/pg-meta/src/pg-meta-schemas.ts | 신뢰 경계와 입력 변환 규칙 표 |
| 6주차 | 카탈로그, 실행 계획, 성능 회귀 방지 | packages/pg-meta/src/sql/studio/table-editor/table.ts → packages/pg-meta/test/db/plan-guard.ts | CTE별 필터 지도와 테스트 예산 초안 |
apps/studio/pages/와 apps/studio/routes/의 연결을 비교하며 점진적 프레임워크 전환을 배웁니다. 기능과 실행 환경을 한 번에 교체하지 않는 방법을 설명해 보세요.공부의 순서는 제품 기능 목록보다 데이터 한 건의 이동 경로를 기준으로 잡을 때 서로 다른 기술을 연결하기 쉽습니다.
| 용어 | 쉬운 설명 |
|---|---|
| PostgreSQL | 표와 관계, 트랜잭션, 역할을 갖춘 관계형 데이터베이스입니다. Supabase의 데이터 중심축입니다. |
| 스키마 | 테이블과 함수 등을 이름으로 묶는 공간입니다. 폴더와 비슷하지만 데이터베이스 권한과도 연결됩니다. |
| PostgREST | 데이터베이스의 테이블·뷰·함수를 HTTP API 자원으로 제공하는 서버입니다. |
| API 게이트웨이 | 외부 요청을 받아 알맞은 내부 서비스로 보내는 공통 입구입니다. 기본 Compose에서는 Envoy입니다. |
| JWT | 서명된 주장들을 담는 토큰 형식입니다. 내용을 읽는 것과 서명·유효 조건을 검증하는 것은 다릅니다. |
| JWKS | 서명 검증 등에 쓰는 키를 JSON으로 표현한 묶음입니다. 함수 라우터는 설정된 키 집합을 검증에 사용합니다. |
| 인증 · 인가 | 인증은 요청자의 신원을 확인하는 일, 인가는 그 요청자가 무엇을 할 수 있는지 판단하는 일입니다. |
| GRANT | 역할에 테이블 조회 같은 권한을 부여하는 SQL 명령입니다. 행별 정책을 켜는 명령과는 다릅니다. |
| RLS | 같은 테이블에서도 사용자나 역할에 따라 접근 가능한 행을 제한하는 데이터베이스 규칙입니다. |
| publishable key | 공개 클라이언트가 프로젝트 API에 접근할 때 쓰는 키입니다. 사용자의 행 권한을 대신 판단하지 않습니다. |
| service_role | 일반 클라이언트에 노출하면 안 되는 높은 권한의 역할입니다. 일반 사용자의 RLS 동작을 시험하는 용도로 부적절합니다. |
| 카탈로그 | pg_class·pg_attribute처럼 테이블과 열 자체의 정보를 저장하는 시스템 테이블 집합입니다. |
| OID | PostgreSQL 내부 객체의 식별자입니다. 편집기 SQL은 요청한 테이블의 OID로 조회 범위를 좁힙니다. |
| CTE | WITH로 이름 붙인 쿼리 부분입니다. 복잡한 SQL을 여러 논리 단계로 나눌 때 사용합니다. |
| EXPLAIN | 데이터베이스가 쿼리를 처리할 계획을 보여줍니다. 예상 비용은 실행 시간 단위와 다릅니다. |
| EXPLAIN ANALYZE | 쿼리를 실제 수행하며 계획과 측정 정보를 얻습니다. 읽기 전용 설명 명령이라고 생각하면 안 됩니다. |
| Seq Scan | 대상을 순서대로 훑는 탐색입니다. 작은 대상에는 합리적이지만 불필요하게 큰 범위면 문제가 됩니다. |
| 브랜드 타입 | 같은 기본 타입에 의미를 구분하는 표식을 더하는 기법입니다. 일반 문자열과 SQL 조각을 구분하는 데 쓰입니다. |
| 캐시 무효화 | 저장해 둔 조회 결과가 오래됐음을 표시하여 필요한 시점에 새로 가져오게 하는 과정입니다. |
| 모노레포 | 여러 앱과 공통 패키지를 하나의 저장소에서 관리하는 방식입니다. 모두 같은 프로세스로 실행된다는 뜻은 아닙니다. |
| catalog · lockfile | catalog는 공통 의존성 버전 선언, 잠금 파일은 실제로 선택된 의존성 버전과 해석 정보를 담습니다. |
| 연결 풀 | 매 요청마다 데이터베이스 연결을 새로 만드는 부담을 줄이도록 연결을 관리하고 재사용하는 계층입니다. |
| WebSocket | 클라이언트와 서버가 연결을 유지하며 양방향으로 메시지를 주고받는 통신 방식입니다. |
| 임베딩 | 문장이나 이미지 등의 특징을 숫자 벡터로 표현한 값입니다. 벡터의 차원과 정규화 조건이 중요합니다. |
| HNSW | 벡터를 빠르게 찾기 위한 근사 최근접 이웃 인덱스 방식입니다. 정확도·속도·메모리의 균형을 다룹니다. |
| 자체 호스팅 | 서비스를 직접 관리하는 인프라에 배치하는 방식입니다. 업데이트와 데이터 복구 책임도 함께 맡습니다. |
| 항목 | 상태 |
|---|---|
| 저장소 · 선정 | supabase/supabase · GitHub Trending Daily #6 |
| 스타 / 포크 | 113,641 / 17,020 · 2026-10-23 GitHub 기준 |
| 생성일 | 2019-10-12 |
| 주 언어 | TypeScript · GitHub 기준 |
| 최신 릴리스 | v1.26.08 · 2026-08-07 |
| 최근 push | 2026-10-23 |
| 최근 커밋 | 2f548a69 · 2026-10-23docs(postgis): link to the Security Advisor troubleshooting guide (#51439) |
| 루트 라이선스 | Apache-2.0 · 외부 서비스와 개별 예제의 라이선스는 별도 확인 |
| 추적 파일 | 18,190개 · 해당 커밋의 Git 추적 목록 기준 |
| TypeScript 규모 | .ts 3,817개 / 480,353줄.tsx 4,835개 / 602,934줄합계 1,083,287줄 |
| 다른 언어 규모 | JavaScript 계열 약 69,147줄 · SQL 7,714줄 · Shell 6,463줄 Python 228줄 · Rust 6줄 · Go 소스 0줄 |
| 문서·설정 규모 | Markdown·MDX 297,603줄 · JSON 459,039줄 · YAML 110,843줄 |
| 서비스 구성 범위 | 기본 게이트웨이 Envoy · 로그·분석은 선택 구성 · Storage 기본 저장 방식은 파일 |
줄 수는 추적된 일반 파일을 확장자별로 묶고 물리적 줄을 센 대략적인 규모입니다. 주석·빈 줄·테스트·예제·생성 파일을 포함하며, 마지막 개행이 없는 행도 한 줄로 셌습니다.
JavaScript 계열은 .js·.jsx·.mjs·.cjs의 합입니다. 이미지·잠금 파일의 모든 형식을 소스 줄 수에 섞지는 않았습니다. 서비스별 별도 저장소의 코드는 이 수치에 포함되지 않습니다.
이 글의 구현 설명은 커밋 2f548a69와 그 설정 범위에 한정됩니다. 현재 문서의 최신 버전과 당시 잠금 버전은 달라질 수 있습니다. 제품 릴리스 번호, SDK 버전, 각 Docker 이미지 태그도 서로 독립적입니다.
트렌딩 선정과 스타 수는 사용량·품질·그날의 인기 원인을 입증하지 않습니다. Knowledge Base 앱의 존재만으로 순위 상승과의 인과관계를 설명할 수도 없습니다.
자체 호스팅 구성만으로 클라우드의 모든 운영 기능이 재현되지는 않습니다. 성능·보안·복구 능력은 실제 배치 환경과 데이터·정책을 대상으로 검증해야 합니다. 관리자 조회 결과로 일반 사용자 권한을 판단하거나 기본 비밀 값을 그대로 운영에 사용하면 안 됩니다.