문서 · 레퍼런스
프로젝트 API
프로젝트마다 주소가 있고, supabase-js를 그대로 붙여 써요. 아래 경로는 서버 코드와 테스트로 맞춰 둔 목록이에요.
주소와 키
프로젝트 주소는 https://<ref>.<zone>이에요. 요청마다 apikey 헤더(또는 Authorization: Bearer)로 키를 보내요. 공개 키(publishable)는 브라우저용이고 접근 규칙(RLS)을 따라요. 비밀 키(secret)는 모든 규칙을 건너뛰니 서버에서만 써요. 로그인한 사용자의 토큰을 Authorization: Bearer로 보내면 그 사용자로 실행돼요. 키는 API 키 화면에 있어요.
curl "https://abcdefghijklmnopqrst.end.blackall.ai/rest/v1/notes?select=id,title" \
-H "apikey: <publishable key>"
supabase-js 호환
경로·쿼리·헤더·응답 모양이 Supabase와 같아서 @supabase/supabase-js를 그대로 써요. 주소와 키만 바꾸면 돼요. 각 표의 supabase-js 열은 그 경로를 부르는 함수예요.
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('https://abcdefghijklmnopqrst.end.blackall.ai', '<publishable key>')
데이터 (/rest/v1)
PostgREST와 같은 쿼리 언어예요. 표 이름이 경로이고, 열 이름이 쿼리 매개변수예요. 모든 요청은 호출한 사람의 역할과 접근 규칙(RLS)으로 실행돼요.
| 메서드 | 경로 | 하는 일 | supabase-js |
|---|---|---|---|
| GET | /rest/v1/{table} | 행 읽기 | from('t').select() |
| HEAD | /rest/v1/{table} | 개수만 읽기 | select('*', { head: true, count: 'exact' }) |
| POST | /rest/v1/{table} | 행 넣기 (Prefer: resolution=…이면 upsert) | insert() / upsert() |
| PUT | /rest/v1/{table} | 행 하나 upsert | |
| PATCH | /rest/v1/{table} | 필터에 맞는 행 고치기 | update() |
| DELETE | /rest/v1/{table} | 필터에 맞는 행 지우기 | delete() |
| GET | /rest/v1/rpc/{function} | 함수 호출 (읽기) | rpc('f', args, { get: true }) |
| POST | /rest/v1/rpc/{function} | 함수 호출 | rpc('f', args) |
| POST | /graphql/v1 | GraphQL (pg_graphql, 데이터베이스 → 확장에서 켠 프로젝트만, /rest/v1과 같은 키와 접근 규칙) | fetch / Apollo / urql |
쿼리 매개변수
| 이름 | 설명 |
|---|---|
select | 가져올 열. 별칭(label:title), 형변환(id::text), JSON 경로(data->>name), 외래 키로 이어진 행(*,author(name), !inner) |
order | 정렬. created_at.desc.nullslast, 여러 개는 쉼표 |
limit | 가져올 행 수. 최대 1000 |
offset | 건너뛸 행 수 |
on_conflict | upsert에서 겹침을 판단할 열 |
columns | 삽입할 열을 정해 둬요 |
<column> | 열=연산자.값 필터 |
or · and | or=(a.eq.1,b.gt.2), and=(…), 중첩과 not.or=(…) |
필터 연산자
아래 연산자만 받고, 모르는 연산자는 거절해요. 앞에 not.을 붙이면 반대 조건이에요.
| 연산자 | SQL |
|---|---|
eq | = |
neq | <> |
gt | > |
gte | >= |
lt | < |
lte | <= |
like | like |
ilike | ilike |
match | ~ |
imatch | ~* |
cs | @> |
cd | <@ |
ov | && |
sl | << |
sr | >> |
nxr | &< |
nxl | &> |
adj | -|- |
isdistinct | is distinct from |
is | is null | not null | true | false | unknown |
in | in (…) |
fts | @@ to_tsquery() |
plfts | @@ plainto_tsquery() |
phfts | @@ phraseto_tsquery() |
wfts | @@ websearch_to_tsquery() |
헤더
| 이름 | 설명 |
|---|---|
Prefer | return=minimal|representation|headers-only, count=exact|planned|estimated, resolution=merge-duplicates|ignore-duplicates, missing=default |
Accept | Accept: application/vnd.pgrst.object+json은 행 하나를 객체로 받아요(.single()). 0개나 여러 개면 406 |
Accept-Profile | Accept-Profile, Content-Profile은 public만 받아요 |
Content-Range | 응답의 Content-Range로 전체 개수(count=)를 알려 줘요 |
함수 호출 (rpc)
/rest/v1/rpc/<함수>는 프로젝트 스키마의 SQL 함수를 호출한 사람의 역할로 실행해요. POST는 본문이 인자예요. GET은 인자와 이름이 같은 쿼리 매개변수가 인자이고, 나머지는 결과 필터예요.
const { data, error } = await supabase
.from('notes')
.select('id, title, author(name)')
.eq('done', false)
.order('created_at', { ascending: false })
.range(0, 19)
await supabase.from('notes').insert({ title: 'Hello' }).select().single()
await supabase.rpc('search_notes', { query: 'hello' })
curl "https://abcdefghijklmnopqrst.end.blackall.ai/rest/v1/notes?select=id,title&done=eq.false&order=created_at.desc&limit=20" \
-H "apikey: <publishable key>" \
-H "Prefer: count=exact"
지원하지 않는 것
- 집계(
count(),sum()등)와 스프레드(...rel(*)) select public이 아닌 스키마- 한 번에 1000행을 넘는 읽기 (
limit은 1000에서 잘려요)
인증 (/auth/v1)
GoTrue와 같은 경로와 응답이에요. 이메일·비밀번호, 매직 링크, 익명 로그인, 소셜 로그인(PKCE 포함), TOTP 2단계 인증, 비밀 키로 하는 사용자 관리를 지원해요.
| 메서드 | 경로 | 하는 일 | supabase-js |
|---|---|---|---|
| POST | /auth/v1/signup | 가입. 이메일과 비밀번호가 없으면 익명 로그인 | auth.signUp() / auth.signInAnonymously() |
| POST | /auth/v1/token?grant_type=password | 이메일과 비밀번호로 로그인 | auth.signInWithPassword() |
| POST | /auth/v1/token?grant_type=refresh_token | 세션 갱신 | auth.refreshSession() |
| POST | /auth/v1/token?grant_type=pkce | PKCE 코드를 세션으로 교환 | auth.exchangeCodeForSession() |
| POST | /auth/v1/token?grant_type=web3 | 지갑이 서명한 Sign-In with Ethereum 메시지로 로그인 | auth.signInWithWeb3() |
| POST | /auth/v1/token?grant_type=id_token | Google·Apple ID 토큰으로 로그인 | auth.signInWithIdToken() |
| POST | /auth/v1/otp | 매직 링크 메일이나 문자 인증번호 보내기 | auth.signInWithOtp({ email } | { phone }) |
| POST | /auth/v1/verify | 메일의 토큰이나 문자 인증번호 확인 (signup, magiclink, recovery, email_change, sms, phone_change) | auth.verifyOtp() |
| GET | /auth/v1/verify | 메일의 링크를 열었을 때. 등록한 주소로 세션과 함께 돌려보냄 | |
| POST | /auth/v1/resend | 확인 메일 다시 보내기 | auth.resend() |
| POST | /auth/v1/recover | 비밀번호 재설정 메일 | auth.resetPasswordForEmail() |
| POST | /auth/v1/reset | 재설정 토큰으로 새 비밀번호 저장 (돌아갈 주소가 없을 때의 화면이 씀) | |
| GET | /auth/v1/user | 로그인한 사용자와 2단계 인증 요소 | auth.getUser() |
| PUT | /auth/v1/user | 이메일·비밀번호·메타데이터 바꾸기 | auth.updateUser() |
| GET | /auth/v1/reauthenticate | 비밀번호를 바꾸기 전에 확인 코드를 메일로 보내기 | auth.reauthenticate() |
| POST | /auth/v1/logout | 로그아웃 (scope=local|others|global) | auth.signOut({ scope }) |
| GET | /auth/v1/settings | 켜져 있는 로그인 방법 | |
| GET | /auth/v1/authorize | 소셜 로그인 시작 | auth.signInWithOAuth() |
| GET | /auth/v1/callback | 제공자에서 돌아오는 곳 (Apple은 POST) | |
| DELETE | /auth/v1/user/identities/{id} | 연결한 소셜 계정 끊기 | auth.unlinkIdentity() |
| POST | /auth/v1/factors | TOTP 등록 | auth.mfa.enroll({ factorType: 'totp' }) |
| POST | /auth/v1/factors/{id}/challenge | 확인 시작 | auth.mfa.challenge() |
| POST | /auth/v1/factors/{id}/verify | 코드 확인 (aal2 세션) | auth.mfa.verify() |
| DELETE | /auth/v1/factors/{id} | 요소 지우기 | auth.mfa.unenroll() |
| GET | /auth/v1/admin/users | 사용자 목록 (비밀 키, per_page 최대 1000) | auth.admin.listUsers({ page, perPage }) |
| POST | /auth/v1/admin/users | 사용자 만들기 (비밀 키) | auth.admin.createUser() |
| POST | /auth/v1/sso | SAML SSO 시작 (IdP 주소를 돌려줌, PKCE 가능) | auth.signInWithSSO({ domain } | { providerId }) |
| GET | /auth/v1/sso/saml/metadata | 이 프로젝트의 SP 메타데이터 (IdP에 등록) | |
| POST | /auth/v1/sso/saml/acs | IdP가 SAML 응답을 보내는 곳 (서명·대상·재사용 확인) | |
| GET | /auth/v1/admin/sso/providers | SSO 제공자 목록 (비밀 키) | auth.admin.sso.listProviders() |
| POST | /auth/v1/admin/sso/providers | SSO 제공자 추가 (metadata_url 또는 metadata_xml, domains) | auth.admin.sso.createProvider() |
| GET | /auth/v1/admin/sso/providers/{id} | SSO 제공자 하나 | auth.admin.sso.getProvider() |
| PUT | /auth/v1/admin/sso/providers/{id} | SSO 제공자 바꾸기 (인증서 교체 뒤 메타데이터 다시 읽기) | auth.admin.sso.updateProvider() |
| DELETE | /auth/v1/admin/sso/providers/{id} | SSO 제공자 지우기 | auth.admin.sso.deleteProvider() |
| GET | /auth/v1/admin/users/{id} | 사용자 하나 (비밀 키) | auth.admin.getUserById() |
| PUT | /auth/v1/admin/users/{id} | 사용자 고치기 (비밀 키) | auth.admin.updateUserById() |
| DELETE | /auth/v1/admin/users/{id} | 사용자 지우기 (비밀 키) | auth.admin.deleteUser() |
| POST | /auth/v1/invite | 초대 메일 보내기: 프로젝트의 SMTP와 템플릿으로 (비밀 키) | auth.admin.inviteUserByEmail() |
| POST | /auth/v1/admin/generate_link | 메일 링크 만들기: signup, invite, magiclink, recovery (비밀 키) | auth.admin.generateLink() |
| GET | /auth/v1/.well-known/jwks.json | 사용자 토큰을 확인하는 공개 키 (ES256) | |
| GET | /auth/v1/.well-known/openid-configuration | OpenID 설정 (issuer, jwks_uri) | |
| GET | /auth/v1/.well-known/oauth-authorization-server | OAuth 인증 서버 메타데이터 (RFC 8414, MCP 클라이언트용) | |
| GET | /.well-known/oauth-authorization-server/auth/v1 | 같은 메타데이터, RFC 8414가 정한 주소 |
토큰 받기 (grant_type): password, refresh_token, pkce, id_token(Google·Apple의 ID 토큰).
소셜 로그인 제공자: google, kakao, naver, apple, github, gitlab, microsoft, facebook, line, discord, x, bitbucket, figma, linkedin_oidc, notion, twitch, slack_oidc, spotify, zoom, azure, keycloak, workos, twitter, slack. 제공자 설정은 콘솔이나 관리 API의 set_auth_provider로 해요.
await supabase.auth.signUp({ email: '[email protected]', password: 'a-long-passphrase' })
await supabase.auth.signInWithPassword({ email: '[email protected]', password: 'a-long-passphrase' })
await supabase.auth.signInWithOAuth({ provider: 'google', options: { redirectTo: 'https://app.example.com' } })
const { data: { user } } = await supabase.auth.getUser()
지원하지 않는 것
- TOTP가 아닌 2단계 인증 (WebAuthn, 전화)
- 재인증(
reauthenticate)과 신원 연결(linkIdentity). 연결 끊기만 돼요 auth.admin.inviteUserByEmail. 대신generateLink({ type: 'invite' })를 써요
파일 (/storage/v1)
Supabase Storage와 같은 경로예요. 파일 기록은 프로젝트의 표에 있어서, 파일을 누가 읽고 쓸지는 그 표의 접근 규칙이 정해요.
파일 하나는 기본 50MB까지 받아요(버킷마다 더 낮게 정할 수 있어요). 서명 URL은 최대 7일, 업로드용 서명 URL은 2시간 유효해요. 버킷 이름은 소문자·숫자·.·_·-이에요.
| 메서드 | 경로 | 하는 일 | supabase-js |
|---|---|---|---|
| GET | /storage/v1/bucket | 버킷 목록 | storage.listBuckets() |
| POST | /storage/v1/bucket | 버킷 만들기 (public, file_size_limit, allowed_mime_types) | storage.createBucket() |
| GET | /storage/v1/bucket/{bucket} | 버킷 하나 | storage.getBucket() |
| PUT | /storage/v1/bucket/{bucket} | 버킷 설정 바꾸기 | storage.updateBucket() |
| DELETE | /storage/v1/bucket/{bucket} | 빈 버킷 지우기 | storage.deleteBucket() |
| POST | /storage/v1/bucket/{bucket}/empty | 버킷 비우기 | storage.emptyBucket() |
| POST | /storage/v1/object/{bucket}/{path} | 올리기 (x-upsert) | from('b').upload() |
| PUT | /storage/v1/object/{bucket}/{path} | 덮어쓰기 | from('b').update() |
| GET | /storage/v1/object/{bucket}/{path} | 내려받기 (호출한 사람의 권한, HEAD도 됨) | from('b').download() |
| DELETE | /storage/v1/object/{bucket}/{path} | 파일 하나 지우기 | |
| DELETE | /storage/v1/object/{bucket} | 여러 파일 지우기 (prefixes, 최대 1000) | from('b').remove([paths]) |
| GET | /storage/v1/object/public/{bucket}/{path} | 공개 버킷의 파일 | from('b').getPublicUrl() |
| GET | /storage/v1/object/authenticated/{bucket}/{path} | 로그인한 사용자로 내려받기 | |
| POST | /storage/v1/object/list/{bucket} | 폴더 한 단계 목록 (limit 최대 1000) | from('b').list(prefix, { limit, offset, sortBy, search }) |
| GET | /storage/v1/object/info/{bucket}/{path} | 파일 정보 | from('b').info() |
| POST | /storage/v1/object/move | 옮기기 | from('b').move() |
| POST | /storage/v1/object/copy | 복사 | from('b').copy() |
| POST | /storage/v1/object/sign/{bucket}/{path} | 서명 URL 만들기 | from('b').createSignedUrl(path, expiresIn, { transform }) |
| POST | /storage/v1/object/sign/{bucket} | 서명 URL 여러 개 (최대 100) | from('b').createSignedUrls(paths, expiresIn) |
| GET | /storage/v1/object/sign/{bucket}/{path} | 서명 URL로 내려받기 (?token=) | |
| POST | /storage/v1/object/upload/sign/{bucket}/{path} | 업로드용 서명 URL 만들기 | from('b').createSignedUploadUrl() |
| PUT | /storage/v1/object/upload/sign/{bucket}/{path} | 서명 URL로 올리기 (?token=) | from('b').uploadToSignedUrl() |
| GET | /storage/v1/render/image/public/{bucket}/{path} | 공개 이미지 변환 | from('b').getPublicUrl(path, { transform }) |
| GET | /storage/v1/render/image/authenticated/{bucket}/{path} | 로그인한 사용자로 이미지 변환 | from('b').download(path, { transform }) |
| GET | /storage/v1/render/image/sign/{bucket}/{path} | 서명 URL로 이미지 변환 | |
| POST | /storage/v1/upload/resumable | 이어 올리기 시작 (TUS 1.0, Upload-Length, Upload-Metadata에 bucketName·objectName) | tus-js-client / Uppy |
| PATCH | /storage/v1/upload/resumable/{id} | 이어 올리기 (Upload-Offset, application/offset+octet-stream) | |
| HEAD | /storage/v1/upload/resumable/{id} | 지금까지 받은 크기 확인 | |
| DELETE | /storage/v1/upload/resumable/{id} | 이어 올리기 취소 | |
| GET | /storage/v1/s3/{bucket}/{key} | S3 호환 API (SigV4 서명, 콘솔에서 만든 S3 키, path-style, 리전 기본값 ap-northeast-2) | AWS SDK / rclone |
업로드는 본문 그대로(Content-Type이 파일 형식) 보내거나 multipart/form-data로 보내요. POST는 같은 이름이 있으면 거절하고(x-upsert: true면 덮어씀), PUT은 덮어써요.
이미지 변환
/render/image/{public|authenticated|sign}/<버킷>/<경로>에 쿼리로 줘요. 이미지 변환기가 없는 서버에서는 원본을 그대로 주고 x-blackwall-transform 헤더로 알려 줘요.
| 이름 | 설명 |
|---|---|
width | 1~2500 픽셀 |
height | 1~2500 픽셀 |
resize | cover(기본), contain, fill |
quality | 20~100 (기본 80) |
format | origin(기본), webp, avif, png, jpeg |
const bucket = supabase.storage.from('avatars')
await bucket.upload('me.png', file, { upsert: true })
const { data } = bucket.getPublicUrl('me.png', { transform: { width: 200, height: 200 } })
const { data: signed } = await bucket.createSignedUrl('me.png', 3600)
지원하지 않는 것
- 파일마다 다른 캐시 설정(
cacheControl옵션)은 저장하지 않아요
실시간 (/realtime/v1)
wss://<ref>.<zone>/realtime/v1/websocket?apikey=<키>&vsn=1.0.0은 Phoenix 채널(supabase-js가 쓰는 것)이에요. vsn=2.0.0은 배열 모양 메시지와 바이너리 브로드캐스트예요. /realtime/v1(websocket 없이)은 같은 기능의 단순한 JSON 프로토콜이에요.
채널 주제는 realtime:<이름>이에요. 같은 이름에 들어온 클라이언트끼리 브로드캐스트와 접속 상태(presence)를 나눠요.
클라이언트가 보내는 이벤트
| 이벤트 | 설명 |
|---|---|
phx_join | 채널에 들어가요. config.broadcast.self/ack, config.presence.key, config.postgres_changes, access_token |
phx_leave | 채널에서 나가요 |
heartbeat | phoenix 주제로 보내면 서버가 바로 phx_reply로 답해요. 연결 확인용이에요 |
access_token | 로그인한 사용자의 새 토큰으로 바꿔요 |
broadcast | 채널의 다른 사람에게 보내요 (데이터베이스를 거치지 않음) |
presence | track으로 내 상태를 알리고 untrack으로 지워요 |
서버가 보내는 이벤트
| 이벤트 | 설명 |
|---|---|
phx_reply | 보낸 메시지에 대한 답 (ok 또는 error) |
presence_state | 들어갔을 때 이미 있는 사람들 |
presence_diff | 들어오고 나간 사람 |
broadcast | 채널의 다른 사람에게 보내요 (데이터베이스를 거치지 않음) |
postgres_changes | 표의 행이 바뀜 (INSERT, UPDATE, DELETE) |
phx_close | 채널이 닫힘 |
system | 토큰 교체 실패 같은 알림 |
postgres_changes는 콘솔이나 set_realtime으로 실시간을 켠 표에서만 와요. 각 행은 받는 사람의 접근 규칙으로 확인한 뒤 보내요. 필터는 열=eq|neq|lt|lte|gt|gte|in.값이에요.
supabase
.channel('room-1')
.on('broadcast', { event: 'cursor' }, ({ payload }) => console.log(payload))
.on('presence', { event: 'sync' }, () => console.log('online'))
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'notes', filter: 'done=eq.false' }, (change) => console.log(change.new))
.subscribe()
wss://abcdefghijklmnopqrst.end.blackall.ai/realtime/v1/websocket?apikey=<publishable key>&vsn=1.0.0
{"topic":"realtime:room-1","event":"phx_join","payload":{"config":{"broadcast":{"self":false},"presence":{"key":"me"},"postgres_changes":[{"event":"*","schema":"public","table":"notes"}]}},"ref":"1","join_ref":"1"}
{"topic":"realtime:room-1","event":"broadcast","payload":{"type":"broadcast","event":"cursor","payload":{"x":1}},"ref":"2","join_ref":"1"}
{"topic":"phoenix","event":"heartbeat","payload":{},"ref":"3"}
지원하지 않는 것
- 비공개 채널(
config.private: true) - UPDATE의 이전 값 (
old는 비어 있어요)
함수 (/functions/v1)
/functions/v1/<이름>은 배포한 함수를 불러요. 모든 HTTP 메서드를 그대로 전하고, 본문은 2MB까지예요. 비공개(기본) 함수는 이 프로젝트의 키가 있어야 부를 수 있어요: 공개 키(anon), 로그인한 사용자의 토큰(authenticated), 비밀 키(service_role). 공개 함수는 키 없이 불러요. 함수는 토큰 대신 확인된 클레임을 받아요. 이름은 소문자로 시작하는 소문자·숫자·-이에요.
| 메서드 | 경로 | 하는 일 | supabase-js |
|---|---|---|---|
| * | /functions/v1/{name} | 함수 호출 (모든 메서드) | functions.invoke('name', { body }) |
const { data, error } = await supabase.functions.invoke('hello', { body: { name: 'world' } })
curl -X POST "https://abcdefghijklmnopqrst.end.blackall.ai/functions/v1/hello" \
-H "Authorization: Bearer <user token or secret key>" \
-H "content-type: application/json" \
-d '{"name":"world"}'
그 밖의 경로
| 메서드 | 경로 | 하는 일 | supabase-js |
|---|---|---|---|
| POST | /kv/v1 | KV 명령 (Upstash 형식 ["SET", "k", "v"], /kv/v1/pipeline, /kv/v1/keys, /kv/v1/usage도, 비밀 키) | @upstash/redis |
| * | /kv/v1/{key} | KV 키 하나 읽기·쓰기·지우기 (GET·PUT·DELETE, /incr, /expire) | |
| GET | /config/v1 | 엣지 설정 전체 (비밀 키) | @vercel/edge-config |
| GET | /config/v1/{key} | 엣지 설정 값 하나 | get('key') |
| POST | /ai/v1/{endpoint} | AI 게이트웨이, OpenAI 호환 (chat/completions, embeddings, images/generations, audio/speech, audio/transcriptions, models) | openai SDK (baseURL) |
| GET | /health | 프로젝트가 응답하는지 ({ project, ok }) | |
| GET | /realtime/v1/websocket | Phoenix 채널 (websocket) | channel('name').subscribe() |
| GET | /realtime/v1 | 단순 JSON 프로토콜 (websocket) |
오류
오류는 경로마다 Supabase 클라이언트가 읽는 모양 그대로예요. 데이터는 PostgREST처럼 code(PGRST…), message, details, hint이고, 인증은 GoTrue처럼 code, error_code, msg(토큰 받기는 OAuth의 error, error_description도), 파일은 storage-js처럼 statusCode, error, message예요. 메시지는 Accept-Language(한국어, 영어, 일본어)를 따라요.
{ "code": "PGRST116", "message": "…", "hint": null, "details": "…" }