엣지: 분석, 엣지 설정, KV, AI 게이트웨이, 점진적 배포, 미들웨어
사이트 앞에서 도는 것: 개인정보 없는 웹 분석과 속도 측정, 배포 없이 바꾸는 값과 기능 플래그, 키-값 저장소, OpenAI 호환 AI 게이트웨이, 새 배포를 일부 방문자에게 먼저 보내기, middleware.ts.
엣지
사이트와 API 앞에서 도는 것들이에요. 모두 콘솔의 분석·엣지 화면, 관리 REST, 아래 도구로 다뤄요.
웹 분석과 속도 인사이트
get_analytics 방문·유입·국가·기기 합계
get_speed_insights Core Web Vitals 75번째 백분위
set_insights 켜기·끄기 (소유자·관리자)
get_analytics_breakdown 캠페인(UTM), 라우트, 사용자 정의 이벤트와 속성, 걸러낸 봇
set_analytics_previews 프리뷰 방문 세기 (소유자·관리자)
export_analytics 합계를 CSV로
list_analytics_reports 맞춤 리포트 목록
get_analytics_report 맞춤 리포트 하나
save_analytics_report 맞춤 리포트 만들기·바꾸기 (개발자 이상)
remove_analytics_report 맞춤 리포트 삭제켜면 사이트 HTML에 /_oneend/insights/script.js가 들어가요. 쿠키와 브라우저 저장소를 쓰지 않고, IP와 브라우저 정보는 저장하지 않으며 합계만 남겨요. 방문자는 하루 동안만 구분해요.
Vercel의 @vercel/analytics·@vercel/speed-insights를 쓰는 앱은 코드를 고치지 않아도 같은 곳에 집계돼요. 모든 사이트가 /_vercel/insights/script.js·/_vercel/speed-insights/script.js와 보고 경로를 사이트의 파일·rewrite보다 먼저 응답해요. 꺼져 있으면 스크립트는 아무것도 보내지 않아요. 사용자 정의 이벤트는 oneend.track('Signup', { plan: 'pro' })나 @vercel/analytics의 track()으로 보내고, 이름과 속성의 값별로 세요. 첫 페이지 주소의 utm_source·utm_medium·utm_campaign과 프레임워크의 라우트(/blog/[slug])도 세요. 사람이 아닌 방문(AI 크롤러, 검색 엔진, 그 밖의 프로그램)은 세지 않고 걸러낸 수만 남기며 계량하지 않아요. 프리뷰와 dpl- 주소의 방문은 set_analytics_previews로 켜기 전에는 세지 않아요.
수집 이벤트는 analytics_events로 사용량을 재요. 국가는 운영자가 지정한 헤더나 주소 범위 표에서만 읽어요. 둘 다 없는 서버에서는 국가를 추측하지 않고 ??(알 수 없음)로 세고, 결과의 note에 그렇게 적어요.
엣지 설정과 기능 플래그
get_edge_config 전체와 버전, 한도
set_edge_config 키 저장·삭제 (개발자 이상, 기록이 남음)
get_edge_config_history 누가 언제 무엇을 바꿨는지
list_edge_config_tokens 읽기 토큰과 연결된 프로젝트
create_edge_config_token 읽기 토큰 만들기 (관리자, 한 번만 보임, 바로 연결 가능)
connect_edge_config_token 다른 프로젝트의 배포에 변수로 연결
disconnect_edge_config_token 연결 끊기
revoke_edge_config_token 토큰 폐기
list_flags 플래그와 엣지 설정의 플래그를 함께, 어느 쪽이 정하는지
evaluate_flag 앱이 받을 값 (컨텍스트, 환경)
set_flag_enabled 한 환경에서 켜기·끄기
get_flag 플래그 하나의 변형, 환경마다의 설정과 규칙
create_flag 플래그 만들기 (모든 환경에서 꺼진 채로)
update_flag 설명과 변형 바꾸기 (관리자)
set_flag_environment 한 환경의 켜기, 꺼졌을 때 변형, 기본으로 내보낼 것(serve)
add_flag_rule 규칙 추가 (조건 1~3개, serve)
move_flag_rule 규칙 순서 바꾸기 (up | down)
remove_flag_rule 규칙 삭제
set_flag_archived 보관·되살리기 (관리자)
list_flag_segments 세그먼트 목록
create_flag_segment 세그먼트 만들기
update_flag_segment 세그먼트의 조건, 포함·제외 id (관리자)
remove_flag_segment 세그먼트 삭제 (관리자, 쓰는 규칙이 없을 때)
list_flag_sdk_keys /flags/v1/evaluate를 부르는 키
create_flag_sdk_key SDK 키 만들기 (server | client, 한 번만 보임)
revoke_flag_sdk_key SDK 키 폐기
get_remote_cache Turborepo 원격 캐시: 용량, 적중, 이번 달 사용량(remote_cache_gb_hour)
clear_remote_cache 원격 캐시 비우기 (개발자 이상)
list_remote_cache_tokens 원격 캐시 토큰과 turbo에 넣을 api, team
create_remote_cache_token 토큰 만들기 (한 번만 보임, 개발자 이상)
revoke_remote_cache_token 토큰 폐기
get_queue_metrics 큐 기록: 큐마다 대기·가장 오래 기다린 시간, 시간별 보낸·처리된 수몇 초 안에 모든 서버에 적용돼요. 함수와 미들웨어는 ctx.config.get(key), 다른 서버는 secret 키로 GET https://<ref>.<zone>/config/v1을 불러요.
기능 플래그는 값의 모양으로 정해요: true, {"enabled": true, "percent": 20}, {"enabled": true, "allow": ["user-1"]}. ctx.flags.isEnabled(key, userId)는 같은 사용자에게 언제나 같은 답을 줘요.
콘솔의 기능 플래그(규칙, 세그먼트, 환경, SDK 키)와 엣지 설정의 플래그는 한 번에 읽혀요. 순서: 같은 키의 플래그(보관되지 않은 것)가 정하고, 없으면 엣지 설정의 값이 정하고, 둘 다 없으면 찾을 수 없음(isEnabled는 false)이에요. ctx.flags, OneEnd.flags, SDK 키로 부르는 /flags/v1/evaluate, secret 키로 부르는 /config/v1/flags/<key>가 모두 이 순서를 따르고, 답의 source(flag, edge_config)가 어느 쪽이 정했는지 말해요. ctx.flags.evaluate(key, { user: { id } })는 플래그의 값과 변형을 돌려줘요.
같은 조직의 다른 프로젝트도 이 엣지 설정을 읽을 수 있어요. 읽기 토큰(bwec_…)을 만들어 그 프로젝트에 연결하면, 그 프로젝트의 다음 배포부터 GLOBAL_CONFIG(이름은 고를 수 있음)에 연결 문자열 https://<ref>.<zone>/config/v1?token=…이 들어가요. 토큰은 읽기만 하고, 폐기하면 바로 막혀요. 프로덕션에 연결하는 일은 그 프로젝트의 소유자·관리자만 해요.
점진적 배포
get_rolling_releases 설정과 진행 중인 배포
set_rolling_releases 단계 비율, 길이, 자동 진행, 자동 중단 기준
advance_rolling_release 다음 단계로
promote_rolling_release 모두에게 바로
abort_rolling_release 중단하고 이전 배포로켜 두면 새 프로덕션 배포가 단계 비율(예: 10%, 50%)만큼만 방문자를 받아요. 방문자는 __oneend_rr 쿠키로 한쪽에 고정돼요.
새 배포의 5xx 비율이 기준을 넘으면 게이트웨이가 스스로 중단하고 이전 배포로 되돌려요. 이전 배포로 rollback해도 중단돼요.
KV 저장소
list_kv_keys 키 목록 (비밀을 볼 수 있는 역할)
get_kv 값 하나
set_kv 저장 (ttl 초)
delete_kv 삭제함수에서는 ctx.kv.get/set/del/incr/expire/ttl/list를 써요. REST는 secret 키로 https://<ref>.<zone>/kv/v1이에요. Upstash 형식이라 @upstash/redis를 그대로 쓸 수 있어요:
import { Redis } from '@upstash/redis'
const redis = new Redis({ url: 'https://<ref>.<zone>/kv/v1', token: process.env.ONEEND_SECRET_KEY })
await redis.set('session:abc', 'payload', { ex: 60 })요청은 kv_operations로, 저장한 바이트는 데이터베이스 용량으로 재요. 값 하나와 전체 크기는 요금제가 정해요.
AI 게이트웨이
get_ai_gateway 주소, 설정, 공급자, 30일 사용량
set_ai_gateway 켜기, 사용자 허용, 캐시, 분당 한도, 플랫폼 대체
add_ai_provider 공급자 추가 (키는 암호화해 저장)
set_ai_provider_enabled 공급자 끄기·켜기
remove_ai_provider 공급자 삭제
get_ai_gateway_logs 최근 호출
set_ai_budgets 프로젝트 예산 (달러, 하루·주·달)
list_ai_gateway_keys 게이트웨이 키 목록
create_ai_gateway_key 앱 하나에 줄 키 (예산과 함께, 한 번만 보임)
set_ai_gateway_key_budgets 키의 예산 바꾸기
set_ai_gateway_key_limits 키의 분당 요청 수·토큰 수 한도
set_ai_model_aliases 모델 별칭 (blackall-air -> 공급자의 모델, 시스템 지침과 함께)
set_ai_gateway_hooks 키로 부른 호출 앞에 묻고 뒤에 알리는 훅 (서명)
revoke_ai_gateway_key 키 폐기
get_ai_spend 지출: 오늘·주·달, 하루별·모델별·키별import OpenAI from 'openai'
const ai = new OpenAI({ baseURL: 'https://<ref>.<zone>/ai/v1', apiKey: process.env.ONEEND_SECRET_KEY })
await ai.chat.completions.create({ model: 'openai/gpt-4o-mini', messages: [{ role: 'user', content: 'hi' }] })OpenAI 클라이언트를 이 주소와 secret 키로 써요. provider/model은 그 공급자만, 그냥 model은 우선순위대로 부르고 실패(연결 실패, 429, 5xx)하면 다음으로 넘어가요.
공급자 주소는 공개된 https만 돼요. 사설·내부·메타데이터 주소와 리다이렉트는 거부해요. 프로젝트의 키는 공급자에게 보내지 않아요.
요청은 ai_gateway_requests로, 플랫폼 공급자(DeepSeek)의 토큰은 ai_gateway_tokens로 재요. 자기 키로 부른 토큰은 그 공급자가 청구해요.
공급자 종류는 openai, anthropic, google, deepseek, openai-compatible이에요. Anthropic은 Messages API로 서로 바꿔 부르고(스트림과 도구 호출 포함), Google은 Gemini의 OpenAI 호환 주소로 불러요.
앱에는 프로젝트의 secret 키 대신 게이트웨이 키(bwai_…)를 줄 수 있어요. 키마다, 그리고 프로젝트 전체에 하루·주·달 예산(달러, UTC)을 두면 다 쓴 뒤의 호출은 공급자에게 보내기 전에 429 budget_exceeded로 거절해요. 호출마다 비용(x-oneend-cost-usd, 기록의 cost_usd)과 첫 토큰까지의 시간(ttft_ms)을 남겨요. 자기 키로 부른 비용은 공급자 정가로 계산한 추정이에요.
그림과 음성: 같은 주소에서 /ai/v1/images/generations(그림), /ai/v1/audio/speech(음성 읽기), /ai/v1/audio/transcriptions(받아쓰기, multipart 파일)도 받아요. OpenAI SDK의 images.generate, audio.speech.create, audio.transcriptions.create가 그대로 돼요. 플랫폼 공급자는 그림과 음성을 만들지 않으니, OpenAI나 OpenAI 호환 GPU 주소(RunPod·Together 같은 곳)를 add_ai_provider로 붙여요. 사용자별 한도와 기록은 대화와 같아요.
앱 사용자별 한도: set_ai_gateway의 user_daily_requests · user_daily_tokens로 앱 사용자 한 명이 하루(한국 시간 0시 기준)에 쓸 요청·토큰을 정해요. 넘으면 429 user_limit으로 답해요. 사용자는 로그인한 사용자의 id이고, 서버에서 secret 키로 부를 때는 x-oneend-user 헤더(또는 본문의 user)에 그 사람의 id를 넣어요. 자기 키로 부른 토큰도 사용자별로 세요. 많이 쓴 사용자는 get_ai_gateway의 top_users_today · top_users_30d에 나와요.
미들웨어
get_middleware 실행 여부와 시간 한도
set_middleware 끄기·켜기, 시간 한도 (소유자·관리자)import { NextResponse } from 'next/server'
export function middleware(request) {
if (!request.cookies.get('session')) return NextResponse.redirect(new URL('/login', request.url))
return NextResponse.next({ headers: { 'x-frame-options': 'DENY' } })
}
export const config = { matcher: ['/app/:path*'] }Next.js의 middleware.ts(또는 proxy.ts)는 빌드할 때 묶여 요청마다 먼저 실행돼요. 다른 프레임워크는 blackwall.json에 "middleware": "middleware.ts"로 지정해요.
함수와 같은 격리 환경에서 실행하고, next/server의 NextResponse(next, rewrite, redirect, json, cookies)를 써요. rewrite는 같은 사이트의 경로로만 돼요. 실패하거나 시간을 넘기면 페이지를 보여 주지 않아요.
미들웨어 안에서는 @vercel/edge-config의 get()이나 OneEnd.config.get()으로 엣지 설정을, OneEnd.kv로 KV를 읽어요.
실행 횟수는 middleware_invocations로, 켜져 있는 동안의 메모리는 runtime_gb_hour로 재요.
REST
관리 REST:
GET /v1/projects/<ref>/analytics?days=7
GET /v1/projects/<ref>/analytics/breakdown?days=7&event=Signup
PUT /v1/projects/<ref>/analytics/previews
GET /v1/projects/<ref>/analytics/export?days=30
GET /v1/projects/<ref>/analytics/reports
POST /v1/projects/<ref>/analytics/reports
GET /v1/projects/<ref>/analytics/reports/<id>
PATCH /v1/projects/<ref>/analytics/reports/<id>
DELETE /v1/projects/<ref>/analytics/reports/<id>
GET /v1/projects/<ref>/speed-insights?days=7&device=mobile
PUT /v1/projects/<ref>/insights
GET /v1/projects/<ref>/edge-config
GET /v1/projects/<ref>/flags
GET /v1/projects/<ref>/flags/evaluate?key=promo&environment=production&context={...}
PUT /v1/projects/<ref>/flags/<key>/environments/<env>
GET /v1/projects/<ref>/remote-cache
DELETE /v1/projects/<ref>/remote-cache/artifacts
GET /v1/projects/<ref>/queues/metrics?hours=24
PATCH /v1/projects/<ref>/edge-config
GET /v1/projects/<ref>/edge-config/history
GET /v1/projects/<ref>/edge-config/tokens
POST /v1/projects/<ref>/edge-config/tokens
DELETE /v1/projects/<ref>/edge-config/tokens/<id>
PUT /v1/projects/<ref>/edge-config/tokens/<id>/connections/<target>
DELETE /v1/projects/<ref>/edge-config/tokens/<id>/connections/<target>
GET /v1/projects/<ref>/rolling-releases
PUT /v1/projects/<ref>/rolling-releases
POST /v1/projects/<ref>/rolling-releases/advance | promote | abort
GET /v1/projects/<ref>/kv?prefix=session:
GET /v1/projects/<ref>/kv/<key>
PUT /v1/projects/<ref>/kv/<key>
DELETE /v1/projects/<ref>/kv/<key>
GET /v1/projects/<ref>/ai-gateway
PUT /v1/projects/<ref>/ai-gateway
GET /v1/projects/<ref>/ai-gateway/logs
POST /v1/projects/<ref>/ai-gateway/providers
PATCH /v1/projects/<ref>/ai-gateway/providers/<id>
DELETE /v1/projects/<ref>/ai-gateway/providers/<id>
PUT /v1/projects/<ref>/ai-gateway/budgets
GET /v1/projects/<ref>/ai-gateway/keys
POST /v1/projects/<ref>/ai-gateway/keys
PATCH /v1/projects/<ref>/ai-gateway/keys/<id>
DELETE /v1/projects/<ref>/ai-gateway/keys/<id>
PUT /v1/projects/<ref>/ai-gateway/keys/<id>/limits
PUT /v1/projects/<ref>/ai-gateway/aliases
PUT /v1/projects/<ref>/ai-gateway/hooks
GET /v1/projects/<ref>/ai-gateway/spend?days=30
GET /v1/projects/<ref>/middleware
PUT /v1/projects/<ref>/middleware