콘텐츠로 이동
Study NoteSupabase

3. 시작하기

대시보드는 탐색과 디버깅용이다. 스키마 변경의 진실은 migrations/에 있다

대시보드에서 바로 시작하는 경로와 CLI로 로컬에서 시작해 마이그레이션으로 배포하는 경로

A 경로는 감을 잡는 데 좋다. 5분이면 동작하는 것을 본다. B 경로가 실제 개발 방식이다. 스키마가 코드로 남고, 팀과 공유되고, 되돌릴 수 있다.

이 장은 A로 시작해서 B로 넘어간다. A에서 멈추면 나중에 반드시 아프다.

  1. supabase.com/dashboard에서 GitHub 로그인

  2. New project → 조직 선택

  3. 입력할 것 세 가지

    • Name — 프로젝트 이름
    • Database Password — Postgres postgres 사용자 비밀번호. 여기서 잘 저장해 둘 것
    • Region — 사용자와 가장 가까운 곳 (한국 서비스면 Northeast Asia (Seoul))
  4. 1~2분 기다리면 프로비저닝 완료

메뉴하는 일자주 쓰나
Table Editor스프레드시트처럼 테이블 보기/편집◎
SQL Editor임의 SQL 실행, 저장된 쿼리◎
Authentication사용자 목록, 로그인 제공자 설정, 이메일 템플릿◎
Storage버킷과 파일 관리○
Database스키마, 함수, 트리거, 확장, 역할, 복제 설정○
Edge Functions배포된 함수와 로그○
Reports / Logs쿼리 성능, API 로그, 에러 추적○ (문제 생겼을 때)
Advisors보안·성능 자동 점검 결과◎ 꼭 볼 것
Project SettingsAPI 키, 연결 문자열, 컴퓨트 설정○

Project Settings → API Keys에서 확인한다.

터미널 창
# 프로젝트 URL — 모든 요청의 베이스
NEXT_PUBLIC_SUPABASE_URL=https://abcdefghijklmno.supabase.co
# publishable key — 브라우저에 노출해도 되는 키
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_xxxxxxxxxxxx
# secret key — 서버에서만. 절대 NEXT_PUBLIC_ 접두사를 붙이지 말 것
SUPABASE_SECRET_KEY=sb_secret_xxxxxxxxxxxx
  • 예전 프로젝트에는 anon / service_role JWT 키가 보인다. 폐기 예정이므로 신규는 새 키를 쓴다
  • NEXT_PUBLIC_ 접두사가 붙은 값은 브라우저 번들에 그대로 들어간다. secret key에 붙이면 즉시 사고다
  • 키가 유출됐다면 대시보드에서 회전(rotate)할 수 있다

Table Editor 클릭보다 SQL로 만드는 습관을 들이자. 나중에 그대로 마이그레이션이 된다.

create table public.posts (
id bigint generated always as identity primary key,
author_id uuid not null default auth.uid() references auth.users (id) on delete cascade,
title text not null check (char_length(title) between 1 and 200),
body text,
published boolean not null default false,
created_at timestamptz not null default now()
);
-- 테이블을 만들면 RLS를 켜는 것까지가 한 세트다
alter table public.posts enable row level security;
-- 정책이 없으면 아무도 못 읽는다. 최소 정책 두 개
create policy "누구나 공개 글을 읽는다"
on public.posts for select
using (published = true);
create policy "본인 글은 본인이 관리한다"
on public.posts for all
to authenticated
using ((select auth.uid()) = author_id)
with check ((select auth.uid()) = author_id);
터미널 창
npm install @supabase/supabase-js
lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
)
// 조회 — SELECT id, title, created_at FROM posts WHERE published ORDER BY created_at DESC LIMIT 10
const { data, error } = await supabase
.from('posts')
.select('id, title, created_at')
.eq('published', true)
.order('created_at', { ascending: false })
.limit(10)
// 삽입 — author_id는 넣지 않는다. 기본값 auth.uid()가 채운다
const { data: created, error: insertError } = await supabase
.from('posts')
.insert({ title: '첫 글', body: '내용' })
.select()
.single()

