# 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
```

---

https://end.blackall.ai/docs/edge?lang=en
