가이드

브라우저에 넣어도 되는 API 키, 안 되는 키

Onttle v1.0부터 API 키는 읽기 전용 게시용 키(pk-proj_)와 서버 전용 시크릿 키(sk-proj_)로 나뉩니다. Next.js에서 목록은 브라우저가, 업로드는 Route Handler가 맡도록 나눠 쓰는 법과 401·403을 읽는 법을 정리했습니다.

Next.js에서 외부 API를 붙이다 보면 한 번쯤 이 고민을 합니다. 클라이언트 컴포넌트에서 바로 fetch를 하고 싶은데, 키가 process.env에서 undefined로 나옵니다. 앞에 NEXT_PUBLIC_을 붙이면 해결됩니다. 그리고 그 순간 그 키는 누구나 볼 수 있는 값이 됩니다.
Next.js는 NEXT_PUBLIC_으로 시작하는 환경 변수를 빌드 시점에 클라이언트 번들에 그대로 넣습니다(Next.js 문서).
Onttle v1.0을 정식 출시하면서 API 키를 두 종류로 나눈 것도 이 장면 때문입니다. [확인 필요: 키를 나누기로 한 실제 계기나 고민 한 줄 — 예: 베타 때 키 하나로 운영하며 걱정했던 점] 이 글에서는 두 키가 어떻게 다른지, Next.js 앱에서 어디에 무엇을 두는지 코드로 정리합니다.
키는 두 종류입니다
2026년 10월 30일 v1.0.0부터 API 키를 만들 때 유형을 고릅니다.

게시용 키 pk-proj_…

시크릿 키 sk-proj_…

GET (조회)

O

O

POST·PUT·PATCH·DELETE

X

O

파일 업로드

X

O

Chat 메시지·임베딩 검색

X

O

쓰는 곳

브라우저, 공개 사이트 (허용 호스트와 함께)

서버에서만

두 가지를 기억해 두면 좋습니다. 첫째, 키 유형은 만든 뒤에 바꿀 수 없습니다. 유형을 잘못 골랐다면 새 키를 발급해야 합니다. 둘째, 전체 키는 생성 직후 한 번만 보입니다. Onttle은 해시만 저장하고, 대시보드에는 sk-proj_abcd…wxyz처럼 접두사와 마지막 4자리, 키별 마지막 사용 시각만 표시합니다(인증 문서).

환경 변수부터 나눕니다
# .env.local
NEXT_PUBLIC_ONTTLE_PROJECT_ID=YOUR_PROJECT_ID
NEXT_PUBLIC_ONTTLE_PUBLISHABLE_KEY=pk-proj_YOUR_PUBLISHABLE_KEY

# NEXT_PUBLIC_ 없이: 서버 코드에서만 읽힘
ONTTLE_SECRET_KEY=sk-proj_YOUR_SECRET_KEY
규칙은 하나입니다. NEXT_PUBLIC_ 뒤에는 pk-proj_만 옵니다.
브라우저에서: 게시용 키로 목록 읽기
글 목록처럼 공개해도 되는 데이터를 클라이언트에서 직접 불러올 때는 게시용 키를 씁니다.
// app/posts/post-list.tsx
'use client'

import { useEffect, useState } from 'react'

const PROJECT_ID = process.env.NEXT_PUBLIC_ONTTLE_PROJECT_ID
const PUBLISHABLE_KEY = process.env.NEXT_PUBLIC_ONTTLE_PUBLISHABLE_KEY

type Post = { id: string; title: string } // 필드는 테이블 스키마에 맞게

