Edge: analytics, edge config, KV, AI gateway, rolling releases, middleware
What runs in front of the site: web analytics and speed measurement with no personal data, values and feature flags that change without a deploy, a key-value store, an OpenAI-compatible AI gateway, sending a new deployment to some visitors first, and middleware.ts.
Edge
What runs in front of the site and the API. Everything here is in the console's Analytics and Edge screens, the management REST API and the tools below.
Web Analytics and Speed Insights
get_analytics Totals of visits, referrers, countries, devices
get_speed_insights Core Web Vitals at the 75th percentile
set_insights On or off (owners and admins)
get_analytics_breakdown Campaigns (UTM), routes, custom events and properties, bots filtered
set_analytics_previews Count preview visits (owners and admins)
export_analytics Totals as CSV
list_analytics_reports Custom reports
get_analytics_report One custom report
save_analytics_report Create or change a custom report (developers and above)
remove_analytics_report Delete a custom reportWhen on, the site's HTML gets /_oneend/insights/script.js. It uses no cookies and no browser storage, keeps no address or browser details, only totals, and tells visitors apart for one day only.
Apps that use Vercel's @vercel/analytics and @vercel/speed-insights are counted in the same place with no code change: every site answers /_vercel/insights/script.js, /_vercel/speed-insights/script.js and the paths they report to, ahead of the site's own files and rewrites. While this is off, those scripts send nothing. Custom events are sent with oneend.track('Signup', { plan: 'pro' }) or track() from @vercel/analytics, and counted by name and by each property's values. The first page's utm_source, utm_medium and utm_campaign and the framework's route (/blog/[slug]) are counted too. Visits that are not people (AI crawlers, search engines, other programs) are not counted; only how many were filtered is kept, and they are not metered. Visits to previews and dpl- addresses are not counted until set_analytics_previews turns that on.
Events are metered as analytics_events. Countries are read only from a header or address range table the operator names. On a server with neither, no country is guessed: visits are counted as ?? and the report carries a note saying so.
Edge config and feature flags
get_edge_config All of it, its version and limits
set_edge_config Set or delete keys (developers and above; kept in the history)
get_edge_config_history Who changed what, and when
list_edge_config_tokens Read tokens and the projects they are connected to
create_edge_config_token Create a read token (admins, shown once, can connect right away)
connect_edge_config_token Connect to another project's deployments as a variable
disconnect_edge_config_token Disconnect
revoke_edge_config_token Revoke a token
list_flags Flags and edge config flags together, and which decides
evaluate_flag What the app would get (context, environment)
set_flag_enabled On or off in one environment
get_flag One flag's variants, and each environment's settings and rules
create_flag Create a flag (off in every environment)
update_flag Change the description and variants (admins)
set_flag_environment One environment's switch, off variant and default serve
add_flag_rule Add a rule (1 to 3 conditions, serve)
move_flag_rule Reorder a rule (up | down)
remove_flag_rule Remove a rule
set_flag_archived Archive or restore (admins)
list_flag_segments Segments
create_flag_segment Create a segment
update_flag_segment A segment's conditions and included or excluded ids (admins)
remove_flag_segment Remove a segment (admins, when no rule uses it)
list_flag_sdk_keys Keys that call /flags/v1/evaluate
create_flag_sdk_key Create an SDK key (server | client, shown once)
revoke_flag_sdk_key Revoke an SDK key
get_remote_cache Turborepo remote cache: size, hits, this month's metered storage (remote_cache_gb_hour)
clear_remote_cache Clear the remote cache (developers and above)
list_remote_cache_tokens Remote cache tokens and the api and team to give turbo
create_remote_cache_token Create a token (shown once, developers and above)
revoke_remote_cache_token Revoke a token
get_queue_metrics Queue history: depth and oldest wait per queue, sent and processed over timeEvery server has a change within a few seconds. Functions and middleware read ctx.config.get(key); other servers call GET https://<ref>.<zone>/config/v1 with the secret key.
A feature flag is decided by the shape of its value: true, {"enabled": true, "percent": 20}, {"enabled": true, "allow": ["user-1"]}. ctx.flags.isEnabled(key, userId) always gives the same user the same answer.
The console's feature flags (rules, segments, environments, SDK keys) and the edge config's flags are read in one call. The order: a flag of the same key (not archived) decides; otherwise the edge config value decides; with neither, the flag is not found (isEnabled is false). ctx.flags, OneEnd.flags, /flags/v1/evaluate with an SDK key and /config/v1/flags/<key> with the secret key all follow this order, and the answer's source (flag, edge_config) says which decided. ctx.flags.evaluate(key, { user: { id } }) returns the flag's value and variant.
Other projects in the organization can read this edge config too. Create a read token (bwec_…) and connect it to a project: from that project's next deployment, GLOBAL_CONFIG (the name is yours to choose) holds the connection string https://<ref>.<zone>/config/v1?token=…. A token only reads, and stops at once when revoked. Connecting into production takes that project's owners or admins.
Rolling releases
get_rolling_releases Settings and the release in progress
set_rolling_releases Stage shares, length, auto advance, auto stop threshold
advance_rolling_release To the next stage
promote_rolling_release To everybody now
abort_rolling_release Stop and go backWhen on, a new production deployment gets only its stage's share of visitors (10%, then 50%, for example). Each visitor is kept on one side by the __oneend_rr cookie.
When the new deployment's 5xx rate is over the threshold, the gateway stops the release itself and serves the previous deployment. Rolling back to the previous deployment stops it too.
KV store
list_kv_keys Keys (roles that may see secrets)
get_kv One value
set_kv Save (ttl in seconds)
delete_kv DeleteFunctions use ctx.kv.get/set/del/incr/expire/ttl/list. Over REST it is https://<ref>.<zone>/kv/v1 with the secret key, in Upstash's format, so @upstash/redis works as it is:
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 })Requests are metered as kv_operations and stored bytes as database storage. The plan sets the size of one value and of everything.
AI gateway
get_ai_gateway Address, settings, providers, 30 days of use
set_ai_gateway On, user calls, cache, rate per minute, platform fallback
add_ai_provider Add a provider (the key is sealed)
set_ai_provider_enabled Turn a provider off or on
remove_ai_provider Remove a provider
get_ai_gateway_logs Recent calls
set_ai_budgets The project's budgets (dollars, day, week, month)
list_ai_gateway_keys Gateway keys
create_ai_gateway_key A key for one app (with budgets, shown once)
set_ai_gateway_key_budgets Change a key's budgets
set_ai_gateway_key_limits A key's requests and tokens per minute
set_ai_model_aliases Model aliases (blackall-air -> a provider's model, with a system prompt)
set_ai_gateway_hooks Hooks asked before and told after a key's call (signed)
revoke_ai_gateway_key Revoke a key
get_ai_spend Spend: today, week, month, per day, model and keyimport 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' }] })Point an OpenAI client at this address with the secret key. provider/model calls that provider only; a bare model goes by priority and moves on after a failure (no connection, 429, 5xx).
Provider addresses must be public https. Private, internal and metadata addresses and redirects are refused. The project's own key is never sent to a provider.
Requests are metered as ai_gateway_requests, and the platform provider's (DeepSeek) tokens as ai_gateway_tokens. Tokens on your own key are billed by that provider.
Provider kinds are openai, anthropic, google, deepseek and openai-compatible. Anthropic is called through its Messages API, translated both ways (streams and tool calls included); Google through Gemini's OpenAI-compatible address.
An app can be given a gateway key (bwai_…) instead of the project's secret key. With day, week and month budgets (dollars, UTC) on a key and on the whole project, a call past one is refused with 429 budget_exceeded before it reaches a provider. Every call records its cost (x-oneend-cost-usd, cost_usd in the log) and the time to its first token (ttft_ms). Calls on your own key are estimated at the provider's list price.
Pictures and audio: the same address also takes /ai/v1/images/generations (pictures), /ai/v1/audio/speech (text read aloud) and /ai/v1/audio/transcriptions (dictation, a multipart file). The OpenAI SDK's images.generate, audio.speech.create and audio.transcriptions.create work unchanged. The platform provider makes no pictures or audio, so add OpenAI or an OpenAI-compatible GPU address (RunPod, Together and the like) with add_ai_provider. Per-user limits and logs work as for chat.
Limits per app user: user_daily_requests and user_daily_tokens in set_ai_gateway set what one user of your app may use in a day (midnight Korean time). Past it the gateway answers 429 user_limit. The user is the signed-in user's id; when your server calls with the secret key, put the person's id in the x-oneend-user header (or the body's user). Tokens on your own key count per user too. The heaviest users are in get_ai_gateway's top_users_today and top_users_30d.
Middleware
get_middleware Whether it runs, and the time limit
set_middleware Off or on, and the time limit (owners and admins)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's middleware.ts (or proxy.ts) is bundled at build time and runs first on each request. Other frameworks name theirs in blackwall.json as "middleware": "middleware.ts".
It runs in the same sandbox as functions, with NextResponse from next/server (next, rewrite, redirect, json, cookies). A rewrite can only go to a path on the same site. If it fails or runs over time, the page is not shown.
Inside middleware, @vercel/edge-config's get() or OneEnd.config.get() reads the edge config, and OneEnd.kv the KV store.
Runs are metered as middleware_invocations, and its memory while warm as runtime_gb_hour.
REST
Management 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