One Server

문서 · 레퍼런스

프로젝트 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/v1GraphQL (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_conflictupsert에서 겹침을 판단할 열
columns삽입할 열을 정해 둬요
<column>열=연산자.값 필터
or · andor=(a.eq.1,b.gt.2), and=(…), 중첩과 not.or=(…)

필터 연산자

아래 연산자만 받고, 모르는 연산자는 거절해요. 앞에 not.을 붙이면 반대 조건이에요.

연산자SQL
eq=
neq<>
gt>
gte>=
lt<
lte<=
likelike
ilikeilike
match~
imatch~*
cs@>
cd<@
ov&&
sl<<
sr>>
nxr&<
nxl&>
adj-|-
isdistinctis distinct from
isis null | not null | true | false | unknown
inin (…)
fts@@ to_tsquery()
plfts@@ plainto_tsquery()
phfts@@ phraseto_tsquery()
wfts@@ websearch_to_tsquery()

헤더

이름설명
Preferreturn=minimal|representation|headers-only, count=exact|planned|estimated, resolution=merge-duplicates|ignore-duplicates, missing=default
AcceptAccept: application/vnd.pgrst.object+json은 행 하나를 객체로 받아요(.single()). 0개나 여러 개면 406
Accept-ProfileAccept-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=pkcePKCE 코드를 세션으로 교환auth.exchangeCodeForSession()
POST/auth/v1/token?grant_type=web3지갑이 서명한 Sign-In with Ethereum 메시지로 로그인auth.signInWithWeb3()
POST/auth/v1/token?grant_type=id_tokenGoogle·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/factorsTOTP 등록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/ssoSAML SSO 시작 (IdP 주소를 돌려줌, PKCE 가능)auth.signInWithSSO({ domain } | { providerId })
GET/auth/v1/sso/saml/metadata이 프로젝트의 SP 메타데이터 (IdP에 등록)
POST/auth/v1/sso/saml/acsIdP가 SAML 응답을 보내는 곳 (서명·대상·재사용 확인)
GET/auth/v1/admin/sso/providersSSO 제공자 목록 (비밀 키)auth.admin.sso.listProviders()
POST/auth/v1/admin/sso/providersSSO 제공자 추가 (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-configurationOpenID 설정 (issuer, jwks_uri)
GET/auth/v1/.well-known/oauth-authorization-serverOAuth 인증 서버 메타데이터 (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 헤더로 알려 줘요.

이름설명
width1~2500 픽셀
height1~2500 픽셀
resizecover(기본), contain, fill
quality20~100 (기본 80)
formatorigin(기본), 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채널에서 나가요
heartbeatphoenix 주제로 보내면 서버가 바로 phx_reply로 답해요. 연결 확인용이에요
access_token로그인한 사용자의 새 토큰으로 바꿔요
broadcast채널의 다른 사람에게 보내요 (데이터베이스를 거치지 않음)
presencetrack으로 내 상태를 알리고 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/v1KV 명령 (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/websocketPhoenix 채널 (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": "…" }