Orbit
Edge Functions
Edge functions are small JavaScript handlers that run on Kapsule's edge network before a request reaches your project, so you can do redirects, header injection, A/B routing and bot filtering without a round trip to your app. This guide covers writing one, the entry-point forms accepted, path matching, how multiple functions interact, deploying, and what happens when a function throws.
Where They Live
Open your project in Orbit and click the Edge functions tab. Every function belongs to a project and is listed with its status badge: LIVE, PAUSED, NOT DEPLOYED or DEPLOY FAILED.

Creating a Function
- Click New function.
- Fill in Name (up to 120 characters).
- Fill in Path pattern, the URL pattern this function should run on.
- Choose a Trigger: Request (before origin), Response (after origin), or Both.
- Choose an Environment scope: All environments, Production only, or Preview only.
- Write the handler in Function body.
- Click Save, then Deploy to edge.
The request object is always in scope, and the source is capped at 64 KB.
Writing a Handler
Return a Response to answer the request at the edge. Return nothing, or undefined, to pass the request through to your project unchanged.
export default {
async fetch(request) {
const url = new URL(request.url)
// Redirect /old to /new
if (url.pathname === '/old') {
return new Response(null, {
status: 301,
headers: { Location: '/new' },
})
}
// Returning nothing passes through to the origin
},
}
The named-function form works too:
export default async function handler(request) {
// Add a security header to every response
const response = await fetch(request)
const headers = new Headers(response.headers)
headers.set('X-Frame-Options', 'DENY')
return new Response(response.body, { status: response.status, headers })
}
Supported Entry-Point Forms
| Form | Example |
|---|---|
Object with a fetch method | export default { async fetch(request) { ... } } |
Object with an arrow fetch property | export default { fetch: async (request) => { ... } } |
| Named function declaration | export default async function handler(request) { ... } |
| Bare body, no export | Write the statements directly, with no wrapper |
Anything else that uses export default, such as export default class, is rejected when you click Save, with an error naming the two forms to use instead. Validation runs at save time rather than at deploy time, so you find out immediately and a broken function is never published.
Path Patterns
The Path pattern decides which requests run the function. It must start with / and can be up to 2048 characters. * matches any run of characters and ? matches a single character. A pattern with no wildcard matches that exact path and everything beneath it.
| Pattern | Matches |
|---|---|
/* | Every path |
/api/* | Anything starting with /api/ |
/blog/*/comments | For example /blog/my-post/comments |
/page | /page, and anything under /page/ |
The request Object
request is a standard Fetch API Request. You can read the URL, method, headers and body:
export default {
async fetch(request) {
const url = new URL(request.url)
const cookie = request.headers.get('cookie') ?? ''
const ua = request.headers.get('user-agent') ?? ''
if (ua.includes('BadBot')) {
return new Response('Forbidden', { status: 403 })
}
},
}
Do not assume a header exists because you have seen it on another platform. Read the headers your own function actually receives (log them from the function, or return them in a debug response on a throwaway path) before you branch on one. A handler that branches on a header that is never present silently takes the wrong path on every request.
Common Patterns
Redirect Old URLs
export default {
async fetch(request) {
const url = new URL(request.url)
const redirects = {
'/old-about': '/about',
'/old-contact': '/contact',
}
const dest = redirects[url.pathname]
if (dest) return Response.redirect(url.origin + dest, 301)
},
}
For a handful of straightforward path-to-path moves, use the built-in redirect engine instead: it needs no code and is configured in Settings. See Configuring Redirects and Rewrites.
Add Security Headers
export default async function handler(request) {
const response = await fetch(request)
const headers = new Headers(response.headers)
headers.set('X-Frame-Options', 'DENY')
headers.set('X-Content-Type-Options', 'nosniff')
headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
})
}
For static header rules you do not need code either. Settings has a response headers section with quick-add presets for HSTS, CSP, no-embed, no-sniff, referrer policy and CORS.
A/B Routing
export default {
async fetch(request) {
const url = new URL(request.url)
const variant = Math.random() < 0.5 ? 'a' : 'b'
url.searchParams.set('variant', variant)
return fetch(url.toString(), request)
},
}
Environment Scope
| Scope | Runs on |
|---|---|
| All environments | Production, staging and every branch preview |
| Production only | The production environment |
| Preview only | Every non-production environment |
Ship a new function as Preview only first, confirm it behaves on a branch preview, then switch it to All environments. An edge function runs in front of every request, so a mistake in one is a mistake on every page at once.
How Multiple Functions Interact
All of your project's enabled functions are evaluated in the order they were created. For each request, the edge walks the list and runs the first function whose path pattern matches the request path and whose scope covers the environment.
- If that function returns a
Response, it is sent and evaluation stops. - If it returns nothing, evaluation continues to the next matching function.
- If none of them returns a
Response, the request goes to your project as normal.
This means a broad /* function created early can shadow a narrower one created later, if the broad one returns a Response. Create the specific ones first, or make the broad one return nothing for paths it should not handle.
Deploying
Click Deploy to edge. The deploy regenerates a single combined router for the whole edge network from the current state of the database, so enabling, disabling, editing or deleting any function republishes everything. Changes usually take effect within a few seconds.
Each function row keeps a Deploy log showing the steps of the last deploy, and a DEPLOY FAILED badge with the error if it did not succeed.
Pause takes a function out of the router without deleting it, which is the quickest way to back out a misbehaving function. Resume puts it back.
When a Function Throws
An exception inside a function is caught at the edge. The error is logged and the request falls through to your project as if the function had returned nothing.
This is a safety net, not a monitoring system. A function that throws on every request fails silently from your visitors' point of view, and your traffic simply behaves as though the function does not exist. If a function stops having its intended effect, suspect an exception before you suspect the routing.
Limits
- Up to 20 functions per project. The 21st is rejected.
- Up to 64 KB of source per function.
- Up to 120 characters for the name, and 2048 characters for the path pattern.
Related Reading
- Configuring Redirects and Rewrites, the no-code option for path rules
- Deploying Your Project
- Branch Preview Deployments in Orbit for testing a function before it reaches production