~/web-app-dev.md
WEB_APP_DEV

@supabase/server는 서버 함수의 보일러플레이트가 아니라 요청 권한 계약이다

10분 읽기·2026.07.30·출처 4·00
오늘의 질문

Edge Function과 API route마다 반복하던 인증 코드는 단순 중복일까, 아니면 요청 권한 계약이 흩어져 있다는 신호일까?

web-app-dev.md
server auth contract

@supabase/server는 서버 함수의 보일러플레이트가 아니라 요청 권한 계약이다

1. 왜 지금 봐야 하나

Supabase가 2026년 5월 @supabase/server를 public beta로 공개했고, 현재 문서는 v1.4.1 기준으로 “v1.X public beta, SemVer 아래 첫 안정 계열”이라고 설명한다. Supabase 블로그가 이 패키지를 소개한 이유는 꽤 현실적이다. Supabase가 익명 분석한 25,000개의 배포된 Edge Functions에서 개발자들이 거의 같은 준비 코드를 반복하고 있었다는 것이다.

반복되던 코드는 대략 이런 것들이다.

  • SUPABASE_ANON_KEY로 사용자 스코프 클라이언트 만들기
  • SUPABASE_SERVICE_ROLE_KEY로 RLS를 우회할 수 있는 admin client 만들기
  • JWT 검증하기
  • claims 파싱하기
  • CORS 처리하기
  • auth context를 handler에 전달하기
  • _shared/*.ts 유틸을 함수마다 복사하기

처음 보면 “좋다, 보일러플레이트 줄이는 SDK구나”로 끝날 수 있다. 하지만 웹앱 개발자에게 더 중요한 신호는 따로 있다. 서버 함수마다 흩어진 인증·클라이언트 생성·CORS·컨텍스트 주입 코드는 사실 요청이 어떤 권한 모드로 들어왔는지 정의하는 계약이다.

AI 코딩 도구를 쓰면 이 문제가 더 커진다. 에이전트는 “비슷한 함수 하나 더 만들어줘”라는 요청에 기존 함수를 복사해 빠르게 변형한다. 그 과정에서 service_role 클라이언트가 불필요한 경로에 남거나, JWT 검증은 빠졌는데 데이터 조회는 되는 함수가 생기거나, CORS preflight만 임시로 열어둔 코드가 그대로 배포될 수 있다. @supabase/server를 배워야 하는 이유는 편의성보다 이 흩어진 권한 계약을 한 곳에 드러내기 위해서다.

2. 핵심 개념

핵심 개념은 단순하다. 서버 함수는 “함수 하나”가 아니라 요청 인증 모드 → Supabase client 종류 → RLS 적용 여부 → 응답 CORS가 이어지는 파이프라인이다.

@supabase/server 문서의 기본 예시는 withSupabase({ auth: 'user' }, handler) 형태다. 이때 handler는 인증이 성공한 뒤에만 실행되고, ctx.supabase, ctx.supabaseAdmin, ctx.userClaims, ctx.jwtClaims, ctx.authMode 같은 context를 받는다.

여기서 중요한 구분이 있다.

1) ctx.supabase: RLS가 적용되는 사용자/익명 클라이언트

auth: 'user' 모드에서 ctx.supabase는 로그인 사용자 JWT에 스코프된 클라이언트다. 문서 예시도 “RLS-scoped — this user only sees their own favorites”라고 설명한다. 즉 이 클라이언트는 “서버에서 실행되니까 안전하다”가 아니라, DB의 RLS 정책을 그대로 통과하는 사용자 관점의 클라이언트다.

auth: 'publishable' 모드에서는 apikey header의 publishable key를 검증하지만, 사용자 identity는 없다. 문서는 이 경우 ctx.supabase가 anonymous, 즉 anon role이며 RLS가 여전히 적용된다고 설명한다. publishable key는 “우리 클라이언트에서 온 요청일 가능성이 높다”는 거친 gate일 뿐, 사용자를 식별하지 않는다.

2) ctx.supabaseAdmin: RLS를 우회할 수 있는 관리자 클라이언트

반대로 supabaseAdmin은 service role 접근을 가진다. 문서 예시는 auth: 'secret'으로 API key를 검증한 뒤 ctx.supabaseAdmin.from('featured_games').select()를 실행한다. 이 패턴은 백오피스, cron, 내부 서비스에는 유용하지만 사용자 요청 handler에 습관적으로 섞이면 위험하다.

따라서 코드 리뷰에서 봐야 할 질문은 “서버 코드니까 admin client를 써도 되나?”가 아니라 “이 endpoint의 요청자가 정말 RLS를 우회해도 되는 주체인가?”다.

3) auth mode는 라우트의 제품 의미다

auth: 'none', auth: 'publishable', auth: 'user', auth: 'secret'은 단순 옵션 이름이 아니다.

  • none: health check, 공개 webhook landing처럼 누구나 호출 가능해야 하는 경로
  • publishable: 로그인 전 카탈로그처럼 사용자 신원은 없지만 앱 클라이언트 gate는 두고 싶은 경로
  • user: 로그인 사용자의 데이터와 RLS 정책을 그대로 써야 하는 경로
  • secret: cron, 내부 admin, 서버 간 호출처럼 사용자 RLS가 아니라 별도 운영 권한이 필요한 경로

이 모드를 라우트마다 명시하면, 서버 함수 파일 자체가 권한 문서가 된다.

3. 최신 이슈와 연결

Supabase Developer Update - May 2026은 @supabase/server를 “auth, client creation, CORS, context injection을 여러 runtime에서 처리하는 새 SDK”로 소개한다. 지원 대상으로 Edge Functions, Vercel Functions, Deno, Bun, Cloudflare Workers를 언급했고, 블로그 본문에는 Hono도 포함되어 있다.

현재 문서도 같은 방향으로 확장되어 있다. Edge Functions/Deno에서는 npm:@supabase/server로 직접 import할 수 있고, npm/pnpm 설치도 가능하다. 또 Hono, H3/Nuxt, Elysia, NestJS 어댑터가 문서화되어 있다. 특히 Hono 어댑터는 @supabase/server/adapters/hono를 통해 Hono v4 계열에 붙일 수 있다고 안내한다.

이 흐름은 “Supabase 전용 Edge Function만 편해졌다”보다 넓다. 작은 팀의 웹앱 backend는 점점 여러 runtime에 걸쳐 흩어진다.

  • Supabase Edge Function: DB와 가까운 작업, webhook, scheduled job
  • Vercel Functions / Next.js Route Handler: 프론트 배포와 붙은 API
  • Hono / Cloudflare Workers: 빠른 edge API, proxy, lightweight backend
  • Bun/Deno: 내부 도구나 별도 API 서버

문제는 runtime이 늘어날수록 auth 코드도 각자 조금씩 달라진다는 점이다. 어떤 곳은 Authorization header를 직접 파싱하고, 어떤 곳은 apikey만 확인하고, 어떤 곳은 CORS를 넓게 열고, 어떤 곳은 service role client를 전역으로 만든다. @supabase/server는 이런 차이를 모두 없애지는 않지만, 최소한 “이 라우트는 어떤 auth mode로 들어와야 하는가”라는 공통 언어를 준다.

npm registry 기준으로도 @supabase/server의 latest는 2026년 7월 22일 수정된 1.4.1이다. 즉 5월 발표에서 끝난 실험 패키지가 아니라, 7월 현재도 빠르게 다듬어지는 서버 권한 유틸 계열로 보는 편이 맞다.

4. 개발자 관점 해석

웹앱 팀이 이 패키지를 볼 때 가장 피해야 할 해석은 “이제 JWT 검증 코드를 직접 안 써도 된다”에서 멈추는 것이다. 더 실무적인 해석은 세 가지다.

첫째, endpoint를 권한 모드별로 분류하라

API 파일 목록을 열고 각 route에 아래 라벨을 붙여보자.

라우트 예시권장 auth mode이유
/healthnone서비스 상태 확인은 사용자 identity가 필요 없다.
/catalogpublishable로그인 전 조회지만, anonymous RLS와 클라이언트 gate를 둘 수 있다.
/me/favoritesuser로그인 사용자별 행만 보여야 한다.
/admin/featuredsecret 또는 별도 admin 권한운영 주체가 RLS를 우회할 수 있는지 별도 검토가 필요하다.
/cron/rebuild-rankingssecret사용자 요청이 아니라 서버 간/스케줄 호출이다.

이 분류가 끝나지 않았다면 패키지 도입보다 설계가 먼저다. withSupabase는 설계를 대신해주지 않는다. 이미 결정한 권한 모드를 코드에 강하게 표현하게 해줄 뿐이다.

둘째, admin client 사용 지점을 “위험 surface”로 표시하라

ctx.supabaseAdmin이 필요한 endpoint는 반드시 이유가 있어야 한다. 예를 들어 여러 사용자의 데이터를 집계하는 cron job이나 운영자가 콘텐츠를 큐레이션하는 route는 그럴 수 있다. 하지만 사용자가 자기 프로필을 수정하는 route에서 admin client를 쓰고, request body의 user_id를 믿어버리면 RLS를 우회한 horizontal privilege escalation이 된다.

실무 체크는 간단하다.

  • handler 안에서 supabaseAdmin을 쓰는 줄을 검색한다.
  • 해당 라우트의 auth mode가 secret 또는 확실한 admin 검증인지 확인한다.
  • 사용자 입력의 user_id, project_id, team_id로 admin query를 필터링한다면, 그 값이 claims나 별도 권한 테이블과 매칭되는지 확인한다.

셋째, AI 에이전트에게 “권한 표”를 먼저 주라

Supabase 문서는 npx skills add supabase/server처럼 AI coding agent용 skill도 언급한다. 이건 흥미로운 신호다. 패키지 사용법 자체를 AI에게 알려주는 것도 중요하지만, 더 중요한 것은 프로젝트의 라우트 권한 표를 같이 주는 것이다.

예를 들어 에이전트에게 이렇게 지시할 수 있다.

zsh — 생존확인.sh
새 API route를 만들 때는 먼저 auth mode를 하나 고른다.
- 로그인 사용자 데이터: withSupabase({ auth: 'user' })
- 로그인 전 공개 카탈로그: withSupabase({ auth: 'publishable' })
- 운영/cron: withSupabase({ auth: 'secret' }) + admin client 사용 이유 주석
- health check: auth 없음
supabaseAdmin은 사용자 요청 route에서 기본 사용 금지.

이런 규칙이 없으면 에이전트는 가장 가까운 예시를 복사한다. 그리고 가장 가까운 예시가 admin client를 쓰고 있다면, 새 endpoint에도 그 위험이 전파된다.

5. 내 프로젝트에 적용할 체크포인트

  • 서버 함수와 API route 목록을 뽑고 none, publishable, user, secret 중 하나로 분류한다.
  • 각 route가 쓰는 Supabase client를 표시한다: RLS-scoped client인지, service role admin client인지.
  • supabaseAdmin이 등장하는 handler에는 “왜 RLS 우회가 필요한가” 주석이나 설계 문서를 붙인다.
  • 로그인 전 route에서 publishable key를 사용자 identity처럼 오해하지 않는다. publishable은 known client gate일 뿐이다.
  • 사용자 route는 가능하면 ctx.supabase와 RLS로 먼저 해결하고, admin query는 마지막 선택지로 둔다.
  • CORS를 route별 제품 경계로 본다. “일단 *”가 아니라 어떤 origin의 어떤 클라이언트가 부를 수 있는지 적는다.
  • Hono, Edge Functions, Vercel Functions처럼 runtime이 여러 개라면 같은 auth mode 이름을 문서에 통일한다.
  • AI 코딩 도구용 프로젝트 규칙에 @supabase/server auth mode와 admin client 금지/허용 조건을 넣는다.

6. 오늘 10분 액션

  1. 현재 프로젝트에서 SUPABASE_SERVICE_ROLE_KEY, createClient, Authorization, apikey, cors를 검색한다.
  2. 각 파일 옆에 public, client-gated, user, internal 네 라벨 중 하나를 적는다.
  3. service_role 또는 supabaseAdmin을 쓰는 곳을 별도 목록으로 만든다.
  4. 그중 사용자 요청으로 호출되는 route가 있으면, RLS-scoped ctx.supabase로 바꿀 수 있는지 검토한다.
  5. 새 API route 템플릿에 withSupabase({ auth: ... })와 “왜 이 auth mode인가?” 주석을 넣는다.

7. 더 볼 자료

중복 회피 메모

로컬 content/generated와 Supabase 최근 web_app_dev 글을 확인했다. 최근 글은 Custom OIDC 신원 경계, TanStack DB 클라이언트 데이터 그래프, RLS 권한 행렬 테스트, Realtime binary payload, Hono 입력 객체 경계, Next.js Partial Prefetching/App Shell, Supabase Pipelines/CDC, PostgREST JWT kid, self-hosted Auth URL 등을 다뤘다. 2026-05-30 글이 Supabase 신규 테이블 Data API 자동 노출과 GRANT/RLS 기본값 전환을 다룬 것도 확인했다. 이번 글은 Supabase를 다시 다루지만 Data API 노출 기본값, RLS 정책 테스트, OIDC identity linking이 아니라 @supabase/server를 통해 서버 함수별 auth mode, RLS-scoped client, service-role admin client, CORS/context injection을 하나의 요청 권한 계약으로 정리하는 데 집중한다.

오늘 10분 액션+5%

댓글 0

최신순 ▾
한 줄 남기기

혹시 이 글을 읽는 동료 개발자가 있다면, GitHub으로 로그인하고 한 줄 흔적을 남겨줘요. (스팸 방지용 로그인이에요)