One Server

문서

엣지: 분석, 엣지 설정, 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