이 클라이언트는 브라우저용이다. Next.js에서는 이렇게 쓰지 않고 @supabase/ssr을 쓴다 (13장).

대시보드만 쓰면 곧 이런 상황이 온다.

  • “이 컬럼 누가 언제 추가했지?” — 기록이 없다
  • “스테이징이랑 프로덕션 스키마가 다른데요” — 손으로 맞춰야 한다
  • “실수로 프로덕션 테이블을 지웠어요” — 되돌릴 코드가 없다
  • “새로 온 사람이 로컬 환경 세팅을 못 해요” — 재현 가능한 정의가 없다

해법은 하나다. 스키마를 SQL 파일로 버전 관리하고, 로컬에서 먼저 돌린다. 그게 CLI가 하는 일이다.

터미널 창
supabase init
  • 디렉터리supabase/
    • config.toml 로컬 스택 설정 — 포트, Auth, 스토리지
    • 디렉터리migrations/ 스키마 변경 이력. 여기가 진실이다
      • …
    • 디렉터리functions/ Edge Functions 소스
      • …
    • seed.sql 로컬 DB 초기 데이터

supabase/ 디렉터리는 반드시 git에 커밋한다. 이게 곧 백엔드 소스 코드다. .gitignore에는 supabase/.temp, supabase/.branches 정도만 넣는다.

터미널 창
# Docker가 켜져 있어야 한다
supabase start
Started supabase local development setup.
API URL: http://127.0.0.1:54321
GraphQL URL: http://127.0.0.1:54321/graphql/v1
DB URL: postgresql://postgres:postgres@127.0.0.1:54322/postgres
Studio URL: http://127.0.0.1:54323
Mailpit URL: http://127.0.0.1:54324
JWT secret: super-secret-jwt-token-with-at-least-32-characters-long
anon key: eyJhbGciOiJIUzI1NiIs...
service_role key: eyJhbGciOiJIUzI1NiIs...

프로덕션과 같은 구성의 스택이 통째로 로컬에 뜬다. Auth도, Storage도, Realtime도 전부 동작한다. 로컬 키는 모든 개발자에게 동일한 고정값이라 커밋해도 안전하다.

출력에 구형 anon key / service_role key(JWT)가 보이는 건 오타가 아니다 — 로컬 스택은 아직 구형 키 체계로 돈다. 고정값이라 무방하고, 새 키(sb_publishable_... / sb_secret_...)는 호스팅된 프로젝트 기준이다.

포트서비스설명
54321API Gateway/rest/v1, /auth/v1, /storage/v1, /functions/v1 전부 여기로
54322Postgrespsql, Prisma, DBeaver로 직접 붙는 주소
54323Studio로컬 대시보드. 원격과 UI가 동일하다
54324Mailpit로컬 메일 서버 — 가입 확인 메일, 매직 링크를 여기서 확인

54324가 특히 유용하다. 이메일 인증이나 매직 링크 플로우를 실제 메일 발송 없이 브라우저에서 그대로 테스트할 수 있다.

터미널 창
supabase migration new create_posts
# → supabase/migrations/20260805120000_create_posts.sql 생성

빈 파일이 생기고, 여기에 SQL을 직접 쓴다. 의도가 명확한 SQL이 남는다는 게 이 방식의 장점이다. 테이블 · RLS · 인덱스를 한 파일 안에서 한 세트로 묶어 쓸 수 있다.

어느 쪽이든 결과물은 migrations/ 안의 SQL 파일이고, 이게 진실이다.

터미널 창
supabase db reset

이 명령이 하는 일 —

  1. 로컬 DB를 완전히 비운다

  2. migrations/ 안의 SQL을 타임스탬프 순서대로 전부 재실행한다

  3. seed.sql(또는 config에 지정한 시드)을 실행한다

