One Server

Docs

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 report

When 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 time

Every 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 back

When 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      Delete

Functions 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 key
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' }] })

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