export function PostList() {
const [posts, setPosts] = useState<Post[]>([])

useEffect(() => {
fetch(
`https://api.onttle.com/v1/${PROJECT_ID}/tables/posts/contents?page=1&size=20`,
{ headers: { Authorization: `Bearer ${PUBLISHABLE_KEY}` } },
)
.then((res) => res.json())
.then((data) => setPosts(data.list))
}, [])

return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
응답은 list와 pagination(page, size, total)으로 옵니다(페이지네이션 문서). REST API로는 발행된 콘텐츠만 조회되니, 초안이 노출될 걱정은 하지 않아도 됩니다.

게시용 키를 만들 때 허용 호스트에 서비스 도메인을 넣어 두면, 그 도메인에서 온 요청만 받습니다. 다만 게시용 키는 번들에 그대로 실리는 공개 값입니다. 허용 호스트는 다른 사이트에서 내 키를 가져다 쓰는 걸 막는 장치로 보고, 키로 읽을 수 있는 데이터는 처음부터 공개해도 되는 것만 두세요.

참고로 서버 컴포넌트에서 렌더링하는 목록이라면 굳이 브라우저에서 부를 필요가 없습니다. 서버에서 fetch에 next: { revalidate: 60 }을 주는 편이 키도 노출되지 않고 캐시도 됩니다. 게시용 키는 "브라우저에서 꼭 불러야 할 때"를 위한 키입니다.
서버에서: 시크릿 키로 업로드하기
커버 이미지 업로드는 쓰기 작업이라 시크릿 키가 필요합니다. 브라우저는 내 Route Handler로 파일을 보내고, Route Handler가 시크릿 키를 붙여 Onttle로 전달합니다.
// app/api/upload/route.ts
export async function POST(request: Request) {
// 1. 먼저 로그인·권한 확인 (이 줄이 없으면 누구나 내 키로 업로드할 수 있습니다)
// const session = await auth(); if (!session) return new Response(null, { status: 401 })

const incoming = await request.formData()
const file = incoming.get('file')
if (!(file instanceof File)) {
return Response.json({ message: 'file 필드가 필요합니다.' }, { status: 400 })
}

const formData = new FormData()
formData.append('file', file)
formData.append('isOcr', 'false')

const res = await fetch(
`https://api.onttle.com/v1/${process.env.NEXT_PUBLIC_ONTTLE_PROJECT_ID}/files`,
{
method: 'POST',
headers: { Authorization: `Bearer ${process.env.ONTTLE_SECRET_KEY}` },
body: formData,
},
)

if (!res.ok) {
return Response.json({ message: `Upload failed: ${res.status}` }, { status: res.status })
}

const data = await res.json()
return Response.json({ id: data.id, url: data.presigned_url })
}
응답의 id(uuid)를 콘텐츠의 이미지 필드(예: thumbnail)에 넣으면 글과 연결됩니다(파일 업로드 문서). [확인 필요: presigned_url의 만료 여부 — 만료된다면 화면 표시용 URL을 어떻게 얻는지]
가장 중요한 줄은 1번 주석입니다. 아무나 호출할 수 있는 업로드 라우트는 시크릿 키를 공개한 것과 결과가 거의 같습니다.
401과 403은 다른 문제입니다
키를 나눈 뒤 가장 자주 보게 될 에러는 두 가지입니다(에러 코드 문서).
401 Unauthorized: 키 자체가 문제입니다. Authorization 헤더가 없거나, Bearer 접두사 없이 키만 넣었거나, 키가 틀렸거나 삭제된 경우입니다.

403 Forbidden: 키는 맞는데 권한이 없습니다. 게시용 키로 쓰기·업로드·Chat·임베딩을 부른 경우, 허용되지 않은 호스트에서 부른 경우, 다른 프로젝트 데이터에 접근한 경우입니다.

게시용 키로 업로드를 부르면 이런 응답이 옵니다.
{
"type": "forbidden",
"title": "Forbidden",
"message": "This endpoint requires a secret API key (sk-proj_…). Publishable keys (pk-proj_…) are read-only."
}
마무리
정리하면 이렇습니다. NEXT_PUBLIC_ 뒤에는 pk-proj_만, 쓰기·업로드·AI 기능은 서버의 sk-proj_로, 서버 라우트에는 인증을 먼저 붙입니다.
여러분은 프론트엔드에서 쓰는 API 키를 어떻게 나눠 관리하고 계신가요? 다음 글에서는 시크릿 키가 필요한 ask/chat과 임베딩 검색을 서버 라우트 하나로 감싸 문서 검색창을 만들어 보겠습니다.
Onttle에서 프로젝트를 만들고 키 두 개를 발급해 보세요.