-- supabase/seed.sql
insert into auth.users (id, email, encrypted_password, email_confirmed_at, role, aud)
values (
'00000000-0000-0000-0000-000000000001',
'dev@example.com',
crypt('password123', gen_salt('bf')),
now(), 'authenticated', 'authenticated'
);
insert into public.posts (author_id, title, body, published) values
('00000000-0000-0000-0000-000000000001', '첫 글', '내용', true),
('00000000-0000-0000-0000-000000000001', '초안', '아직 작성 중', false);
  • auth.users 직접 insert는 시드 편의용이다. GoTrue가 쓰는 토큰 컬럼들이 비어 이 계정으로 로그인은 안 될 수 있다 — 로그인 테스트 계정은 Studio나 signUp으로 만든다
  • 시드는 로컬과 프리뷰 브랜치에만 적용된다. 프로덕션에는 실행되지 않는다
  • 고정 UUID를 쓰면 테스트 코드에서 참조하기 쉽다
  • 시드가 커지면 여러 파일로 나누고 config.toml의 [db.seed]에서 glob으로 지정한다

login, link, db push는 비슷해 보여도 서로 다른 자격 증명을 쓴다. 작업 중 명령만 빠르게 찾으려면 3B장 원격 프로젝트 명령어를 본다.

자격 증명용도보관 위치
Personal Access TokenCLI가 프로젝트 목록·설정 같은 Management API를 호출supabase login이 OS 자격 증명 저장소에 보관하거나 SUPABASE_ACCESS_TOKEN으로 주입
프로젝트 ref로컬 supabase/ 디렉터리가 어느 호스팅 프로젝트를 가리키는지 식별supabase link 후 supabase/.temp/에 기록, Git에는 커밋하지 않음
DB 비밀번호CLI가 비밀번호 기반 원격 Postgres 연결을 사용할 때 인증링크할 때 자격 증명 저장소에 보관하거나 SUPABASE_DB_PASSWORD로 주입
터미널 창
# 1) Management API 로그인 — DB 비밀번호와 다른 자격 증명이다
supabase login
# 2) 로컬 디렉터리를 원격 프로젝트에 연결 (project-ref는 대시보드 URL에 있다)
supabase link --project-ref abcdefghijklmno
# 3) 적용 전 이력과 실행 대상을 확인
supabase migration list --linked
supabase db push --linked --dry-run
# 4) 로컬 마이그레이션을 원격에 적용
supabase db push --linked
# 반대 방향: 원격에서 이미 손댄 스키마를 로컬로 끌어온다
supabase db pull --linked
  • 현재 CLI에서 link의 DB 비밀번호는 선택 사항이다. 비밀번호 없이 연결되는 인증 경로를 쓸 수 있고, 입력했다면 네이티브 자격 증명 저장소에 보관되어 이후 db push 명령에 매번 적지 않아도 된다
  • 저장된 자격 증명을 쓸 수 없거나 CI에서 실행한다면 SUPABASE_DB_PASSWORD를 프로세스 환경에 주입한다
  • db push는 아직 적용되지 않은 마이그레이션만 순서대로 실행한다
  • 기본 db push는 seed.sql을 실행하지 않는다. 정말 필요한 환경에서만 --include-seed를 명시한다
  • 원격에서 대시보드로 직접 바꿔 놓은 게 있으면 충돌한다 → db pull로 먼저 흡수
  • 실무에서는 db push를 사람이 직접 치지 않고 CI에서 실행한다 (14장)

결론부터 말하면 .env에 저장할 수는 있지만 db push가 그 파일을 일반 셸 환경처럼 무조건 읽는다고 가정하면 안 된다. Supabase CLI에서 .env가 쓰이는 방식은 세 가지가 서로 다르다.

파일·옵션자동 인식 범위
프로젝트 루트 .envconfig.toml에서 env(NAME)으로 참조한 값을 CLI가 찾는다
프로젝트 루트 .env.localNext.js는 읽지만 Supabase CLI 설정용 자동 로더의 문서화된 파일은 아님
supabase/functions/.env 또는 functions serve --env-file로컬 Edge Function 런타임에 주입한다
secrets set --env-file호스팅 Edge Function 시크릿을 업로드한다

SUPABASE_DB_PASSWORD는 CLI 프로세스의 환경변수다. 다음 중 하나를 사용한다.

터미널 창
# 한 번만 쓸 때 — 값은 셸 히스토리에 남기지 않도록 프롬프트에서 입력해 export하는 편이 낫다
export SUPABASE_DB_PASSWORD='프로젝트-DB-비밀번호'
supabase db push --linked --dry-run
supabase db push --linked
unset SUPABASE_DB_PASSWORD

파일로 관리하려면 앱의 공개 키가 든 .env.local과 분리한 .env.supabase를 만들고, dotenv 로더를 명시적으로 사용한다.

.env.supabase
SUPABASE_DB_PASSWORD="프로젝트-DB-비밀번호"
터미널 창
# .env.supabase는 반드시 .gitignore에 포함한다
npx @dotenvx/dotenvx run -f .env.supabase -- supabase db push --linked --dry-run
npx @dotenvx/dotenvx run -f .env.supabase -- supabase db push --linked

타입 생성 — 여기서 개발 경험이 갈린다

섹션 제목: “타입 생성 — 여기서 개발 경험이 갈린다”
터미널 창
# 로컬 DB 기준
supabase gen types typescript --local > lib/database.types.ts
# 원격 프로젝트 기준
supabase gen types typescript --project-id abcdefghijklmno > lib/database.types.ts
import type { Database } from './database.types'
const supabase = createClient<Database>(url, key)
const { data } = await supabase.from('posts').select('id, title')
// data: { id: number; title: string }[] | null ← 컬럼 이름 오타가 컴파일 에러가 된다

package.json 스크립트로 등록해 두고, 스키마를 바꿀 때마다 돌린다.

{
"scripts": {
"db:types": "supabase gen types typescript --local > lib/database.types.ts",
"db:reset": "supabase db reset && npm run db:types"
}
}
[api]
enabled = true
port = 54321
schemas = ["public", "graphql_public"] # API에 노출할 스키마
max_rows = 1000 # 한 번에 반환할 최대 행 수
[db]
port = 54322
major_version = 17
[auth]
site_url = "http://127.0.0.1:3000"
additional_redirect_urls = ["http://127.0.0.1:3000/auth/callback"]
jwt_expiry = 3600
enable_signup = true
[auth.email]
enable_confirmations = false # 로컬에서는 끄면 편하다
[auth.external.github]
enabled = true
client_id = "env(GITHUB_CLIENT_ID)"
secret = "env(GITHUB_SECRET)"

max_rows는 기억해 둘 만하다. 클라이언트가 무한정 데이터를 긁어가는 것을 막는 안전장치다.

db push와 config push도 구분한다.

  • supabase db push — supabase/migrations/의 DB 스키마 변경을 적용
  • supabase config push — config.toml의 지원되는 프로젝트 설정을 연결된 프로젝트에 적용

로컬 site_url = "http://127.0.0.1:3000"이 들어 있는 파일을 프로덕션에 그대로 config push하면 호스팅 Auth의 Site URL까지 localhost로 바뀔 수 있다. 첫 배포에서는 호스팅 Auth URL과 OAuth 제공자를 대시보드에서 명시적으로 설정하고, 설정을 코드로 배포하려면 환경별 값을 분리한 뒤 변경 내용을 검토한다.

마이그레이션 작업 흐름 — migration new부터 SQL 작성·로컬 검증·타입 갱신·PR을 거쳐 CI의 db push까지
  • 대시보드는 탐색과 디버깅용, 스키마 변경의 진실은 migrations/
  • supabase start → 프로덕션과 같은 스택이 로컬에 뜬다. Mailpit(54324)으로 메일까지 테스트
  • db reset이 통과해야 커밋한다
  • 타입 생성은 스크립트로 자동화해 둔다
  • 리전과 DB 비밀번호는 프로젝트 생성 시점의 되돌리기 어려운 결정이다