# Easel API The public Easel REST API is under development and is not yet a supported automation surface. Use the dashboard, GitHub integration, and [Easel CLI](/docs/cli) for current workflows. Planned scope [#planned-scope] The public API is expected to cover authentication, projects, deployments, environment configuration, domains, logs, and cache operations. Endpoint availability and stability will be documented here as each resource becomes supported. Do not build production automation against undocumented dashboard endpoints. Current automation [#current-automation] * Use [Git deployments](/docs/deployments/git) for repository-driven delivery. * Use the [Easel CLI](/docs/cli) for supported local and CI workflows. * Contact [hello@easel.sh](mailto:hello@easel.sh) when a workflow requires an API capability that is not yet public. # Common workflows These workflows map typical tasks to the guides that cover them on Easel. They are documentation walkthroughs, not a gallery of deployable example repositories. Deploy a web application [#deploy-a-web-application] Connect a Git repository, let Easel detect your framework, and ship preview URLs on every branch. * [Getting started](/docs/getting-started) * [Deployment URLs](/docs/deployments/urls) * [Frameworks](/docs/framework-guides) Preview and production [#preview-and-production] Use separate environments so previews stay isolated from production traffic. * [Environments](/docs/deployments/environments) * [Production releases](/docs/deployments/production-releases) * [Promoting deployments](/docs/deployments/promote) Protect traffic [#protect-traffic] Add firewall rules and site-wide Attack Mode without a separate security vendor. * [WAF rules](/docs/security/waf-rules) * [Custom firewall rules](/docs/security/custom-rules) * [Attack Mode](/docs/security/attack-mode) * [Access protection](/docs/security/deployment-protection) Automate from CI [#automate-from-ci] * [CI/CD](/docs/guides/ci-cd) * [CLI](/docs/cli) ```bash npm install -g @easel-sh/cli easel login easel link --project my-app easel list --prod easel logs --follow --expand ``` More resources [#more-resources] * [Debugging](/docs/guides/debugging) * [Troubleshooting](/docs/troubleshooting) * [Changelog](/changelog) # Getting started This guide walks through the complete first-deployment workflow. You will: 1. Import a GitHub repository. 2. Review the detected project configuration. 3. Create the first deployment. 4. Open the application and inspect a request. 5. Create a Preview deployment from a branch. 6. Release the change to Production. Before you begin [#before-you-begin] You need: * An Easel account ([book a call](https://cal.com/sean-easel/lets-look-at-your-infrastructure-bill) if you are not on the platform yet) * A GitHub repository * Permission to install or authorize the Easel GitHub App * A [supported framework](/docs/framework-guides) * Any environment variables required by the application If you do not have an application ready, start from a small framework starter or an Easel fixture under [`github.com/easel-sh/easel-sh/tree/main/fixtures`](https://github.com/easel-sh/easel-sh/tree/main/fixtures). 1. Import a repository [#1-import-a-repository] 1. Sign in at [app.easel.sh](https://app.easel.sh). 2. Create or select a workspace. 3. Choose **Create Project**. 4. Under **Import Git Repository**, continue with GitHub and authorize the GitHub App if prompted. 5. Select the repository and continue to **New Project**. Easel reads the repository and attempts to detect: * Framework * Project root * Install command * Build command * Output directory * Production branch (default `main`) The GitHub App requests permissions needed for repository contents, commit statuses, deployments, pull-request discovery, and PR preview comments. See [Git deployments](/docs/deployments/git). 2. Review project configuration [#2-review-project-configuration] Before deploying, confirm the detected settings on the **New Project** screen. Framework [#framework] Make sure Easel selected the correct framework. The framework controls build behavior, route discovery, function bundling, static output, and related defaults. See the matching [framework guide](/docs/framework-guides). Root directory [#root-directory] For a monorepo, select the directory containing the application. Example: ```text apps/web ``` Build settings [#build-settings] Review: ```text Install command Build command Output directory ``` Use the framework defaults unless the repository requires custom settings. Production branch [#production-branch] Choose the branch that should create Production deployments. A common choice is `main`. Pushes to other branches create Preview deployments. You can change this later in **Project settings → Environments**. Environment variables [#environment-variables] Add variables required by the build or application before deploying. Use different values for Preview and Production when the application should connect to separate services. Configure them in **Project settings → Environment Variables**. See [Deployment environments](/docs/deployments/environments). 3. Create the first deployment [#3-create-the-first-deployment] Choose **Deploy**. Easel creates a deployment and moves it through statuses such as Queued, Building, and Ready. Open the build logs while the deployment runs. If the build fails, start with the first actionable error rather than the final process-exit message. See [Deployment troubleshooting](/docs/deployments/troubleshooting). First-deployment behavior [#first-deployment-behavior] Connecting a repository triggers an initial build. The environment follows the usual mapping: * The configured Production branch creates a **Production** deployment * Other branches create **Preview** deployments **Auto-assign production domains** is on by default. When a Production deployment reaches Ready, Easel marks it **Current** and assigns the project Production URL and live custom domains. If auto-assign is off, the deployment stays **Staged** until you [promote](/docs/deployments/promote) it. The default project URL looks like: ```text https://{project}-{workspace}.preview.easelusercontent.com ``` See [Deployment URLs](/docs/deployments/urls). 4. Open the deployment [#4-open-the-deployment] When the deployment reaches **Ready**, open its deployment URL. Confirm: * The homepage loads * A deep route loads * Static assets load * Server-rendered or API routes work * Authentication initializes correctly, when applicable A unique deployment URL identifies that specific commit deployment. Stable branch Preview and Production URLs may point to different deployments over time. 5. Inspect a request [#5-inspect-a-request] Generate a request by opening the deployed application. Then: 1. Open the project in Easel. 2. Select **Logs** (or **Observability** for charts). 3. Find the request you just generated. 4. Open the request detail, then the **Logs** and **Trace** tabs. See [Request details](/docs/observability/request-details) and [Observability](/docs/observability). 6. Create a Preview deployment [#6-create-a-preview-deployment] Create a branch locally: ```bash git checkout -b improve-homepage ``` Make a visible change, commit it, and push the branch: ```bash git add . git commit -m "Improve homepage" git push -u origin improve-homepage ``` Easel creates a new Preview deployment that: * Uses Preview configuration * Does not replace Production traffic * Receives a unique deployment URL * Updates a stable branch Preview URL after it reaches Ready * Can attach a pull-request Preview URL when a PR is open Open the Preview URL and verify the change. See [Preview deployments](/docs/deployments/previews). 7. Open a pull request [#7-open-a-pull-request] Open a pull request from the feature branch into the Production branch. The GitHub integration can show deployment status, Preview URL, and commit checks on the pull request. Each new commit creates another deployment. The stable Preview URL moves only after the new deployment reaches Ready. 8. Release to Production [#8-release-to-production] Production-branch workflow (default) [#production-branch-workflow-default] When auto-assign is on: 1. Merge the pull request. 2. The merge commit on the Production branch creates a **new** Production deployment (Preview artifacts are not reused for production traffic). 3. The current Production deployment continues serving traffic while the new deployment builds. 4. After the new deployment reaches Ready, Easel assigns Production traffic to it. A failed deployment leaves the previous Current deployment in place. Manual release workflow [#manual-release-workflow] When **Auto-assign production domains** is off: 1. Create or wait for a Ready Production deployment (**Staged**). 2. Review it on its unique URL. 3. Choose **Promote** to make it Current. Promoting a **Preview** deployment rebuilds with Production environment variables. Instant promote of a Staged Production deployment does not rebuild. See [Promoting deployments](/docs/deployments/promote). 9. Verify Production [#9-verify-production] Open the Production URL and confirm: * The new change is present * The expected deployment is marked Current * Requests appear in Logs * Functions and APIs work * Cache behavior is expected * No new runtime errors appear Platform protections [#platform-protections] Every supported deployment receives Easel’s platform protections automatically. You do not need custom firewall rules before making a normal application available. For project-specific controls, see: * [Firewall](/docs/security/firewall) * [Custom rules](/docs/security/custom-rules) * [Attack Mode](/docs/security/attack-mode) Add a custom domain [#add-a-custom-domain] After verifying Production, connect a domain you control. See [Add a custom domain](/docs/domains/add-a-domain). What you have now [#what-you-have-now] You now have: * A GitHub-connected Easel project * A Ready deployment * A defined Production branch * Automatic Preview deployments * A Production release workflow * Request-level observability * Easel platform protections For AI agents [#for-ai-agents] Prefer Markdown over HTML when reading these docs: * Append `.md` to any docs URL (for example `/docs/getting-started.md`), or send `Accept: text/markdown`. * Index: [`/llms.txt`](/llms.txt) · Full corpus: [`/llms-full.txt`](/llms-full.txt) * MCP server for Cursor and other clients: [`/api/mcp`](/api/mcp) Next steps [#next-steps] # Easel documentation # Store and expire data in Runtime Cache Runtime Cache is a deployment-scoped key/value store shared across function isolates. Use it for computed fragments, rate-limit counters, and other short-lived data that does not belong in your primary database. On Easel it implements the same `getCache()` contract as `@vercel/functions`. For complete HTTP responses at the edge, use [CDN caching](/docs/cdn/caching) instead. Prerequisites [#prerequisites] Your deployment must receive platform `RUNTIME_CACHE_ENDPOINT`. Easel injects that on deploy for supported runtimes. Auth is an invoke-time platform JWT bound onto `getContext().cache` (same path stock `@vercel/functions` `getCache()` prefers on Vercel functions). Using Runtime Cache [#using-runtime-cache] Pick your stack. Both paths share the same platform store and tag clock. Next.js Other ```ts title="app/products/[id]/page.tsx" import { cacheLife, cacheTag } from "next/cache"; async function getProduct(id: string) { "use cache: remote"; cacheTag(`product-${id}`, "products"); cacheLife({ expire: 3600 }); return loadProduct(id); } export default async function Page({ params, }: { params: Promise<{ id: string }>; }) { const { id } = await params; const product = await getProduct(id); return
{JSON.stringify(product, null, 2)}
; } ```
```ts title="api/catalog.ts" import { getCache } from "@vercel/functions"; export default { async fetch(request: Request) { const id = new URL(request.url).searchParams.get("id") ?? "unknown"; const cache = getCache({ namespace: "catalog" }); const key = `product:${id}`; const cached = await cache.get(key); if (cached) { return Response.json(cached); } const product = await loadProduct(id); await cache.set(key, product, { tags: [`product-${id}`, "products"], ttl: 3600, }); return Response.json(product); }, }; ```
You can also call `getCache()` from Next.js Route Handlers the same way as the Other example. Expire by tag [#expire-by-tag] Next.js Other ```ts title="app/actions.ts" "use server"; import { revalidateTag } from "next/cache"; export async function updateProduct(id: string) { await saveProduct(id); revalidateTag(`product-${id}`); revalidateTag("products"); } ``` ```ts title="api/update-product.ts" import { getCache } from "@vercel/functions"; export default { async fetch(request: Request) { const id = new URL(request.url).searchParams.get("id") ?? "unknown"; await saveProduct(id); await getCache({ namespace: "catalog" }).expireTag([ `product-${id}`, "products", ]); return Response.json({ ok: true }); }, }; ``` `revalidateTag` and `expireTag` both update the shared platform tag clock and notify the edge CDN. `expireTag` also deletes matching Runtime Cache entry objects. If HTTP responses used the same `Cache-Tag` values, those CDN objects are eligible for purge. See [Invalidate and revalidate cached content](/docs/cdn/revalidation). API surface [#api-surface] * **`get`**: returns the stored value, or a miss when missing or expired * **`set`**: writes a value; optional `ttl` (seconds), `tags`, and `name` * **`delete`**: removes one key * **`expireTag`**: removes every entry that carried that tag (or tags) Maximum item size is **2 MB**. Treat the cache as best-effort speed, not durable storage. Quotas and observability [#quotas-and-observability] Workspace plans include a Runtime Cache storage pool. Observability shows usage so you can trim keys or upgrade before writes fail. Prefer namespaced keys per feature so one workload does not crowd out another in the same workspace. Related guides [#related-guides] * [Invalidate and revalidate cached content](/docs/cdn/revalidation) * [Cache responses at the edge](/docs/cdn/caching) * [Cache observability](/docs/observability/cache) * [Observability](/docs/observability) # Easel SDKs Official language SDKs are not yet generally available. Use framework-native APIs for application behavior, the [Easel CLI](/docs/cli) for deployment automation, and documented HTTP interfaces only after the public API is released. Future SDK documentation will cover versioning, authentication, retries, pagination, error types, and compatibility policy when clients ship. Build and deploy today [#build-and-deploy-today] * [CLI](/docs/cli) * [Getting started](/docs/getting-started) * [Deployments](/docs/deployments) * [API status](/docs/api) Need programmatic access beyond the CLI? Contact [hello@easel.sh](mailto:hello@easel.sh). # Troubleshooting Use this page when a deploy is up but the site, cache, or firewall does not behave as expected. For framework-specific limits, see [Framework guides](/docs/framework-guides). Custom domain does not go live [#custom-domain-does-not-go-live] 1. Confirm the DNS records match what **Project settings → Custom Domains** shows (CNAME for subdomains, A for apex). 2. Wait for DNS propagation. Some providers finish in a few minutes; others take longer. 3. Check domain status in the dashboard. It must leave pending DNS before TLS can finish. 4. Open the hostname over `https://` after status shows live. See [Domains](/docs/domains) for the full setup flow. First HTTPS request fails after pointing DNS [#first-https-request-fails-after-pointing-dns] The first request after DNS points at Easel can fail until the certificate is ready. Wait a short time and retry. Later requests use the provisioned certificate. Details: [HTTPS and TLS](/docs/cdn/https-and-tls). Preview URL works but custom domain does not [#preview-url-works-but-custom-domain-does-not] Custom domains serve production only. Confirm you pushed to the production branch and that production is healthy on the generated production URL first. See [Environments](/docs/deployments/environments). Unexpected cache MISS or BYPASS [#unexpected-cache-miss-or-bypass] Check the `X-Easel-Cache` response header. Time to live (TTL) is how long a response may stay cached: | Value | What to check | | -------- | ------------------------------------------------------------------------------------------------------------ | | `MISS` | First request after deploy or after TTL expiry is normal. Confirm your cache headers allow edge caching. | | `BYPASS` | Response or request is not cacheable (for example `Set-Cookie`, `Authorization`, `private`, or missing TTL). | | `HIT` | Served from the CDN cache. | Easel does not invent a default edge TTL when Cache-Control is missing. Dynamic pages stay uncached until you set eligible headers. See [Caching](/docs/cdn/caching). Content stays stale after you invalidate [#content-stays-stale-after-you-invalidate] 1. Confirm you expired the same tag string you wrote (`Cache-Tag`, Runtime Cache `tags`, or Next `cacheTag` / `revalidateTag`). 2. For CDN-only objects, confirm the response included `Cache-Tag` (or `x-next-cache-tags` / `surrogate-key`) when it was stored. 3. For Runtime Cache, call `expireTag` (or delete the key) and verify `RUNTIME_CACHE_*` env is present on the deployment. 4. For Next.js ISR, use `revalidatePath` / `revalidateTag` and fetch the page again; the first request after expiry may regenerate. See [Invalidate and revalidate cached content](/docs/cdn/revalidation) and [Runtime Cache](/docs/runtime-cache). Visitors see 403 or a challenge page [#visitors-see-403-or-a-challenge-page] 1. Open **Project settings → Custom WAF rules** and look for **Deny** or **Challenge** rules that match the path, IP, or headers in use. 2. Check whether **Attack challenge mode** is on for the project. 3. Use a **Log** action first when testing new rules so you can observe matches without blocking users. 4. Confirm `X-Easel-Firewall-Action` and `X-Easel-Firewall-Rule-Id` on the response when present. See [Custom firewall rules](/docs/security/custom-rules) and [Attack Mode](/docs/security/attack-mode). Site returns 503 with x-easel-serve-denied [#site-returns-503-with-x-easel-serve-denied] 1. Read the `x-easel-serve-denied` header (or response body). 2. If the code is `PROJECT_PAUSED`, an Owner or Admin can turn off **Pause production** in **Project settings**. Preview URLs should still work. 3. If the code is `WORKSPACE_SUSPENDED`, the whole workspace is blocked until the platform clears suspension. 4. Wait up to `30s` after resume or unsuspend for the edge manifest cache to expire, then retry. Environment variable missing at runtime [#environment-variable-missing-at-runtime] 1. Confirm the variable targets the right environment (production vs preview). 2. Remember changes apply to new deployments only. Redeploy after edits. 3. For a single preview branch, check whether a branch override is set. Configure variables in **Project settings → Environment Variables**, then redeploy. Still stuck [#still-stuck] Include the deployment URL, approximate time, and `X-Easel-Id` response header when you contact support. That id correlates the request in edge logs. See [System headers](/docs/cdn/headers). # Caching Easel can cache static files and dynamic HTTP responses so repeated requests can be served without running application code again. Static deployment assets are handled automatically. Dynamic responses are cached when they include an eligible shared-cache policy. Static and dynamic caching [#static-and-dynamic-caching] Easel caches two broad types of content. | Static deployment assets | Dynamic responses | | ---------------------------------------- | --------------------------------------------- | | Produced during the build | Produced by a function or external origin | | Served without invoking application code | May require application code on a cache miss | | Versioned with the deployment | Controlled by response cache headers | | Suitable for long-lived caching | Usually use shorter, application-defined TTLs | Examples of static deployment assets include JavaScript bundles, CSS, images, fonts, and generated HTML files. Examples of dynamic responses include server-rendered pages, API responses, route handlers, and responses proxied from an external origin. Cache a dynamic response [#cache-a-dynamic-response] Return a shared-cache directive with a positive lifetime: ```http id="kbn2sh" Cache-Control: public, max-age=0, must-revalidate CDN-Cache-Control: public, s-maxage=3600 ``` This configuration means: * Browsers must revalidate the response. * Easel may reuse the response for one hour. * A later request can be served without invoking the application. `CDN-Cache-Control` is the recommended header for controlling Easel’s shared cache without changing browser behavior. Cache-Control headers [#cache-control-headers] Easel recognizes the following response headers, in descending order of precedence: 1. `Vercel-CDN-Cache-Control` 2. `CDN-Cache-Control` 3. `Cache-Control` Only the highest-priority header present is used to determine Easel’s shared-cache policy. Portable CDN policy [#portable-cdn-policy] Use `CDN-Cache-Control` when the browser and CDN should have different policies: ```http id="th916n" Cache-Control: private, max-age=0 CDN-Cache-Control: public, s-maxage=600 ``` The browser does not retain a reusable private copy, while Easel can cache the response for ten minutes. Vercel compatibility [#vercel-compatibility] Easel recognizes `Vercel-CDN-Cache-Control` so applications migrated from Vercel preserve their existing CDN behavior. ```http id="gc48wx" Vercel-CDN-Cache-Control: public, s-maxage=3600 ``` When both `Vercel-CDN-Cache-Control` and `CDN-Cache-Control` are present, the Vercel-specific header takes precedence. Cache eligibility [#cache-eligibility] A dynamic response is eligible for shared caching when all of the following are true: * The request method is `GET` or `HEAD`. * The response has a cacheable status. * The selected cache policy contains a positive shared-cache lifetime. * The response is not marked `private`, `no-store`, or `no-cache`. * The response does not contain `Set-Cookie`. * The request does not contain `Authorization`. * The request is not a range request. If any condition prevents caching, Easel returns: ```http id="qed72a" X-Easel-Cache: BYPASS ``` Cacheable status codes [#cacheable-status-codes] Easel can cache responses with these status codes: * `200` * `301` * `302` * `307` * `308` * `404` A cache policy does not make every status code cacheable. Shared cache lifetime [#shared-cache-lifetime] Use `s-maxage` to define how long Easel may consider a response fresh: ```http id="8dnfpm" CDN-Cache-Control: public, s-maxage=300 ``` The response remains fresh in the shared cache for five minutes. When `s-maxage` is absent, Easel can use `max-age` from the selected cache-control header. ```http id="w627pt" CDN-Cache-Control: public, max-age=300 ``` For dynamic responses, prefer `s-maxage` when the browser and Easel should use different lifetimes. Browser and CDN caching [#browser-and-cdn-caching] Browser caching and Easel caching are related but separate. ```http id="p2f8xe" Cache-Control: public, max-age=60 CDN-Cache-Control: public, s-maxage=3600 ``` This response can be: * reused by the browser for one minute * reused by Easel for one hour After the browser copy expires, the browser may request the resource again while Easel continues serving it from the CDN cache. After the fresh lifetime [#after-the-fresh-lifetime] When the shared-cache lifetime ends and the response did not set `stale-while-revalidate`, the next matching request waits while Easel obtains an updated response, then stores that response if it remains eligible. ```http id="8s0qxj" CDN-Cache-Control: public, s-maxage=60 ``` This policy means the response is fresh for 60 seconds. After that, Easel does not serve the expired entry as a cache hit. Stale-while-revalidate [#stale-while-revalidate] Set an explicit `stale-while-revalidate` window to serve the expired entry while Easel refreshes it in the background: ```http id="swr-cdn" CDN-Cache-Control: public, s-maxage=60, stale-while-revalidate=300 ``` With this policy: * the response is fresh for 60 seconds (`X-Easel-Cache: HIT`) * for the next 300 seconds, Easel may serve the stale body (`X-Easel-Cache: STALE`) while refreshing in the background * after that window, the next request waits for a sync refetch Omit the directive when you want a sync refresh after the fresh TTL. Easel does not invent a default stale window when the directive is absent. Frameworks such as Next.js may emit `stale-while-revalidate` for you. Responses that are not cached [#responses-that-are-not-cached] Private responses [#private-responses] Do not place user-specific responses in a shared cache. ```http id="33tw38" Cache-Control: private, no-store ``` Use this for account pages, personalized API responses, authentication results, and other content that must not be reused across visitors. Cookies [#cookies] A response containing `Set-Cookie` bypasses shared caching. ```http id="cf8xeg" Set-Cookie: session=... ``` Separate cookie-setting behavior from cacheable content when possible. For example, set the cookie on one endpoint and redirect to a cacheable page. Authorization [#authorization] Requests containing an `Authorization` header bypass shared caching. This prevents authenticated responses from being reused by unrelated requests. Range requests [#range-requests] Range requests bypass the shared cache. Easel does not currently serve partial content (`206`) for deployment static assets. Missing cache policy [#missing-cache-policy] Dynamic responses are not stored when they do not include a usable positive cache lifetime. ```http id="08jl2m" Cache-Control: public ``` This response is public but does not define how long it may remain fresh. Cache results [#cache-results] Easel exposes the result of cache processing through `X-Easel-Cache`. | Value | Meaning | | -------- | ------------------------------------------------------------------------------------------------------------------------ | | `HIT` | A fresh cached response was served | | `STALE` | A stale cached response was served within an explicit `stale-while-revalidate` window while refreshing in the background | | `MISS` | No usable fresh or SWR-eligible stale entry was found | | `BYPASS` | The request or response was not eligible for caching | A first request commonly returns `MISS`, while a later identical request returns `HIT`. After the fresh lifetime, an explicit `stale-while-revalidate` window may return `STALE`. ```bash id="n0gql2" curl -I https://example.com/products curl -I https://example.com/products ``` Example: ```http id="whaj21" HTTP/2 200 CDN-Cache-Control: public, s-maxage=300 X-Easel-Cache: HIT ``` Treat `X-Easel-Cache` as the authoritative cache-result header. An `Age` header may appear on some cached responses depending on the serving path; its absence does not mean the response was uncached. Cache keys [#cache-keys] A cache key identifies which requests can reuse the same stored response. Easel scopes entries to the deployment (or the hostname when deployment attribution is unavailable) and keys them by the request path and query string. Only `GET` and `HEAD` are eligible for shared caching, so the method is not a separate key dimension. Compression negotiation is handled by storing uncompressed bodies and encoding on egress, so `Accept-Encoding` does not create separate cache entries. Requests with different paths or query strings are treated as different cache entries unless a documented routing or cache configuration changes that behavior. Query strings [#query-strings] These URLs are distinct cache entries: ```text id="1nrch6" /products?page=1 /products?page=2 ``` Avoid adding high-cardinality or unnecessary query parameters to cacheable URLs. Tracking parameters can reduce cache reuse when every distinct URL creates a separate cache entry. Vary [#vary] The `Vary` response header indicates that a response changes based on selected request headers. ```http id="t4mryw" Vary: Accept-Language ``` Use `Vary` carefully. Headers with many possible values reduce cache reuse. Compression negotiation is handled automatically. Easel stores uncompressed bodies and encodes on egress, and merges: ```http id="ohafq4" Vary: Accept-Encoding ``` Do not use `Vary: *` for cacheable responses. Static deployment assets [#static-deployment-assets] Static files produced during the build are versioned with the deployment and can be served without invoking an Easel Function. Content-hashed files such as: ```text id="svgyg2" /assets/app.9f3ac21.js ``` can use long cache lifetimes because a changed file receives a new URL. HTML and other route-level files may use shorter policies because their URLs commonly remain stable across deployments. Static deployment behavior, asset headers, and deployment isolation are covered in [Static files](/docs/cdn/static-files). Framework caching [#framework-caching] Supported frameworks can generate cache policies and revalidation metadata automatically. Examples include: * Next.js ISR and Cache Components * Nuxt route rules * SvelteKit adapter output * framework-generated static pages Easel maps supported framework behavior onto the CDN and Runtime Cache where appropriate. You can still inspect the resulting HTTP policy through response headers such as: ```http id="y9ix3f" Cache-Control CDN-Cache-Control X-Easel-Cache ``` See the relevant [framework guide](/docs/framework-guides) for framework-specific behavior. Revalidation and purging [#revalidation-and-purging] Expiration determines when a cached response naturally becomes stale. Revalidation and purging allow you to refresh content earlier through: * cache tags * framework revalidation APIs * supported Runtime Cache invalidation * project cache controls Use [Revalidation and purging](/docs/cdn/revalidation) when content must be refreshed before its configured TTL expires. Debugging cache behavior [#debugging-cache-behavior] Inspect the response headers: ```bash id="vnj820" curl -I https://example.com ``` The response is always MISS [#the-response-is-always-miss] Check that: * the response has a positive shared-cache TTL * repeated requests use the same hostname, path, and query string * the response status is cacheable * the cache entry has not expired between requests * the deployment has not changed The response is BYPASS [#the-response-is-bypass] Check for: * `Set-Cookie` * an `Authorization` request header * a `Range` request header * `private` * `no-store` * `no-cache` * a missing positive TTL * an unsupported response status The browser shows old content [#the-browser-shows-old-content] The browser may be using its own cached copy even after the Easel cache has been invalidated. Compare the browser policy in `Cache-Control` with the shared policy in `CDN-Cache-Control`. Use a shorter browser `max-age` when content must reflect CDN invalidation quickly. The cache contains too many variants [#the-cache-contains-too-many-variants] Inspect: * query parameters * `Vary` * locale or geography-based behavior * generated tracking parameters * URLs that include unique identifiers Reducing unnecessary variation increases the cache hit rate. Related documentation [#related-documentation] * [CDN overview](/docs/cdn) * [Revalidation and purging](/docs/cdn/revalidation) * [Static files](/docs/cdn/static-files) * [Compression](/docs/cdn/compression) * [Request and response headers](/docs/cdn/headers) * [Runtime Cache](/docs/runtime-cache) * [Framework guides](/docs/framework-guides) # Compression Easel automatically compresses eligible responses before sending them to visitors. When supported by the client, Easel prefers Brotli compression and falls back to gzip. Responses that are already compressed or unlikely to benefit from compression are sent unchanged. Supported encodings [#supported-encodings] Easel negotiates compression using the request’s `Accept-Encoding` header. For example: ```http Accept-Encoding: br, gzip ``` When both encodings are supported, Easel prefers Brotli: ```http Content-Encoding: br ``` When Brotli is unavailable but gzip is supported, Easel may return: ```http Content-Encoding: gzip ``` When the client does not advertise a supported encoding, Easel sends the response without compression. Automatic compression [#automatic-compression] Compression can apply to both static files and dynamic responses. Common examples include: * HTML * CSS * JavaScript * JSON * XML * SVG * plain text * source maps * WebAssembly * common font and manifest formats Easel compresses a response only when doing so is expected to reduce its transferred size. Responses that are not compressed [#responses-that-are-not-compressed] Easel generally does not compress: * responses that already include `Content-Encoding` * byte-range responses * very small responses * formats that are already compressed * responses with an unsupported or missing content type Common already-compressed formats include: * JPEG * PNG * WebP * AVIF * GIF * MP4 * WebM * MP3 * ZIP * gzip archives * Brotli-compressed files Compressing these formats again usually provides little benefit and can increase processing overhead. Minimum response size [#minimum-response-size] Easel does not compress responses smaller than 256 bytes. For very small responses, compression headers and processing can outweigh the reduction in payload size. This threshold applies to the uncompressed response body. Precompressed responses [#precompressed-responses] When an application or origin already returns a compressed response, Easel preserves the encoding instead of compressing it again. For example: ```http Content-Encoding: gzip ``` The response body must match the declared encoding. Do not manually set `Content-Encoding` unless the body has already been encoded. Declaring gzip or Brotli for an uncompressed body causes browsers and other clients to fail when decoding the response. Content negotiation [#content-negotiation] Compressed and uncompressed responses are different representations of the same resource. Easel adds or preserves: ```http Vary: Accept-Encoding ``` This prevents a cache from serving a Brotli-compressed response to a client that does not support Brotli. Applications should not remove `Accept-Encoding` from `Vary` when a response can be compressed. Compression and caching [#compression-and-caching] Compression works alongside CDN caching. A cacheable response can be stored and later delivered using an encoding supported by the requesting client. For example: ```http CDN-Cache-Control: public, s-maxage=3600 Content-Encoding: br Vary: Accept-Encoding X-Easel-Cache: HIT ``` The cache status describes whether the HTTP response was reused. The content encoding describes how the response body was transferred. See [Caching](/docs/cdn/caching) for cache eligibility and cache-control behavior. Static files [#static-files] Eligible static deployment files are compressed automatically. This commonly includes: ```text /index.html /assets/app.js /assets/styles.css /data/products.json /images/logo.svg ``` No application configuration is required. Content-hashed assets can still use long cache lifetimes independently from their compression behavior. See [Static files](/docs/cdn/static-files). Dynamic responses [#dynamic-responses] Responses returned from Easel Functions can also be compressed automatically. For example: ```ts export async function GET() { return Response.json({ products: await loadProducts(), }); } ``` When the resulting JSON response is eligible, Easel can compress it before delivery. Applications do not need to manually compress ordinary HTML, JSON, JavaScript, or text responses. Streaming responses [#streaming-responses] Streaming responses can be compressed when the selected runtime and response path support streaming compression. Easel forwards data as it becomes available rather than waiting for the complete response body before sending it. Compression may add a small amount of buffering before the first compressed output is produced. For latency-sensitive streams, validate the behavior with a preview deployment. Do not assume that every streaming format benefits from compression. Event streams with very small messages may trade reduced bandwidth for additional buffering. Server-Sent Events [#server-sent-events] Server-Sent Events use: ```http Content-Type: text/event-stream ``` Compression behavior can affect when small event chunks become visible to the client. For highly latency-sensitive event streams, explicitly test whether compression is appropriate. Compression may buffer small chunks before producing output. Range requests [#range-requests] Range requests are not compressed and bypass the shared CDN cache. Do not treat partial content (`206`) delivery as a supported feature for deployment static assets. Clients that send `Range` for those assets should not expect compressed or cacheable partial responses from Easel’s CDN path. Content types [#content-types] Compression eligibility depends partly on the response `Content-Type`. Examples of commonly compressible types include: ```http text/html text/css text/plain text/javascript application/javascript application/json application/xml image/svg+xml application/wasm ``` Use a correct content type whenever possible. A missing or incorrect `Content-Type` can prevent compression or cause the client to interpret the response incorrectly. Custom compression [#custom-compression] Most applications should rely on Easel’s automatic compression. Manual compression may be appropriate when: * an upstream origin already provides a compressed representation * an application serves a custom precompressed format * a large static file is generated outside the normal build process * exact compression parameters are part of the application protocol When manually compressing, ensure that: * the response body matches `Content-Encoding` * `Vary: Accept-Encoding` is present * the client supports the selected encoding * caches do not mix encoded and unencoded variants Do not manually compress a response and then allow a framework or proxy to compress it a second time. Verify compression [#verify-compression] Use `curl` with automatic encoding negotiation: ```bash curl --compressed -I https://example.com ``` A Brotli-compressed response may include: ```http HTTP/2 200 Content-Type: text/html; charset=utf-8 Content-Encoding: br Vary: Accept-Encoding ``` Test gzip explicitly: ```bash curl -I \ -H "Accept-Encoding: gzip" \ https://example.com ``` Test Brotli explicitly: ```bash curl -I \ -H "Accept-Encoding: br" \ https://example.com ``` Test without compression support: ```bash curl -I \ -H "Accept-Encoding: identity" \ https://example.com ``` Compare transferred sizes [#compare-transferred-sizes] Use `curl` to compare the response size with and without compression: ```bash curl -sS \ -H "Accept-Encoding: identity" \ -o /dev/null \ -w "Uncompressed: %{size_download} bytes\n" \ https://example.com ``` ```bash curl -sS --compressed \ -o /dev/null \ -w "Compressed: %{size_download} bytes\n" \ https://example.com ``` The exact savings depend on the response contents. Text-heavy HTML, CSS, JavaScript, and JSON usually compress well. Images, archives, and video generally do not. Troubleshooting [#troubleshooting] The response is not compressed [#the-response-is-not-compressed] Check that: * the request includes `Accept-Encoding: br` or `gzip` * the response is larger than the minimum size * the response uses a supported content type * the response does not already include `Content-Encoding` * the request is not a range request * the format is expected to benefit from compression Brotli is not selected [#brotli-is-not-selected] Confirm that the request advertises Brotli: ```http Accept-Encoding: br, gzip ``` Some clients, proxies, and development tools advertise only gzip. The browser reports a decoding error [#the-browser-reports-a-decoding-error] Check whether the application manually set: ```http Content-Encoding: gzip ``` or: ```http Content-Encoding: br ``` without actually encoding the response body. Also check for double compression by an application framework or upstream proxy. Different clients receive different body sizes [#different-clients-receive-different-body-sizes] This is expected when clients advertise different compression support. A Brotli-capable browser may receive a smaller response than a client that supports only gzip or no compression. Compression appears to delay a stream [#compression-appears-to-delay-a-stream] Compression can buffer small chunks before producing output. Test the route with and without compression support from the client. For event streams or highly interactive responses, measure whether the savings outweigh any added buffering. Related documentation [#related-documentation] * [CDN overview](/docs/cdn) * [Caching](/docs/cdn/caching) * [Static files](/docs/cdn/static-files) * [Request and response headers](/docs/cdn/headers) # Request and response headers Easel adds a small set of trusted headers to requests and responses for routing, observability, security, and cache diagnostics. Use documented headers when application behavior depends on platform metadata. Undocumented internal headers may change without notice and are not a stable interface. Request headers [#request-headers] Easel can attach trusted metadata before forwarding a request to application code. These headers describe the request as observed by Easel rather than values supplied directly by the client. Request ID [#request-id] ```http X-Easel-Id: req_01J... ``` A unique identifier for the request. Use this value to correlate: * client responses * function logs * platform logs * traces * firewall events * support investigations The same request ID may also be returned in the response. Do not generate or overwrite this header in application code. Deployment metadata [#deployment-metadata] On function invokes, Easel may expose deployment context through documented request headers. For example: ```http X-Deployment-Id: dpl_01J... X-Easel-Deployment-Environment: production ``` Possible environment values: ```text production preview development ``` Use deployment metadata for diagnostics and environment-aware behavior. Prefer project environment variables for ordinary application configuration. Request headers must not replace stable configuration such as database URLs, API credentials, or feature flags. Function region [#function-region] Dynamic requests may include the region in which application code is running: ```http X-Easel-Function-Region: iad ``` This can help diagnose latency relative to your databases and APIs. Functions currently run in US East (`iad`). The header identifies that compute region on the request your application receives. Do not store application state locally based on region headers. Function instances remain ephemeral and independently scalable. Forwarded headers [#forwarded-headers] Easel may forward standardized proxy information using headers such as: ```http Forwarded: for=203.0.113.42;proto=https;host=example.com ``` or: ```http X-Forwarded-For: 203.0.113.42 X-Forwarded-Proto: https X-Forwarded-Host: example.com ``` `X-Forwarded-Proto` is the protocol the visitor used when connecting to Easel. For deployed applications, the expected value is normally `https`. `X-Forwarded-Host` is the hostname requested by the visitor before internal routing or rewrites. Use it when application behavior depends on the public hostname, such as canonical URLs, domain-specific branding, or authentication callback URLs. Validate hostnames before using them to construct security-sensitive URLs. Clients or upstream proxies can send forwarded headers themselves. Treat them as proxy metadata, not as proof of identity, unless your integration documents a trusted proxy chain. Frameworks may expose the same information through their own request APIs. Trust boundaries [#trust-boundaries] Headers received from the public internet are untrusted. Easel overwrites documented platform headers where required for routing and diagnostics. Client-supplied values do not control routing or security decisions. For example, sending: ```http X-Easel-Function-Region: iad X-Easel-Cache: HIT X-Easel-Firewall-Action: allow ``` does not control Easel’s internal routing or security behavior. Only values attached by Easel after the request enters the platform must be treated as trusted platform metadata. Reading request headers [#reading-request-headers] Web standard Request API [#web-standard-request-api] ```ts export async function GET(request: Request) { const requestId = request.headers.get("x-easel-id"); const environment = request.headers.get( "x-easel-deployment-environment", ); const region = request.headers.get("x-easel-function-region"); return Response.json({ requestId, environment, region, }); } ``` Header names are case-insensitive. Next.js App Router [#nextjs-app-router] ```ts import { headers } from "next/headers"; export async function GET() { const requestHeaders = await headers(); return Response.json({ requestId: requestHeaders.get("x-easel-id"), environment: requestHeaders.get("x-easel-deployment-environment"), region: requestHeaders.get("x-easel-function-region"), deploymentId: requestHeaders.get("x-deployment-id"), }); } ``` Use the framework’s normal request-header API where available. Response headers [#response-headers] Easel adds response headers that help identify the request path, cache result, security decision, and platform behavior. Request ID [#request-id-1] ```http X-Easel-Id: req_01J... ``` The identifier associated with the request. Use this value when searching logs or reporting a specific failed request. Cache result [#cache-result] ```http X-Easel-Cache: HIT ``` Indicates how the CDN cache handled the request. | Value | Meaning | | -------- | ------------------------------------------------------------------------------------------------------------------------ | | `HIT` | A fresh cached response was served | | `STALE` | A stale cached response was served within an explicit `stale-while-revalidate` window while refreshing in the background | | `MISS` | No usable fresh or SWR-eligible stale entry was found; origin or regeneration ran | | `BYPASS` | The request or response was not eligible for shared caching | See [Caching](/docs/cdn/caching) for eligibility and debugging. Age [#age] ```http Age: 42 ``` The number of seconds a response has spent in a shared cache. `Age` may be present depending on the serving path. Treat `X-Easel-Cache` as the authoritative cache-result header. Absence of `Age` is not an error. When both are present: ```http X-Easel-Cache: HIT Age: 42 ``` the cached entry has been stored for approximately 42 seconds. Server [#server] ```http Server: Easel ``` Identifies Easel as the response-serving platform. Applications must not depend on this header for behavior or security decisions. Content encoding [#content-encoding] ```http Content-Encoding: br ``` Indicates that Easel or the application compressed the response. Common values include: ```text br gzip ``` Responses may also include: ```http Vary: Accept-Encoding ``` See [Compression](/docs/cdn/compression). Firewall diagnostics [#firewall-diagnostics] When available, Easel may include supported firewall metadata such as: ```http X-Easel-Firewall-Action: allow X-Easel-Firewall-Rule-Id: rule_01J... ``` These headers can help explain why a request was allowed, blocked, or challenged. Firewall metadata may be omitted when it would reveal sensitive security information or when no project rule was involved. Do not expose internal rule identifiers to end users unless they are useful for support or administrative diagnostics. See [Security](/docs/security). Preview indexing policy [#preview-indexing-policy] Preview deployments include: ```http X-Robots-Tag: noindex, nofollow, noarchive ``` This discourages search engines from indexing preview environments. The header does not provide access control. Anyone with the preview URL may still be able to access the deployment unless authentication or another access policy is enabled. HSTS [#hsts] HTTPS responses include: ```http Strict-Transport-Security: max-age=63072000 ``` This instructs compatible browsers to use HTTPS for future requests to the hostname. See [HTTPS and TLS](/docs/cdn/https-and-tls) for behavior and configuration guidance. Cache policy headers [#cache-policy-headers] Applications control browser and shared-cache behavior with response headers such as: ```http Cache-Control: public, max-age=0, must-revalidate CDN-Cache-Control: public, s-maxage=3600 Cache-Tag: products product:123 ``` Easel may consume CDN-specific directives while preserving the browser-facing policy. See: * [Caching](/docs/cdn/caching) * [Revalidation and purging](/docs/cdn/revalidation) Custom response headers [#custom-response-headers] Applications can return their own response headers. ```ts export async function GET() { return Response.json( { ok: true }, { headers: { "Cache-Control": "public, max-age=0", "CDN-Cache-Control": "public, s-maxage=300", "X-App-Version": "2026-08-06", }, }, ); } ``` You can also define headers through supported framework configuration or project routing rules. Custom headers must not use reserved Easel header names. Reserved headers [#reserved-headers] Do not rely on setting or overriding documented platform headers. Examples include: ```text X-Easel-Id X-Easel-Cache X-Easel-Function-Region X-Easel-Firewall-Action X-Easel-Firewall-Rule-Id X-Easel-Deployment-Environment X-Deployment-Id ``` Easel may replace, remove, or ignore these values. Use your own application namespace for custom diagnostics: ```http X-My-App-Version: 42 X-My-App-Trace: checkout ``` Hop-by-hop headers [#hop-by-hop-headers] Hop-by-hop headers apply only to one network connection and are not forwarded unchanged across proxies. Examples include: ```text Connection Keep-Alive Proxy-Authenticate Proxy-Authorization TE Trailer Transfer-Encoding Upgrade ``` Applications must not depend on receiving or controlling these headers through the CDN. WebSocket upgrade behavior, where supported, is documented separately from ordinary HTTP request forwarding. Header size and limits [#header-size-and-limits] Requests and responses are subject to platform limits for: * total header size * individual header size * number of headers * cookie size * URL length Requests exceeding supported limits may be rejected before application code runs. Responses exceeding supported limits may fail or have unsupported headers removed. Privacy and sensitive data [#privacy-and-sensitive-data] Headers may appear in: * access logs * function logs * traces * error reports * support diagnostics Do not place secrets or sensitive personal data in custom headers unless the application requires it and the value is handled appropriately. Avoid logging: ```text Authorization Cookie Set-Cookie API keys session tokens password reset tokens ``` Use request IDs to correlate events instead of copying authentication values into logs. Inspecting response headers [#inspecting-response-headers] Use `curl`: ```bash curl -I https://example.com ``` Example response: ```http HTTP/2 200 Cache-Control: public, max-age=0, must-revalidate CDN-Cache-Control: public, s-maxage=300 X-Easel-Id: req_01J... X-Easel-Cache: HIT Content-Encoding: br Server: Easel ``` For redirects, include only the response headers: ```bash curl -I http://example.com ``` To follow redirects: ```bash curl -IL http://example.com ``` Inspecting request metadata [#inspecting-request-metadata] Create a temporary diagnostic route that returns only non-sensitive headers: ```ts export async function GET(request: Request) { const names = [ "x-easel-id", "x-forwarded-proto", "x-forwarded-host", "x-easel-deployment-environment", "x-easel-function-region", "x-deployment-id", ]; const result = Object.fromEntries( names.map((name) => [name, request.headers.get(name)]), ); return Response.json(result, { headers: { "Cache-Control": "private, no-store", }, }); } ``` Remove diagnostic routes after testing. Do not return cookies, authorization headers, or other secrets to the browser. Troubleshooting [#troubleshooting] A request header is missing [#a-request-header-is-missing] Check whether: * the header is supported for the current route type * the request reached application compute * the framework or adapter removes it * a local development server is being used * an upstream proxy changed the request * the feature is available for the current project plan Some platform headers are present only on deployed requests and not during local development. Deployment and region headers are typically present on function invokes. X-Easel-Cache is always BYPASS [#x-easel-cache-is-always-bypass] Check for: * `Set-Cookie` * `Authorization` * `Range` * `Cache-Control: private` * `Cache-Control: no-store` * `Cache-Control: no-cache` * no positive shared-cache TTL * an unsupported method or status See [Caching](/docs/cdn/caching). The request ID does not appear in function logs [#the-request-id-does-not-appear-in-function-logs] The request may have completed before invoking the function. Possible reasons include: * firewall block * redirect * cache hit * static asset response * middleware-generated response Platform request logs may still contain the request ID even when no function executed. A custom header disappears [#a-custom-header-disappears] The header may be: * reserved by Easel * hop-by-hop * disallowed by the framework * removed by an upstream proxy * larger than a platform limit * overwritten by routing or project configuration Use a non-reserved application header name and inspect both preview and production responses. Headers differ between preview and production [#headers-differ-between-preview-and-production] Preview environments may add indexing, access-control, deployment, or diagnostic headers that do not appear in production. Application configuration and environment-specific routing rules can also differ. Related documentation [#related-documentation] * [CDN overview](/docs/cdn) * [Caching](/docs/cdn/caching) * [Revalidation and purging](/docs/cdn/revalidation) * [Compression](/docs/cdn/compression) * [HTTPS and TLS](/docs/cdn/https-and-tls) * [Security](/docs/security) # HTTPS and TLS Every Easel deployment is available over HTTPS. Easel automatically provisions and renews TLS certificates for deployment URLs and configured custom domains, so visitors can connect securely without manual certificate management. Automatic HTTPS [#automatic-https] Easel enables HTTPS for: * production deployments * preview deployments * Easel-provided deployment domains * configured custom domains Requests made over plain HTTP are redirected to HTTPS. ```text http://example.com ↓ https://example.com ``` Application code does not need to handle the redirect. Easel deployment domains [#easel-deployment-domains] Every deployment receives an Easel-provided HTTPS URL. The certificate is managed automatically and is ready as part of the deployment lifecycle. Deployment URLs are useful for: * previewing changes * testing production builds * sharing pull-request deployments * verifying domain-independent behavior * diagnosing custom-domain configuration No additional certificate configuration is required. Custom domains [#custom-domains] When you add a custom domain to an Easel project, Easel verifies the domain configuration and provisions a certificate for it automatically. A custom domain generally follows this process: 1. Add the domain to the Easel project. 2. Configure the required DNS records shown in project settings. 3. Easel verifies that the domain points to the project. 4. Easel provisions the TLS certificate. 5. The domain becomes available over HTTPS. Certificate provisioning begins after the required DNS records are visible. For setup steps, DNS records, redirects, and migration, see [Domains](/docs/domains) and [HTTPS and TLS for custom domains](/docs/domains/tls). Certificate management [#certificate-management] Easel manages the certificate lifecycle for supported domains. This includes: * certificate issuance * installation * renewal * replacement before expiration * serving the correct certificate for the requested hostname You do not need to upload certificate files or private keys for automatically managed domains. Avoid removing or changing required DNS records after a certificate has been issued. Easel must continue to validate and route the domain correctly in order to renew and serve its certificate. Provisioning status [#provisioning-status] A newly added domain may pass through several states before HTTPS is ready. | Status | Meaning | | ---------------------------- | -------------------------------------------------------------- | | Invalid DNS configuration | The required DNS records are not visible yet or do not match | | Provisioning SSL certificate | The domain is verified and certificate issuance is in progress | | Valid configuration | HTTPS is available | | Configuration error | DNS or domain settings prevent activation | | Removed | The domain was removed from the project | DNS changes are not always visible immediately. Provisioning continues automatically after the correct records propagate. If a domain remains unavailable, inspect its status in the Easel dashboard rather than repeatedly removing and re-adding it. Domain coverage [#domain-coverage] A certificate covers only the hostnames associated with the project and included in the certificate configuration. For example: ```text example.com www.example.com app.example.com ``` are separate hostnames. Adding `example.com` does not necessarily configure every subdomain beneath it. Add each hostname separately. Customer wildcard domains such as `*.example.com` are not currently supported. Apex and subdomain configuration [#apex-and-subdomain-configuration] Easel supports custom domains at both the apex and subdomain level. Examples: ```text example.com www.example.com docs.example.com ``` The required DNS record depends on the hostname and DNS provider. Apex domains commonly use an address or flattening record, while subdomains commonly use a CNAME. Follow the values shown in the Easel dashboard rather than copying records from another project or provider. Domain redirects [#domain-redirects] You can configure one domain as the canonical hostname and redirect alternate domains to it. For example: ```text www.example.com → example.com ``` or: ```text example.com → www.example.com ``` Easel provisions HTTPS for the redirecting hostname as well as the destination hostname. This prevents visitors from encountering a certificate warning before the redirect occurs. See [Domains](/docs/domains) for domain configuration. HTTPS redirects [#https-redirects] Plain HTTP requests are redirected to the equivalent HTTPS URL. For example: ```http GET http://example.com/products ``` is redirected to: ```http https://example.com/products ``` The path and query string are preserved. Applications should generate secure absolute URLs in production and use secure cookies for authentication-related data. HSTS [#hsts] Easel includes the `Strict-Transport-Security` response header on HTTPS responses: ```http Strict-Transport-Security: max-age=63072000 ``` HSTS instructs compatible browsers to use HTTPS for later requests to the hostname. Because browsers can retain HSTS policies for a long time, confirm that the domain can continue serving HTTPS before enabling a custom or more restrictive HSTS policy. Settings such as the following should be used deliberately: ```http Strict-Transport-Security: max-age=63072000; includeSubDomains; preload ``` `includeSubDomains` affects every subdomain, including subdomains that may not be hosted by Easel. Submitting a domain to browser preload lists can also be difficult to reverse. Review all subdomains before enabling preload behavior. Custom security headers [#custom-security-headers] Applications can add additional HTTPS-related response headers through framework or project configuration. Common examples include: ```http Content-Security-Policy: default-src 'self' X-Content-Type-Options: nosniff Referrer-Policy: strict-origin-when-cross-origin Permissions-Policy: camera=(), microphone=() ``` These headers control browser behavior and are separate from TLS certificate management. Test security-policy changes in a preview deployment before applying them to production. A restrictive Content Security Policy can block scripts, styles, images, or third-party integrations required by the application. Secure cookies [#secure-cookies] Authentication cookies should use appropriate security attributes. ```http Set-Cookie: session=...; Secure; HttpOnly; SameSite=Lax; Path=/ ``` The `Secure` attribute prevents the browser from sending the cookie over an unencrypted HTTP connection. The appropriate `SameSite` value depends on the application’s authentication and cross-origin behavior. Certificate Authority Authorization [#certificate-authority-authorization] A domain can use CAA DNS records to restrict which certificate authorities may issue certificates for it. Easel uses Let’s Encrypt for managed certificates. If the domain has CAA records, they must permit Let’s Encrypt. A restrictive or incorrect CAA record can prevent certificate provisioning or renewal. Example CAA records look like: ```text example.com. CAA 0 issue "letsencrypt.org" ``` Do not invent CAA records for other certificate authorities unless you intentionally issue certificates outside Easel. Confirm that any existing CAA policy still permits Let’s Encrypt. When troubleshooting issuance, check for CAA records on both the exact hostname and its parent domain. DNS proxies [#dns-proxies] Some DNS providers can proxy traffic in front of Easel. A proxy can affect: * domain verification * certificate issuance * request routing * client IP forwarding * redirects * caching * security rules When initially configuring a domain, you may need to disable the proxy until Easel verifies the DNS records and provisions the certificate. After activation, confirm that any upstream proxy is configured to connect to Easel over HTTPS and does not introduce conflicting redirects or caching behavior. Connecting to application compute [#connecting-to-application-compute] Visitors establish an HTTPS connection with Easel’s delivery network. Easel then securely routes the request to the deployment resource responsible for serving it. The exact resource may be: * a cached response * a static deployment asset * an Easel Function * an external origin reached through a rewrite You do not need to manage certificates between Easel’s delivery layer and Easel-managed application resources. For external origins, configure HTTPS on the origin and validate the origin certificate whenever supported. External origins [#external-origins] When a rewrite forwards a request to an external service, prefer an HTTPS origin URL: ```text https://api.example.net ``` Avoid forwarding sensitive requests to an unencrypted HTTP origin. When the origin should not be publicly accessible, use an authentication mechanism such as a secret request header or another supported origin-access control. Do not rely only on obscuring the origin hostname. Local development [#local-development] Local framework development servers may use HTTP by default: ```text http://localhost:3000 ``` This is separate from deployed Easel environments, which use HTTPS. Some browser features require a secure context but treat `localhost` as secure for development purposes. Test the deployed preview URL when validating: * secure cookies * HTTPS redirects * certificate behavior * mixed-content warnings * Content Security Policy * third-party OAuth callbacks * production domain handling Mixed content [#mixed-content] A page served over HTTPS should not load active resources over HTTP. For example, this can be blocked by browsers: ```html ``` Use HTTPS for scripts, stylesheets, fonts, API requests, images, and embedded content whenever available. Mixed-content errors appear in the browser developer console and are not caused by certificate provisioning for the page itself. Verifying HTTPS [#verifying-https] Use a browser or `curl` to inspect the connection and response: ```bash curl -I https://example.com ``` A working domain should return an HTTPS response without a certificate error. To inspect the HTTP redirect: ```bash curl -I http://example.com ``` The response should redirect to the HTTPS URL. You can also inspect the certificate with OpenSSL: ```bash openssl s_client \ -connect example.com:443 \ -servername example.com ``` The `-servername` option ensures that the hostname is included during TLS negotiation. Troubleshooting [#troubleshooting] The domain is waiting for DNS [#the-domain-is-waiting-for-dns] Confirm that: * the hostname uses the exact DNS records shown by Easel * conflicting records have been removed * the record is configured on the authoritative DNS provider * a DNS proxy is not hiding the expected value * enough time has passed for the DNS change to propagate Do not add duplicate A, AAAA, or CNAME records unless the Easel configuration explicitly requires them. The certificate is still provisioning [#the-certificate-is-still-provisioning] Confirm that the domain has completed DNS verification. Also check for: * restrictive CAA records * conflicting records for the hostname * an upstream proxy * DNSSEC configuration errors * a recent domain transfer or nameserver change * certificate-authority rate limits If the domain status reports a configuration error, correct the underlying DNS issue before retrying provisioning. The browser shows a certificate warning [#the-browser-shows-a-certificate-warning] Check that: * the browser is opening the exact hostname configured in Easel * the hostname points to the correct Easel project * certificate provisioning is complete * an upstream proxy is not serving a different certificate * the local network or security software is not intercepting TLS A certificate for `example.com` does not automatically validate an unrelated hostname such as `internal.example.com`. HTTP does not redirect to HTTPS [#http-does-not-redirect-to-https] Confirm that the hostname is active and associated with the project. Also check for a custom redirect or upstream proxy that intercepts the HTTP request before it reaches Easel. The site redirects repeatedly [#the-site-redirects-repeatedly] Redirect loops commonly occur when both Easel and an upstream proxy independently force HTTPS while disagreeing about the original request protocol. Configure the upstream proxy to connect to Easel over HTTPS and preserve the forwarded protocol correctly. Also inspect application-level redirects and framework configuration. The certificate does not renew [#the-certificate-does-not-renew] Check that: * the domain still points to Easel * required DNS records remain present * CAA records still permit issuance * the hostname remains attached to the project * an upstream proxy does not prevent validation Easel renews managed certificates automatically when the domain remains correctly configured. OAuth or authentication fails after enabling HTTPS [#oauth-or-authentication-fails-after-enabling-https] Update callback and origin allowlists to use the production HTTPS URL. For example: ```text https://example.com/auth/callback ``` Also verify cookie attributes, trusted origins, redirect URIs, and proxy-header handling. Related documentation [#related-documentation] * [Domains](/docs/domains) * [HTTPS and TLS for custom domains](/docs/domains/tls) * [CDN overview](/docs/cdn) * [Request and response headers](/docs/cdn/headers) * [Security](/docs/security) * [Deployment URLs](/docs/deployments/urls) # CDN Every Easel deployment is served through a global delivery network that handles security, routing, caching, compression, and HTTPS before a request reaches your application. Static assets can be delivered without invoking compute, while cacheable dynamic responses can be reused across requests to reduce latency and function usage. How requests are handled [#how-requests-are-handled] Easel can complete a request through a firewall rule, redirect, cached response, or static deployment asset without invoking application code. Security and routing [#security-and-routing] Easel first applies the rules that determine whether and where a request should continue. These can include: * Web Application Firewall rules * Attack Mode challenges * redirects and rewrites * framework middleware * static and dynamic route matching Requests can be blocked, challenged, redirected, rewritten, or passed to the matching application route. See [Routing](/docs/delivery/routing) and [Redirects and rewrites](/docs/delivery/redirects-and-rewrites). Cached responses [#cached-responses] For eligible requests, Easel can reuse a previously generated response from the CDN cache. A cache hit avoids invoking application code or contacting an external data source. Cache behavior is controlled through standard HTTP cache directives, with support for shared-cache expiration. Easel exposes the result through the `X-Easel-Cache` response header. ```http X-Easel-Cache: HIT ``` Static and dynamic routes [#static-and-dynamic-routes] When a cached response is not available, Easel serves the resource matched by the deployment. Static files such as HTML, JavaScript, CSS, fonts, and images are served as deployment assets without invoking an Easel Function. Server-rendered pages, API routes, server actions, and other dynamic handlers run in Easel Functions. Dynamic responses can also be cached when they include an eligible cache policy. Response delivery [#response-delivery] Before returning a response, Easel applies the relevant delivery behavior. Depending on the request and response, this can include: * CDN caching * Brotli or gzip compression * HTTPS and security headers * custom response headers * request and cache diagnostics What you get automatically [#what-you-get-automatically] Every Easel deployment includes: Delivery network [#delivery-network] Requests enter through Easel’s delivery network and are routed to the appropriate cached asset, deployment resource, or application function. Function compute regions are separate from edge delivery locations. See [Regions](/docs/reference/regions). Static asset delivery [#static-asset-delivery] Files produced during the build are deployed as versioned assets and served without application compute. Dynamic response caching [#dynamic-response-caching] Applications can cache server-rendered pages, API responses, and other HTTP responses using standard cache headers. Cache revalidation [#cache-revalidation] Cached content can expire automatically or be invalidated through cache tags and supported framework APIs. Compression [#compression] Eligible responses are compressed automatically. Easel prefers Brotli when supported by the client and falls back to gzip. HTTPS [#https] Easel provisions and renews TLS certificates for deployment URLs and configured custom domains. Request diagnostics [#request-diagnostics] Response headers expose request IDs, cache results, and other supported delivery metadata for debugging. Choose a guide [#choose-a-guide] | Goal | Guide | | ---------------------------------------- | --------------------------------------------------------------- | | Understand request matching | [Routing](/docs/delivery/routing) | | Configure redirects and rewrites | [Redirects and rewrites](/docs/delivery/redirects-and-rewrites) | | Control how responses are cached | [Caching](/docs/cdn/caching) | | Refresh content before it expires | [Revalidation and purging](/docs/cdn/revalidation) | | Understand how build files are delivered | [Static files](/docs/cdn/static-files) | | Configure and verify compression | [Compression](/docs/cdn/compression) | | Understand certificates and HTTPS | [HTTPS and TLS](/docs/cdn/https-and-tls) | | Inspect platform headers | [Request and response headers](/docs/cdn/headers) | | Diagnose unexpected CDN behavior | [Troubleshooting](/docs/cdn/troubleshooting) | CDN cache and Runtime Cache [#cdn-cache-and-runtime-cache] Easel provides two separate caching systems. | CDN cache | Runtime Cache | | ----------------------------------------- | --------------------------------------------- | | Stores complete HTTP responses | Stores application values | | Used before invoking application code | Accessed from inside an Easel Function | | Controlled through response cache headers | Controlled through the Runtime Cache API | | Best for pages, assets, and API responses | Best for database results and computed values | An application can use both. A server-rendered route might read data from the Runtime Cache and then place the completed HTTP response in the CDN cache. See [Runtime Cache](/docs/runtime-cache) for application-level caching. Related documentation [#related-documentation] * [Functions](/docs/functions) * [Security](/docs/security) * [Runtime Cache](/docs/runtime-cache) # Revalidation and purging Cached content normally remains available until its configured lifetime expires. Easel also provides mechanisms to refresh or invalidate cached content before that happens. Use revalidation when content should be regenerated while preserving normal cache behavior. Use purging when an existing cached entry should no longer be served. Choose an approach [#choose-an-approach] | Goal | Recommended approach | | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------- | | Refresh content after a fixed interval | Set `s-maxage` or framework revalidation | | Accept a sync refresh after TTL | Set `s-maxage` without `stale-while-revalidate`; the next request after expiry waits for a fresh response | | Serve stale while refreshing in the background | Set `s-maxage` with an explicit `stale-while-revalidate` window | | Invalidate related content across many URLs | Use cache tags | | Refresh a specific framework route | Use the framework’s revalidation API | | Clear cached content for a deployment or project | Use project cache controls or ISR / deployment cache clear in the dashboard | | Invalidate application values | Use Runtime Cache tags or keys | Expiration, revalidation, and purging [#expiration-revalidation-and-purging] These mechanisms solve different problems. Expiration [#expiration] Expiration happens automatically when a cached entry reaches the end of its configured lifetime. ```http CDN-Cache-Control: public, s-maxage=300 ``` This response remains fresh for five minutes. After that, Easel must refresh it before it can be served as a fresh response again. Revalidation [#revalidation] Revalidation refreshes stale content by running the route or contacting the origin again. The resulting response can replace the previous cached entry. Revalidation may happen: * after the configured TTL expires (the next matching request waits for a fresh response) * through a supported framework API * after a matching cache tag is invalidated Purging [#purging] Purging invalidates cached content immediately instead of waiting for its TTL to expire. Depending on the operation, a purge can target: * a cache tag * a deployment * a project environment * a supported framework path After a purge, the next matching request normally produces a cache miss and regenerates or retrieves the response. Time-based revalidation [#time-based-revalidation] Set a shared-cache lifetime with `s-maxage`: ```http CDN-Cache-Control: public, s-maxage=300 ``` After five minutes, the entry is no longer fresh. The next matching request waits while Easel obtains an updated response. Use this approach when content changes predictably and a fixed refresh interval is sufficient. Stale-while-revalidate [#stale-while-revalidate] Set an explicit `stale-while-revalidate` window when you want Easel to serve the expired entry while refreshing it in the background: ```http CDN-Cache-Control: public, s-maxage=60, stale-while-revalidate=300 ``` With this policy: * the response is fresh for 60 seconds (`X-Easel-Cache: HIT`) * for the next 300 seconds, Easel may serve the stale body (`X-Easel-Cache: STALE`) while refreshing in the background * after that window, the next request waits for a sync refetch Omit the directive when you want a sync refresh after the fresh TTL. Easel does not invent a default stale window when the directive is absent. Frameworks such as Next.js may emit `stale-while-revalidate` for you. Cache tags [#cache-tags] Cache tags associate related cached content with a shared identifier. A single tag can represent content used by: * multiple URLs * CDN response entries * Runtime Cache values * framework-generated pages * framework data-cache entries For example, a product update may affect: * `/products` * `/products/widget` * `/api/products/widget` * a cached product query * a server-rendered category page Tagging each related entry with `product:widget` allows them to be invalidated together. Tag a response [#tag-a-response] Add one or more tags to a cacheable HTTP response: ```http Cache-Tag: products product:widget category:tools CDN-Cache-Control: public, s-maxage=3600 ``` Tags are separated by spaces. Easel also recognizes compatible tag headers emitted by supported frameworks and adapters, including: ```http x-next-cache-tags: products,product:widget ``` and: ```http Surrogate-Key: products product:widget ``` When multiple supported tag headers are present, Easel associates their normalized tag values with the cached response. Tag naming [#tag-naming] Use stable, predictable tag names. Good examples: ```text products product:widget category:tools organization:acme article:1248 ``` Avoid tags that include: * timestamps * request IDs * session IDs * random values * unbounded user input High-cardinality tags reduce their usefulness and can make invalidation harder to reason about. A common convention is: ```text resource-type:identifier ``` For example: ```text product:123 customer:acct_42 collection:featured ``` Use broader tags alongside specific tags when updates can affect both one item and a collection. ```http Cache-Tag: products product:123 ``` Purge by tag [#purge-by-tag] Purging a tag invalidates every supported cache entry associated with that tag. Conceptually: ```text Purge: product:123 Invalidates: - /products/123 - /api/products/123 - category pages containing product 123 - tagged Runtime Cache values - supported framework cache entries ``` Purging a tag does not require knowing every affected URL. This makes tags suitable for content-management systems, product catalogs, dashboards, and other applications where one update can affect several views. URL-level purge [#url-level-purge] URL-level `PURGE` is not a public product API. To invalidate cached content, use: * cache tags * framework revalidation APIs such as `revalidatePath` or `revalidateTag` * ISR or deployment cache clear controls in the dashboard Do not depend on purging a single URL as a customer-facing capability. Framework revalidation [#framework-revalidation] Supported frameworks can expose native revalidation APIs. Easel maps these operations onto the relevant platform cache layers. Next.js [#nextjs] Revalidate a path [#revalidate-a-path] Use `revalidatePath` to refresh a route: ```ts "use server"; import { revalidatePath } from "next/cache"; export async function updateProduct() { await saveProduct(); revalidatePath("/products"); } ``` This invalidates the supported cached output associated with the path. Revalidate a tag [#revalidate-a-tag] Attach a tag to cached data: ```ts const product = await fetch("https://api.example.com/products/123", { next: { tags: ["product:123"], }, }); ``` Invalidate it after an update: ```ts "use server"; import { revalidateTag } from "next/cache"; export async function updateProduct() { await saveProduct(); revalidateTag("product:123"); } ``` Supported Next.js route, data, and response-cache entries can participate in the same tag invalidation flow. See [Next.js](/docs/framework-guides/nextjs) for framework-specific caching behavior. Other frameworks [#other-frameworks] Frameworks such as Nuxt, SvelteKit, TanStack Start, and React Router may express caching through response headers, framework adapters, or runtime APIs. When a framework produces standard cache directives or supported tag metadata, Easel applies the corresponding CDN behavior. See the relevant [framework guide](/docs/framework-guides) for supported framework-specific behavior. Runtime Cache invalidation [#runtime-cache-invalidation] The Runtime Cache stores application values used inside Easel Functions. It is separate from the CDN cache, which stores complete HTTP responses. A route can use both. For example: 1. A function reads product data from the Runtime Cache. 2. It renders an HTTP response. 3. Easel stores the completed response in the CDN cache. 4. Both entries are associated with `product:123`. 5. Purging that tag invalidates the underlying value and the rendered response. This avoids serving a newly rendered page from stale application data. Use the Runtime Cache API to assign tags when storing application values. ```ts await cache.set("product:123", product, { tags: ["products", "product:123"], }); ``` The exact Runtime Cache API depends on the SDK version. See [Runtime Cache](/docs/runtime-cache). Purge scope [#purge-scope] Cache operations should be scoped to the intended environment. Production, preview, and development deployments use separate deployment contexts. A purge should not unintentionally remove cached content from unrelated deployments. When initiating a purge, identify the relevant: * project * environment * deployment, when applicable * tag A production purge should not normally invalidate independent preview deployments unless the purge explicitly targets shared application cache data. Purge propagation [#purge-propagation] Easel records invalidation state across supported cache layers. After a purge begins: 1. matching entries are marked invalid 2. later requests stop treating them as fresh 3. the next request regenerates or retrieves updated content 4. the replacement response can be cached under the new state A purge does not guarantee that the replacement content already exists. It guarantees that invalidated content is no longer considered a valid fresh response. Applications should ensure that the underlying data source is updated before initiating the purge. Avoid purge races [#avoid-purge-races] Update the source of truth before invalidating its cached representations. Recommended order: ```text 1. Update database or content source 2. Confirm the update succeeded 3. Purge the relevant tag or path 4. Allow the next request to regenerate content ``` Do not purge first and update the database afterward. A request arriving between those operations could regenerate the cache using the old data. For multi-step updates, complete the transaction before triggering invalidation. Purging and browser caches [#purging-and-browser-caches] Easel can invalidate its shared cache, but it cannot directly remove a response already stored in a visitor’s browser. Consider this policy: ```http Cache-Control: public, max-age=3600 CDN-Cache-Control: public, s-maxage=86400 ``` Even after the Easel cache is purged, a browser may continue reusing its copy for up to one hour. Use short browser lifetimes when content must respond quickly to CDN purges: ```http Cache-Control: public, max-age=0, must-revalidate CDN-Cache-Control: public, s-maxage=86400 ``` This allows Easel to cache the response for one day while browsers check for a current response on each visit. Purging personalized content [#purging-personalized-content] User-specific and authenticated responses should generally not be stored in the shared CDN cache. Use: ```http Cache-Control: private, no-store ``` When a response is correctly marked private, shared-cache purging is unnecessary because Easel does not reuse it across visitors. Do not use cache tags as a substitute for appropriate privacy directives. Common patterns [#common-patterns] Product update [#product-update] Tag product pages, API responses, and cached application data: ```http Cache-Tag: products product:123 ``` After updating the product: ```text Purge tag: product:123 ``` Purge `products` as well when the update affects collection pages. Publishing an article [#publishing-an-article] Before publication, keep the response private or uncached. After publication: ```http Cache-Tag: articles article:842 CDN-Cache-Control: public, s-maxage=3600 ``` When the article is edited: ```text Purge tag: article:842 ``` Regenerating a landing page [#regenerating-a-landing-page] For a route that changes periodically: ```http CDN-Cache-Control: public, s-maxage=300 ``` No explicit purge is needed unless an update must appear before the five-minute lifetime expires. After the lifetime ends, the next request waits for a fresh response. Organization-specific content [#organization-specific-content] Include the organization identifier in the tag: ```http Cache-Tag: organization:acme organization:acme:dashboard ``` Purge only the affected organization: ```text Purge tag: organization:acme ``` Do not use a global tag when the update applies to only one organization. Debugging revalidation [#debugging-revalidation] Inspect the response: ```bash curl -I https://example.com/products ``` Relevant headers include: ```http X-Easel-Cache: HIT CDN-Cache-Control: public, s-maxage=300 Cache-Tag: products ``` `X-Easel-Cache` is authoritative for whether Easel reused a cached response. An `Age` header may also appear on some responses, but it can be absent and is not required to interpret cache status. After a successful purge, the next request commonly returns: ```http X-Easel-Cache: MISS ``` A later request may return: ```http X-Easel-Cache: HIT ``` Content remains cached after a purge [#content-remains-cached-after-a-purge] Check that: * the purged tag exactly matches the tag on the response * the purge targeted the correct project and environment * the request uses the expected hostname and query string * the browser is not reusing its own cached response * a framework is not serving content from another cache layer * the updated response was not regenerated using stale application data A page regenerates with old content [#a-page-regenerates-with-old-content] The CDN entry may have been purged while the Runtime Cache or another data cache remained fresh. Use a shared tag across the rendered response and its underlying cached values, or invalidate both layers explicitly. Every request regenerates the response [#every-request-regenerates-the-response] Check whether: * the new response includes a positive shared-cache lifetime * the response contains `Set-Cookie` * the request contains `Authorization` * the route returns a cacheable status * the framework marks the route as dynamic * the cache is being purged repeatedly An outdated response remains visible [#an-outdated-response-remains-visible] After the shared-cache lifetime ends, Easel does not serve the expired entry as a cache hit. The next matching request waits for a fresh response. If an outdated body is still visible, verify that: * the browser is not serving its own cached copy * a framework data cache or Runtime Cache entry was not left fresh * the purge or revalidation targeted the correct tag, path, and environment Operational guidance [#operational-guidance] Prefer expiration when: * updates can appear within a predictable interval * occasional staleness is acceptable * the content is requested frequently enough to regenerate naturally Prefer cache tags when: * one update affects several URLs or cache layers * content relationships are known by the application * updates must appear before the normal TTL expires Prefer framework revalidation or dashboard cache clear when: * a specific route or deployment must refresh without a tag model * you need ISR or project-level invalidation Avoid project-wide purges during normal application updates. Broad purges reduce cache hit rates and can cause many routes to regenerate simultaneously. Related documentation [#related-documentation] * [Caching](/docs/cdn/caching) * [Static files](/docs/cdn/static-files) * [CDN overview](/docs/cdn) * [Runtime Cache](/docs/runtime-cache) * [Next.js](/docs/framework-guides/nextjs) * [Request and response headers](/docs/cdn/headers) # Static files Files produced during your build are deployed as versioned static assets and served through Easel without invoking application compute. Static files are well suited for JavaScript bundles, stylesheets, images, fonts, generated HTML, and files from your project’s public directory. How static files are deployed [#how-static-files-are-deployed] During a deployment, Easel identifies the files produced by your framework or build command and publishes them with that deployment. Examples include: * JavaScript and CSS bundles * images and fonts * source maps * files from `public`, `static`, or equivalent directories * prerendered HTML * framework-generated client assets * downloadable files Requests for these files are served directly and do not invoke an Easel Function. Deployment versioning [#deployment-versioning] Static files belong to a specific deployment. A preview deployment and a production deployment can contain different versions of the same path without overwriting one another. For example: ```text Preview deployment: /assets/app.js → preview build Production deployment: /assets/app.js → production build ``` Promoting or publishing a new deployment updates which deployment serves the production domain. Existing immutable deployment URLs continue to reference their original files. This allows Easel to switch deployments without partially updating a site. Content-hashed assets [#content-hashed-assets] Modern frameworks commonly include a content hash in generated filenames: ```text /assets/app.9f3ac21.js /assets/styles.701ad8c.css /_next/static/chunks/4728.abf0981.js ``` When the file contents change, the filename changes as well. These files can safely use long cache lifetimes because an updated deployment references a new URL instead of replacing the existing asset at the same path. A typical policy is: ```http Cache-Control: public, max-age=31536000, immutable ``` Framework-generated assets may receive this policy automatically. Stable URLs [#stable-urls] Some files retain the same path when their contents change: ```text /index.html /robots.txt /manifest.webmanifest /logo.svg ``` These files should generally use shorter cache lifetimes or require revalidation so visitors can receive updated versions after a deployment. For example: ```http Cache-Control: public, max-age=0, must-revalidate ``` Avoid applying a one-year immutable policy to a stable URL unless the file is guaranteed never to change. Static HTML [#static-html] Pages generated during the build can be served as static HTML without invoking a function. This includes: * prerendered framework routes * statically generated pages * HTML produced by a static-site generator * plain HTML files in the output directory Static HTML and client assets may use different caching policies. A framework can give HTML a short or revalidated lifetime while assigning long immutable lifetimes to hashed JavaScript and CSS files. Output directories [#output-directories] Easel detects the conventional output directory for supported frameworks. Common examples include: | Build tool or framework | Typical output | | ----------------------- | ---------------------------------- | | Vite | `dist` | | Astro | `dist` | | static export | framework-defined export directory | | plain static site | configured project directory | For full-stack frameworks, the static output may be only one part of the deployment. Easel can deploy static assets alongside server-rendered routes and functions. You can override the build command, project root, or output configuration in the project settings when automatic detection is not appropriate. Public directories [#public-directories] Files placed in a framework’s public directory are generally copied into the deployment output without changing their names. Examples include: ```text public/favicon.ico → /favicon.ico public/robots.txt → /robots.txt public/images/logo.svg → /images/logo.svg ``` Because these URLs do not usually contain content hashes, choose their cache policies carefully. Custom response headers [#custom-response-headers] You can configure headers for static files through supported framework configuration or Easel routing configuration. For example, a content-hashed asset can use: ```http Cache-Control: public, max-age=31536000, immutable ``` A stable configuration file might use: ```http Cache-Control: public, max-age=300 ``` A file that must always be checked for updates can use: ```http Cache-Control: public, max-age=0, must-revalidate ``` Custom headers should not weaken the cache safety of framework-generated assets unless you have a specific reason to override the defaults. CDN behavior [#cdn-behavior] Static files are served through the Easel CDN. A request may be served from an existing cached copy or retrieved from the deployment’s asset storage and then cached for later requests. The response can expose the cache result: ```http X-Easel-Cache: HIT ``` Serving a static file does not invoke an application function, regardless of whether the CDN reports `HIT` or `MISS`. A cache miss for a static file means Easel had to retrieve the deployment asset. It does not mean the application was executed. Compression [#compression] Eligible static files are compressed automatically when the visitor supports Brotli or gzip. Common compressible files include: * HTML * CSS * JavaScript * JSON * SVG * XML * text files * WebAssembly Already compressed formats such as JPEG, PNG, WebP, AVIF, ZIP, and most video files are generally sent without additional compression. See [Compression](/docs/cdn/compression). Content types [#content-types] Easel determines the response `Content-Type` from the file extension and deployment metadata. Examples include: ```http Content-Type: text/html; charset=utf-8 Content-Type: text/css; charset=utf-8 Content-Type: application/javascript; charset=utf-8 Content-Type: image/svg+xml Content-Type: font/woff2 ``` Use the correct file extension whenever possible. A missing or incorrect content type can affect browser rendering, compression, and security behavior. Range requests [#range-requests] Range requests bypass shared CDN caching. Partial content (`206`) for deployment static assets is not currently supported. Clients that send a `Range` header for those assets should not expect a successful partial-content response from Easel’s CDN path. Single-page applications [#single-page-applications] Client-rendered single-page applications often need unknown routes to return the application’s HTML entry point. For example: ```text /dashboard/settings ``` may need to serve: ```text /index.html ``` Configure an SPA fallback through supported routing or framework configuration. The fallback should apply only to application routes. Requests for missing assets such as `/assets/missing.js` should still return `404`. See the [Vite guide](/docs/framework-guides/vite). Custom error pages [#custom-error-pages] Static deployments can provide custom error documents where supported. Common examples include: ```text 404.html 500.html ``` The exact convention depends on the framework and routing configuration. A custom error page can remain a static asset even when it is displayed for a failed route. Preview and production deployments [#preview-and-production-deployments] Preview and production deployments use separate deployment versions. This means: * preview assets do not overwrite production assets * the same path can contain different files in different deployments * promoting a deployment changes the production version as a unit * rollback can restore a previous deployment and its assets Do not use a preview deployment URL as a permanent production asset origin. Preview deployments may be deleted or access-controlled independently from production. Asset URLs [#asset-urls] Prefer root-relative URLs for files served from the same deployment: ```html Easel ``` Framework build tools usually rewrite asset references automatically. When deploying beneath a path prefix or using an external asset host, configure the framework’s base path or asset prefix accordingly. Examples include: * Vite `base` * Next.js `basePath` and `assetPrefix` * Nuxt `app.baseURL` * framework-specific public path configuration See the relevant [framework guide](/docs/framework-guides). Missing files [#missing-files] A request for a file that does not exist in the selected deployment returns `404` unless a routing rule or SPA fallback matches it. Check: * the build output contains the file * the filename uses the correct capitalization * the public path is correct * the project root is configured correctly * the output directory is correct * the asset URL includes the expected base path * the deployment being requested is the intended one File paths are case-sensitive. For example: ```text /images/Logo.svg ``` and: ```text /images/logo.svg ``` are different paths. Large files [#large-files] Static files remain subject to project and platform limits. For large downloads, video, backups, or user-uploaded content, use object storage rather than including the files in every application deployment. Application deployments are best suited for assets that are: * generated or versioned with the application * required by the application interface * reasonably sized * updated through normal deployments User-generated files should generally be stored separately from deployment assets. Deployment assets and object storage [#deployment-assets-and-object-storage] Deployment assets are immutable files associated with an application release. Object storage is intended for data that changes independently from application deployments. | Deployment assets | Object storage | | --------------------------- | ----------------------------------------- | | Created during the build | Created or updated at runtime | | Versioned with a deployment | Managed independently | | Read-only after deployment | Can support writes and deletes | | Best for application files | Best for uploads and durable content | | Released through a deploy | Updated through application logic or APIs | Do not write user data to the deployment filesystem or a function’s local filesystem. Debugging static files [#debugging-static-files] Inspect the response headers: ```bash curl -I https://example.com/assets/app.js ``` Useful headers include: ```http Content-Type: application/javascript; charset=utf-8 Cache-Control: public, max-age=31536000, immutable Content-Encoding: br X-Easel-Cache: HIT ``` A file returns 404 [#a-file-returns-404] Verify that: * it exists in the build output * the output directory is configured correctly * the URL uses the correct capitalization * the framework base path matches the deployed URL * a rewrite is not sending the request elsewhere A file has the wrong content type [#a-file-has-the-wrong-content-type] Check the extension and whether a custom header rule overrides `Content-Type`. Avoid serving JavaScript or CSS through extensionless URLs unless the response type is explicitly configured. Visitors receive an old file [#visitors-receive-an-old-file] Check whether the file uses a stable URL with a long browser cache lifetime. For files that change, use content-hashed filenames or shorter browser caching. Purging Easel’s CDN cache does not remove a file already stored in a visitor’s browser. An asset request invokes a function [#an-asset-request-invokes-a-function] Check whether: * a broad rewrite captures the asset path * an SPA fallback also matches files * the framework routes the path dynamically * the file is absent from the static build output Exclude asset directories from broad rewrites and middleware matchers where appropriate. Related documentation [#related-documentation] * [CDN overview](/docs/cdn) * [Caching](/docs/cdn/caching) * [Revalidation and purging](/docs/cdn/revalidation) * [Compression](/docs/cdn/compression) * [Framework guides](/docs/framework-guides) # CDN troubleshooting Use this guide when a request is not being cached, stale content remains visible, compression is missing, HTTPS is failing, or the response does not follow the expected route. Start by inspecting the response headers: ```bash curl -I https://example.com/path ``` Useful headers include: ```http X-Easel-Id: req_... X-Easel-Cache: HIT Cache-Control: public, max-age=0, must-revalidate CDN-Cache-Control: public, s-maxage=300 Content-Encoding: br ``` `Age` may also appear on some responses. Treat `X-Easel-Cache` as the authoritative cache-result header. Absence of `Age` is not an error. Run the same request more than once when testing cache behavior: ```bash curl -I https://example.com/path curl -I https://example.com/path ``` A cacheable response commonly returns `MISS` on the first request and `HIT` on a later request. Identify the request path [#identify-the-request-path] The fastest way to diagnose a CDN issue is to determine how Easel handled the request. | Observation | Likely path | | ------------------------------ | --------------------------------------------------------------------- | | Firewall or challenge response | Security rules completed the request | | Redirect response | A routing or application redirect matched | | `X-Easel-Cache: HIT` | A fresh cached response was served | | `X-Easel-Cache: STALE` | A stale cached response was served while refreshing in the background | | `X-Easel-Cache: MISS` | Easel retrieved or generated a new response | | `X-Easel-Cache: BYPASS` | The request or response was not cacheable | | No function log | The request may have been cached, static, redirected, or blocked | | Function log present | Application compute handled the request | Use `X-Easel-Id` to correlate the client response with Easel logs and traces. The response is not cached [#the-response-is-not-cached] X-Easel-Cache is BYPASS [#x-easel-cache-is-bypass] `BYPASS` means Easel did not attempt to store or reuse the response. Check for these common causes: * The request method is not `GET` or `HEAD`. * The request contains `Authorization`. * The request contains `Range`. * The response contains `Set-Cookie`. * The response is marked `private`. * The response contains `no-store` or `no-cache`. * The response does not include a positive shared-cache lifetime. * The response status is not cacheable. Inspect the relevant headers: ```bash curl -I https://example.com/path ``` A basic cacheable policy looks like: ```http Cache-Control: public, max-age=0, must-revalidate CDN-Cache-Control: public, s-maxage=300 ``` If the response includes: ```http Set-Cookie: session=... ``` Easel bypasses shared caching. Separate cookie-setting behavior from cacheable content when possible. X-Easel-Cache is always MISS [#x-easel-cache-is-always-miss] A repeated `MISS` means the request is eligible for cache processing, but Easel is not finding a reusable matching entry. Check that repeated requests use the same: * hostname * path * query string * deployment * request method * supported `Vary` values These URLs may create different cache entries: ```text /products /products?page=1 /products?page=2 ``` Also check whether: * the TTL is too short * the response changes its `Vary` header * a deployment occurs between requests * the route is being purged repeatedly * the framework marks the route as dynamic * the response exceeds a platform cache limit The second request still returns MISS [#the-second-request-still-returns-miss] Wait long enough for the first response to complete, then repeat the exact request. Use: ```bash curl -sS -D - -o /dev/null https://example.com/path curl -sS -D - -o /dev/null https://example.com/path ``` Confirm that the first response actually includes a positive CDN lifetime. For example: ```http CDN-Cache-Control: public, s-maxage=300 ``` A browser extension, authentication layer, cookie, or upstream proxy may also alter otherwise identical requests. The response has Cache-Control, but is not cached [#the-response-has-cache-control-but-is-not-cached] Easel uses cache headers in this order: 1. `Vercel-CDN-Cache-Control` 2. `CDN-Cache-Control` 3. `Cache-Control` A higher-priority header may override the policy you are inspecting. For example: ```http Vercel-CDN-Cache-Control: no-store CDN-Cache-Control: public, s-maxage=3600 ``` The response is not cached because the Vercel-compatible header takes precedence. Inspect all three headers together. Cached content is stale [#cached-content-is-stale] Content remains old after a purge [#content-remains-old-after-a-purge] Check that: * the purge targeted the correct project * the correct environment was selected * the tag or URL exactly matches the cached entry * the browser is not serving its own cached copy * the underlying application data was updated before the purge * another cache layer still contains old data * the route regenerated successfully after invalidation A CDN purge cannot remove a response already stored in the visitor’s browser. Compare: ```http Cache-Control: public, max-age=3600 CDN-Cache-Control: public, s-maxage=86400 ``` The browser may reuse its copy for one hour even after Easel’s shared cache is purged. For content that must reflect CDN invalidation quickly, use: ```http Cache-Control: public, max-age=0, must-revalidate CDN-Cache-Control: public, s-maxage=86400 ``` A page regenerates with old data [#a-page-regenerates-with-old-data] The CDN response may have been invalidated while the application continued using stale Runtime Cache or framework data-cache entries. Use a shared cache tag across: * the rendered HTTP response * Runtime Cache values * framework data-cache entries * related routes Alternatively, invalidate each layer explicitly. Update the source of truth before initiating invalidation: ```text 1. Update the database or content source 2. Confirm the update succeeded 3. Purge the relevant tag or route 4. Allow the next request to regenerate content ``` Purging before the update completes can cause the route to regenerate using old data. A purge affects less content than expected [#a-purge-affects-less-content-than-expected] A URL purge targets one cache key. It does not automatically invalidate related URLs. Purging: ```text /products/123 ``` does not necessarily purge: ```text /products /api/products/123 /products/123?currency=USD ``` Use a shared tag such as: ```http Cache-Tag: products product:123 ``` when one data change affects multiple representations. Browser caching differs from CDN caching [#browser-caching-differs-from-cdn-caching] The browser and Easel can use different cache lifetimes. ```http Cache-Control: public, max-age=60 CDN-Cache-Control: public, s-maxage=3600 ``` This means: * the browser may reuse its copy for 60 seconds * Easel may reuse its copy for one hour When debugging, test with `curl` to avoid relying on the browser’s local cache. You can also disable the browser cache temporarily in browser developer tools. Do not use a cache-busting query parameter as the first diagnostic step. A changed query string may create a different CDN cache key and hide the original problem. A static file returns 404 [#a-static-file-returns-404] Check that: * the file exists in the framework’s public or output directory * the filename uses the correct letter casing * the build includes the file * the route does not conflict with a rewrite or function * the configured root directory is correct * the expected deployment is receiving the request Production filesystems are case-sensitive. For example: ```text /logo.svg /Logo.svg ``` may refer to different files. Inspect the build output and preview deployment before promoting the change to production. An old static asset is still being served [#an-old-static-asset-is-still-being-served] Confirm that the page references the current asset URL. Content-hashed files should receive a new filename when their contents change: ```text /assets/app.9f3ac21.js /assets/app.32bca78.js ``` If HTML still references the old asset, the problem may be stale HTML rather than stale asset storage. Check: * the HTML cache policy * deployment promotion status * service-worker caches * browser caches * hardcoded asset URLs * upstream proxies A service worker can continue serving old files independently of Easel’s CDN cache. A redirect is unexpected [#a-redirect-is-unexpected] Inspect the response without following redirects: ```bash curl -I https://example.com/path ``` Look for: ```http HTTP/2 301 Location: https://example.com/other-path ``` Possible redirect sources include: * HTTP-to-HTTPS enforcement * project routing rules * framework configuration * middleware * application code * canonical-domain redirects * an upstream proxy Follow the complete redirect chain with: ```bash curl -IL https://example.com/path ``` Review each `Location` header to identify where the chain begins. The site is stuck in a redirect loop [#the-site-is-stuck-in-a-redirect-loop] Redirect loops commonly happen when two layers independently enforce the same rule. Check for combinations such as: * Easel and an upstream proxy both forcing HTTPS * apex and `www` redirects pointing at each other * middleware redirecting back to its own matcher * application code disagreeing with forwarded protocol headers * locale redirects repeatedly adding or removing a path segment Use: ```bash curl -IL --max-redirs 10 https://example.com ``` Inspect each hop. When a proxy sits in front of Easel, ensure that it connects to Easel over HTTPS and preserves the original protocol correctly. A firewall blocks a valid request [#a-firewall-blocks-a-valid-request] Use the returned request ID and any firewall diagnostic headers to inspect the matching security event. Possible causes include: * a custom WAF rule * managed-rule detection * an IP or network restriction * rate limiting * Attack Mode * bot or abuse detection * an upstream proxy causing many visitors to share one apparent IP Reproduce the request with the same: * URL * method * headers * body * authentication state Do not disable broad security protections permanently to resolve one false positive. Narrow the matching rule or add a scoped exception. See [Security](/docs/security). A function was not invoked [#a-function-was-not-invoked] The request may have completed before reaching application compute. Common causes include: * a fresh CDN cache hit * a static deployment asset * a redirect * a firewall block * a challenge response * middleware returning a response * the request reaching a different route than expected Check: ```http X-Easel-Cache X-Easel-Id ``` Then inspect project request logs rather than only function logs. A missing function invocation does not necessarily indicate that Easel dropped the request. A function runs when a cache hit was expected [#a-function-runs-when-a-cache-hit-was-expected] Check that: * the response includes a positive CDN TTL * the route does not set cookies * the request does not include authentication * query strings are identical * `Vary` values are stable * no purge occurs between requests * the framework has not opted the route into dynamic rendering * middleware does not alter the cache key or response * the response status is cacheable For Next.js, request-time APIs such as cookies or headers can make a route dynamic depending on the framework version and route configuration. See the relevant [framework guide](/docs/framework-guides). Compression is missing [#compression-is-missing] Inspect the request and response: ```bash curl -I \ -H 'Accept-Encoding: br, gzip' \ https://example.com/app.js ``` Look for: ```http Content-Encoding: br Vary: Accept-Encoding ``` Compression may not be applied when: * the client does not advertise Brotli or gzip * the response type is not compressible * the response is below the minimum size * the response is already encoded * compression would not reduce the response size * the request is a range request * the response does not include a recognized `Content-Type` * streaming behavior prevents buffering or transformation Compare the compressed and uncompressed response sizes: ```bash curl -sS \ -H 'Accept-Encoding: identity' \ -o /dev/null \ -w '%{size_download}\n' \ https://example.com/app.js curl -sS \ --compressed \ -o /dev/null \ -w '%{size_download}\n' \ https://example.com/app.js ``` See [Compression](/docs/cdn/compression). HTTPS is not active [#https-is-not-active] Check the custom-domain status in Easel. Common causes include: * missing DNS records * conflicting DNS records * DNS changes that have not propagated * a restrictive CAA record * a proxied DNS record * DNSSEC errors * the domain pointing to another project * certificate issuance rate limits Verify DNS independently: ```bash dig example.com dig CNAME www.example.com dig CAA example.com ``` Use the exact DNS values displayed in Easel. Do not repeatedly remove and re-add the domain while DNS is still propagating. The browser shows a certificate warning [#the-browser-shows-a-certificate-warning] Confirm that: * the exact hostname is configured * certificate provisioning is complete * DNS points to the correct Easel project * an upstream proxy is not serving another certificate * the device clock is correct * local security software is not intercepting TLS Inspect the certificate: ```bash openssl s_client \ -connect example.com:443 \ -servername example.com ``` A certificate for `example.com` does not automatically cover every subdomain. See [HTTPS and TLS](/docs/cdn/https-and-tls). HTTP does not redirect to HTTPS [#http-does-not-redirect-to-https] Check: ```bash curl -I http://example.com ``` The response should redirect to the HTTPS URL. If it does not, confirm that: * the hostname is active * the request reaches Easel * an upstream proxy is not intercepting HTTP * no conflicting routing rule handles the request first A custom response header is missing [#a-custom-response-header-is-missing] The header may have been: * overwritten by a higher-priority routing rule * removed by framework behavior * replaced by Easel because it uses a reserved name * treated as hop-by-hop * removed by an upstream proxy * rejected because of a size limit * added only to a route that did not handle the request Avoid custom headers in the reserved `X-Easel-*` namespace. Use: ```http X-My-App-Version: 42 ``` instead of: ```http X-Easel-App-Version: 42 ``` Inspect the complete redirect chain because headers on an intermediate response do not necessarily appear on the final destination. A request metadata header is missing [#a-request-metadata-header-is-missing] Some headers are available only on deployed requests and do not appear during local development. Also check whether: * the route reached application compute * the framework exposes the original request headers * an upstream proxy changed the request * the header is supported for that route type * the documented header name is correct Do not depend on undocumented internal headers. See [Request and response headers](/docs/cdn/headers). Client IP or country appears incorrect [#client-ip-or-country-appears-incorrect] IP geolocation is approximate. Unexpected values may also occur when: * a VPN is in use * a corporate gateway sends the request * a mobile carrier proxies traffic * another CDN or reverse proxy sits in front of Easel * the application reads an untrusted forwarded header * the IP database has not yet reflected a recent allocation Use the documented trusted Easel header rather than arbitrary client-supplied `X-Forwarded-For` values. Do not use IP geolocation as the only signal for high-impact access or compliance decisions. Preview and production behave differently [#preview-and-production-behave-differently] Check for differences in: * environment variables * custom domains * root directories * build commands * framework versions * routing rules * firewall policies * function placement relative to databases * cache state * preview access protection * database branches or external services Preview and production deployments have separate URLs and deployment contexts. A warm production cache does not imply that the preview cache is also warm. Reproduce the issue using the preview deployment’s exact hostname. A change works locally but not after deployment [#a-change-works-locally-but-not-after-deployment] Production builds can expose differences involving: * case-sensitive paths * missing environment variables * build-time versus runtime variables * unsupported filesystem assumptions * native dependencies * framework adapter output * route generation * static versus dynamic rendering * Node.js versions * external network access Inspect the build logs first, then use the request ID to inspect runtime logs. Do not assume that a local development server has the same caching, routing, or security behavior as a deployed preview. An external rewrite fails [#an-external-rewrite-fails] Check that: * the destination URL uses the correct protocol * the origin hostname resolves publicly * the origin accepts requests from Easel * the origin certificate is valid * required authentication headers are present * the rewrite preserves the required path and query string * the origin is not redirecting back to the Easel URL * an upstream firewall is not rejecting Easel traffic Test the origin directly where appropriate: ```bash curl -I https://origin.example.net/path ``` Then test through Easel: ```bash curl -I https://example.com/proxied-path ``` Compare the status, `Location`, cache, and request headers. A response is unexpectedly personalized [#a-response-is-unexpectedly-personalized] A shared cached response should not contain visitor-specific data. Immediately check for: * missing `private` or `no-store` * authentication performed without cache bypass * personalization based on cookies not represented in the cache key * unsupported or incorrect `Vary` * middleware adding user-specific content before caching * application state leaking between requests User-specific responses should generally return: ```http Cache-Control: private, no-store ``` Do not rely on short TTLs to protect private content. Purge the affected content and correct the cache policy before restoring the route. A response is unexpectedly slow [#a-response-is-unexpectedly-slow] Determine whether the request is: * a cache hit * a cache miss * a bypass * a static asset * a dynamic function * an external-origin request Start with: ```bash curl -sS -D - -o /dev/null \ -w 'connect=%{time_connect} starttransfer=%{time_starttransfer} total=%{time_total}\n' \ https://example.com/path ``` A slow cache hit points toward network conditions, response size, or connection setup. A slow miss may involve: * function startup * application execution * database latency * external API latency * distance between the CDN edge and US East compute * response streaming * cache regeneration Use Easel traces to separate platform time from application and downstream-service time. Collecting information for support [#collecting-information-for-support] Include the following when reporting a CDN issue: * project name or ID * deployment ID * affected hostname * complete URL * approximate request time and timezone * request method * response status * `X-Easel-Id` * `X-Easel-Cache` * relevant cache-control headers * whether the issue affects preview, production, or both * reproduction steps * whether an upstream proxy is present Do not send: * session cookies * authorization tokens * private API keys * password-reset links * unredacted personal data A reproducible request is especially useful: ```bash curl -I https://example.com/path ``` Include safe request headers only when they are required to reproduce the behavior. Related documentation [#related-documentation] * [CDN overview](/docs/cdn) * [Caching](/docs/cdn/caching) * [Revalidation and purging](/docs/cdn/revalidation) * [Static files](/docs/cdn/static-files) * [Compression](/docs/cdn/compression) * [HTTPS and TLS](/docs/cdn/https-and-tls) * [Request and response headers](/docs/cdn/headers) * [Security](/docs/security) * [Framework guides](/docs/framework-guides) # Authenticate the CLI Authenticate the CLI so platform commands can access your workspace. Log in [#log-in] Run `easel login` to open a browser session against [app.easel.sh](https://app.easel.sh). After you approve access, the CLI saves a token locally and prints the signed-in user and active workspace. ```bash easel login easel whoami ``` Tokens created by login include deploy permissions (`deployments:create`, `files:write`). If you logged in before those scopes existed, run `easel logout && easel login` once. Token resolution [#token-resolution] Token resolution order is: 1. `--token` / `-t` on the command line 2. The `EASEL_TOKEN` environment variable 3. The token saved by `easel login` For CI or non-interactive shells, set `EASEL_TOKEN` instead of using browser login: ```bash export EASEL_TOKEN="your_access_token_here" easel whoami --format json ``` Log out [#log-out] Remove saved credentials with: ```bash easel logout ``` Next steps [#next-steps] # Deploy from the CLI Upload source from your linked project (or monorepo) to Easel and create a deployment. Git is optional — the CLI hashes and uploads source files, then the platform builds remotely. By default the CLI skips `.env` and `.env.*` (including nested paths) so secrets are not uploaded. To include a specific file, add a negation in `.easelignore` (for example `!.env.example`). > **Note:** Deploy requires a CLI token with write permissions. If you logged in before deploy shipped, run `easel logout && easel login` once. Prerequisites [#prerequisites] 1. Install and authenticate: see [Authentication](/docs/cli/authentication). 2. Link a project: `easel link`, or link a monorepo with `easel link --repo` (or pass `--project`). Deploy [#deploy] Preview (default): ```bash easel deploy ``` Production: ```bash easel deploy --prod ``` Wait until the deployment is ready (or fails): ```bash easel deploy --wait --timeout 10m ``` Monorepos [#monorepos] After `easel link --repo`, you can run `easel deploy` from any app subdirectory. The CLI resolves the project from your cwd and **uploads the whole repository root** so the project's Root Directory (and workspace packages outside that directory) still apply on the remote build. For `--prebuilt`, the CLI uploads `.vercel/output` from the matched app directory instead. Options [#options] | Option | Description | | ------------------- | --------------------------------------------------------------------- | | `--prod` | Deploy to production (default is preview) | | `--project` | Project slug or ID (defaults to linked project) | | `--wait` | Wait until a terminal status (`READY`, `ERROR`, …) | | `--timeout` | Timeout for `--wait` (default `10m`) | | `--archive=tgz` | Pack the tree as a tarball before upload (fewer round-trips) | | `--prebuilt` | Upload `.vercel/output` and skip the remote framework build | | `-y, --yes` | Skip the production confirmation prompt (required for `--prod` in CI) | | `-F, --format json` | Machine-readable output | Prebuilt deploys [#prebuilt-deploys] Build locally, then ship artifacts: ```bash # example: Next.js / Vercel output vercel build easel deploy --prebuilt --prod --yes --wait ``` CI [#ci] ```bash export EASEL_TOKEN=easel_… easel deploy --prod --yes --wait --format json ``` Use a token from `easel login`, or create one in the dashboard with deploy permissions. Next steps [#next-steps] # List and inspect deployments Use the CLI to list deployments for a project and inspect a specific deployment by ID or URL. List deployments [#list-deployments] List deployments for the linked project, or pass a project slug or ID: ```bash easel list easel ls my-app easel list --prod easel list --environment preview --status READY --limit 20 ``` Useful filters: | Option | Description | | --------------- | ---------------------------------------------------------- | | `--status` | Comma-separated statuses (`READY`, `BUILDING`, and others) | | `--environment` | `production` or `preview` | | `--prod` | Shorthand for `--environment production` | | `--limit` | Maximum number of deployments to return | | `--cursor` | Pagination cursor from a previous response | Human output prints a table of ID, status, environment, branch, age, and URL. Use `--format json` when you need machine-readable output. Inspect a deployment [#inspect-a-deployment] Inspect a deployment by ID or by its deployment URL: ```bash easel inspect dpl_1234567890 easel inspect https://my-app-git-main-acme.preview.easelusercontent.com ``` Optional flags: | Option | Description | | ------------ | ------------------------------------------------------------ | | `-l, --logs` | Print build logs | | `--wait` | Wait until the deployment reaches a terminal status | | `--timeout` | Timeout for `--wait` (for example `5m`, `30s`; default `3m`) | Terminal statuses are `READY`, `ERROR`, `CANCELLED`, and `SPUN_DOWN`. ```bash easel inspect dpl_1234567890 --wait --timeout 5m --logs ``` Next steps [#next-steps] # Easel CLI The Easel CLI (`easel`) lets you authenticate from the terminal, link a local directory or monorepo to project(s), **deploy from your working tree**, list and inspect deployments, and read request logs. It is published as [`@easel-sh/cli`](https://www.npmjs.com/package/@easel-sh/cli) and is currently in alpha. > **Note:** This is a preview feature currently under active development. Deploy uploads local source (or `--prebuilt` output) and builds on the platform. You can still deploy from Git or the [dashboard](https://app.easel.sh). Install [#install] Install the CLI globally with npm, or run it with `npx` without a global install: ```bash npm install -g @easel-sh/cli ``` ```bash npx @easel-sh/cli --help ``` The package exposes both `easel` and `cli` binaries. Examples in this guide use `easel`. Check the installed version: ```bash easel -V ``` Global options [#global-options] These options apply to platform commands: | Option | Description | | ---------------------------- | -------------------------------------------------------------- | | `-t, --token token` | API token (overrides `EASEL_TOKEN` and saved credentials) | | `-W, --workspace slug_or_id` | Workspace slug or ID | | `-F, --format human_or_json` | Output format (defaults to `human` in a TTY, `json` otherwise) | | `--no-color` | Disable ANSI colors | Point the CLI at a different API host with `EASEL_API_URL` (defaults to `https://app.easel.sh`). Next steps [#next-steps] # Query request logs Show recent request logs for the linked project, optionally filtered to a deployment. Basic usage [#basic-usage] ```bash easel logs easel logs dpl_1234567890 easel logs --environment production --status-code 5xx --since 1h ``` Log command options [#log-command-options] | Option | Description | | --------------------- | --------------------------------------------------- | | `-f, --follow` | Poll for new request logs | | `-x, --expand` | Print function log lines for each request | | `--project` | Project slug or ID (defaults to the linked project) | | `--deployment` | Deployment ID or URL filter | | `-e, --environment` | `production` or `preview` | | `--status-code` | Status code or family (for example `500`, `5xx`) | | `--level` | Console log level (`warning`, `error`, `fatal`) | | `-q, --query` | Filter query (for example `path:"/api" method:GET`) | | `--request-id` | Show logs for a specific request ID | | `--since` / `--until` | Time bounds (`1h`, `30m`, or ISO 8601) | | `--limit` | Maximum number of requests to return | | `--timeout` | How long `--follow` should run (default `5m`) | Follow live traffic [#follow-live-traffic] Follow live traffic and expand function output: ```bash easel logs --follow --expand --timeout 10m ``` Next steps [#next-steps] # Link a project Link your working tree so later commands know which project to use. Link a single project [#link-a-single-project] ```bash easel link ``` In interactive terminals, the CLI lists projects in your active workspace and prompts you to pick one. In non-interactive mode, pass the project explicitly: ```bash easel link --project my-app ``` The CLI stores the link at `.easel/project.json` in the current directory. Commit that file if your team wants a shared default project for the repo. Link a monorepo [#link-a-monorepo] When one git repo contains multiple Easel projects (each with its own Root Directory), link them once from the monorepo root: ```bash easel link --repo ``` Interactive mode lists workspace projects and their Root Directories. Select one or more (comma-separated numbers, or `all`). Non-interactive mode requires explicit projects: ```bash easel link --repo --project front --project web ``` This writes `.easel/repo.json` at the git repository root (or the current directory if you are not in a git repo), mapping each project to its Root Directory: ```json { "workspaceId": "…", "workspaceSlug": "…", "projects": [ { "id": "…", "slug": "front", "directory": "apps/front" }, { "id": "…", "slug": "web", "directory": "apps/web" } ] } ``` After linking, run commands from any subdirectory. The CLI picks the deepest matching project for your cwd: ```bash cd apps/front && easel deploy cd apps/web && easel logs ``` At the repo root (or when several projects match), pass `--project` or choose interactively. Commit `.easel/repo.json` if your team wants a shared monorepo mapping. A nearest `.easel/project.json` still overrides the repo map for that directory. You can override the project per command with `--project`, or select a workspace with `--workspace` / `-W`. Next steps [#next-steps] # Domains and TLS A hostname on Easel selects which deployment answers the request. Production domains point at the Current Production deployment. Preview and unique deployment URLs point at specific immutable deployments. Hostname types [#hostname-types] | Type | Role | | ------------------------------- | -------------------------------------------------------------------- | | Apex or subdomain custom domain | Serves the Current Production deployment after DNS and TLS are ready | | Deployment URL | Serves one immutable deployment | | Preview URL | Serves the latest Ready deployment for a branch or pull request | | Redirect domain | Forwards traffic to another hostname | Set up a domain [#set-up-a-domain] 1. Add the domain in the project. See [Add a domain](/docs/domains/add-a-domain). 2. Configure DNS. See [DNS](/docs/domains/dns) and [Domain verification](/docs/domains/verification). 3. Wait for HTTPS. See [TLS](/docs/domains/tls) and [HTTPS and TLS](/docs/cdn/https-and-tls). Wildcards and migration [#wildcards-and-migration] * [Wildcard domains](/docs/domains/wildcards) * [Migrate a domain](/docs/domains/migrate) * [Domain redirects](/docs/domains/redirects) * [Domains reference](/docs/domains/reference) Related delivery topics [#related-delivery-topics] * [Routing](/docs/delivery/routing) * [Redirects and rewrites](/docs/delivery/redirects-and-rewrites) * [CDN](/docs/cdn) * [Domains and deployments](/docs/deployments/domains) # Redirects and rewrites Redirects and rewrites change how Easel handles a requested path. Redirects [#redirects] A redirect returns a 3xx response with a `Location` header. The browser or client then requests the destination URL. Use redirects for: * Canonical hostnames * Moved pages * HTTP-to-HTTPS behavior * Legacy path migrations * Locale or product restructuring Choose permanent status codes only when the change is intended to be durable and safe for clients and search engines to cache. Rewrites [#rewrites] A rewrite changes the internal destination without changing the browser-visible URL. Use rewrites for: * Mapping a public path to an internal application route * Serving a single-page application fallback * Proxying a supported external origin * Gradual application migrations Framework configuration [#framework-configuration] Prefer the framework's documented redirect and rewrite configuration when Easel's framework integration consumes it. Examples may include Next.js configuration, framework route rules, or generated deployment manifests. Rule order [#rule-order] Rules are order-sensitive. Put specific rules before broad catch-all rules. ```text /account/settings → /settings/profile /account/* → /new-account/* /* → /index.html ``` Avoid loops [#avoid-loops] A rule loops when its output matches the same rule or another rule that returns to the original path. Test both source and destination forms, including trailing slashes and host normalization. Caching [#caching] Redirect responses can be cached by browsers and shared caches depending on status and cache headers. Rewrites inherit the cache behavior of the final response. Verification [#verification] Use a header-only request to inspect behavior: ```bash curl -I https://example.com/old-path ``` For rewrites, inspect request details and logs to confirm the internal destination. # Routing Routing determines which deployment and resource handle an incoming request. Host routing [#host-routing] The request host first selects an Easel deployment target: * A unique deployment URL selects one immutable deployment. * A branch or pull-request URL selects the latest ready deployment for that alias. * A production domain selects the Current Production deployment. Request evaluation [#request-evaluation] Within the selected deployment, Easel evaluates request handling in a defined order. ```text Host selects deployment ↓ Platform and project security controls ↓ Redirects and rewrites ↓ Framework middleware, when present ↓ Cache and static asset lookup ↓ Dynamic route or Function ↓ Not found response ``` Static routes [#static-routes] Static files produced by the build are served without invoking application code. Asset paths are derived from the deployment output and framework routing metadata. Dynamic routes [#dynamic-routes] Server-rendered pages, API routes, loaders, actions, and other handlers route to Easel Functions. Dynamic route precedence is determined by the framework's generated route manifest and Easel's supported routing model. Middleware [#middleware] Framework middleware can inspect or modify requests before the final route executes. Middleware support, matchers, runtime behavior, and limitations are framework-specific. Rewrites [#rewrites] A rewrite changes the internal destination while preserving the URL shown to the visitor. Rewrites can target another route in the deployment and, where supported, an external origin. Redirects [#redirects] A redirect returns a 3xx response and a `Location` header. Redirects complete before the destination application route runs. Trailing slashes and normalization [#trailing-slashes-and-normalization] Frameworks can define path normalization behavior. Avoid creating rules that redirect between equivalent forms indefinitely. Debugging routing [#debugging-routing] Use the deployment URL to distinguish deployment-output problems from production-domain assignment problems. Inspect request details, redirect headers, cache status, and Function logs for the same request ID. # Add a custom domain Add a custom domain when you want users to access your project through an address you control. You must have permission to change the domain's DNS records. Before you begin [#before-you-begin] You need: * An Easel project with a Ready Production deployment * Access to the project's settings * Access to the domain's DNS provider * An Owner or Admin role in the workspace (or permission to manage project settings) A registrar and DNS provider may be the same company, but they are separate roles. Configure records wherever the domain's authoritative nameservers are hosted. Add the domain [#add-the-domain] 1. Open the project in the Easel dashboard 2. Open **Project settings** 3. Select **Custom Domains** 4. Choose **Add domain** 5. Enter the hostname without a path Examples: ```text example.com www.example.com docs.example.com ``` Do not include: ```text https:// /docs ?query=value ``` Easel rejects hostnames that are already assigned to another project, then displays the required DNS records. When adding the domain, choose **External DNS** (default: CNAME or A at your provider) or **Easel DNS** (point NS to Easel and manage records in workspace **Domains**). See [DNS configuration](/docs/domains/dns). Add an apex domain [#add-an-apex-domain] An apex domain is the registrable root of the domain: ```text example.com ``` Create an **A** record using the edge IP addresses shown in the dashboard. Example structure: ```text Type: A Name: @ Value: ``` Use the exact IP values from the dashboard. Do not copy an IP from another project or from outdated documentation. Do not add an AAAA (IPv6) record unless Easel explicitly shows one. A stale AAAA record can send some users to the wrong endpoint. Add a subdomain [#add-a-subdomain] A subdomain adds a label before the apex domain: ```text www.example.com docs.example.com app.example.com ``` Create a **CNAME** record that points to Easel: ```text Type: CNAME Name: www Value: cname.easelusercontent.com ``` DNS dashboards represent the Name field differently. Some expect `www`; others expect the full hostname. Follow your provider's convention. Add both apex and www [#add-both-apex-and-www] When you add an apex domain, the dashboard offers to also add `www` and redirect the apex to `www` (default on). You can uncheck that option. For a public website, serving both hostnames avoids duplicate URLs: ```text example.com www.example.com ``` Choose one as the canonical domain and redirect the other to it. See [Domain redirects](/docs/domains/redirects). Domain already assigned [#domain-already-assigned] Each hostname can belong to only one Easel project. If the hostname is already assigned, remove it from the existing project first, then add it to the new project. Easel does not offer a silent cross-project or cross-workspace transfer. Complete setup [#complete-setup] After adding the domain: 1. Add the required routing record (A or CNAME) 2. Wait for Easel to detect the configuration (the dashboard polls automatically) 3. Confirm that TLS reaches **Valid Configuration** / Live 4. Test both HTTP and HTTPS 5. Configure a redirect for any alternate hostname The domain does not need a new deployment. Once live, it follows the current production deployment. See [Domains and deployments](/docs/deployments/domains). # DNS configuration The Domain Name System (DNS) maps a hostname such as `www.example.com` to the network endpoint that serves it. Easel does not **require** you to move nameservers. You can keep DNS at your current provider (**External DNS**) or opt into **Easel DNS** so Easel hosts the zone and serves flat A/AAAA answers. After you add a domain, follow the exact records shown in **Project settings → Custom Domains** (or workspace **Domains** for Easel DNS zones). Choose a DNS mode [#choose-a-dns-mode] | Mode | What you change | Leaf answer | | -------------------------- | --------------------------- | ----------------------------------------------------------------- | | **External DNS** (default) | CNAME or A at your provider | Subdomain CNAME → `cname.easelusercontent.com`; apex A → edge IPs | | **Easel DNS** (opt-in) | NS → Easel nameservers | A/AAAA served by Easel for connected hosts | External DNS [#external-dns] Keep your registrar or DNS host. Copy the A or CNAME values from the dashboard into that provider. You do not change NS records. Easel DNS [#easel-dns] 1. Add the apex under workspace **Domains**, or choose **Easel DNS** when adding a project domain. 2. At the registrar, set NS to: * `ns1.easel-dns.com` * `ns2.easel-dns.com` 3. Use **Check Nameservers** until the zone is **ACTIVE**. 4. Connected hosts get protected A/AAAA records. Edit MX, TXT, and other types in the DNS Records table. Deleting an Easel DNS zone while the domain still uses Easel NS takes the domain offline until you point NS elsewhere. Find the authoritative DNS provider [#find-the-authoritative-dns-provider] The registrar is the company where the domain is registered. The authoritative DNS provider is the service named by the domain's NS records. You must edit records at the authoritative DNS provider (or at Easel when using Easel DNS). Examples include: * Cloudflare DNS * Amazon Route 53 * Google Cloud DNS * Namecheap * GoDaddy * Squarespace Domains Changing records in a registrar dashboard has no effect when the domain uses nameservers from another provider. Apex domains (External DNS) [#apex-domains-external-dns] An apex domain is the root hostname: ```text example.com ``` Easel requires an **A** record to the edge IP addresses shown in the dashboard. ```text Type: A Name: @ Value: ``` Verification succeeds when at least one of the domain's A records matches an Easel edge IP. Use the values shown for your project rather than assuming a fixed address from documentation. AAAA records are not part of the current External setup flow. Do not add AAAA unless Easel shows an IPv6 target. ALIAS, ANAME, or CNAME flattening at the apex is not accepted by Easel's verifier today. Use an A record. With **Easel DNS**, the apex A/AAAA records are created automatically when you connect the host. Subdomains (External DNS) [#subdomains-external-dns] Subdomains use a CNAME record: ```text Type: CNAME Name: www Value: cname.easelusercontent.com TTL: Automatic ``` For `docs.example.com`, the Name may be `docs` or the full hostname depending on the provider. A CNAME record must not coexist with other records at the same exact hostname. Remove conflicting A, AAAA, or CNAME records at that name. The CNAME target `cname.easelusercontent.com` is global. It is not workspace-specific or project-specific. With **Easel DNS**, subdomains use A/AAAA (not CNAME) so the leaf stays flat. DNS records used for email [#dns-records-used-for-email] Connecting a website to Easel does not require removing mail records. Preserve records such as: * MX * SPF TXT * DKIM TXT or CNAME * DMARC TXT * Domain-verification TXT records for other services Changing nameservers without recreating these records can interrupt email delivery. If you switch to Easel DNS, recreate MX and related TXT records in the Easel DNS Records editor before or immediately after delegation. TTL and propagation [#ttl-and-propagation] TTL controls how long DNS resolvers may cache a record. Easel DNS defaults to **300** seconds for new records. Before migrating an active domain: 1. Lower the relevant record's TTL in advance 2. Wait for the previous TTL to expire 3. Change the record (or NS) to Easel 4. Verify the new target 5. Raise the TTL after the migration is stable A DNS change may appear at different times in different networks because cached answers expire independently. Do not treat a fixed propagation time as a guarantee. After Easel activates routing, edge manifests refresh on a short TTL (about `30s`), so there can be a brief delay before every edge node serves the new assignment. Proxied DNS providers [#proxied-dns-providers] Providers such as Cloudflare can proxy traffic instead of returning Easel's endpoint directly. A proxy can change TLS termination, client IP forwarding, caching, redirects, request headers, security challenges, and WebSocket behavior. For initial setup with External DNS, use DNS-only (grey cloud) mode until the domain shows a valid configuration and TLS is live. After the domain is live, if you re-enable a proxy: * Terminate TLS at the proxy with a valid certificate for the hostname, or pass through to Easel over HTTPS * Avoid conflicting redirect and cache rules that fight Easel's HTTP-to-HTTPS redirect * Remember that Easel verifies apex domains by A-record IP match; a proxied A record may not match Easel edge IPs during verification Easel DNS zones are not behind a third-party proxy at the DNS layer. CAA records [#caa-records] CAA records restrict which certificate authorities may issue TLS certificates for a domain. Easel uses Let's Encrypt. If the domain has CAA records, they must permit Let's Encrypt: ```text example.com. CAA 0 issue "letsencrypt.org" ``` Check CAA on both the exact hostname and the apex when troubleshooting issuance. DNSSEC [#dnssec] DNSSEC does not normally prevent connecting a domain to Easel, but broken delegation or stale DS records can make the domain fail to resolve or block certificate validation. When moving nameservers (including to Easel DNS): * Update or remove the old DS record at the registrar * Enable DNSSEC again only after the new provider supports it (Easel DNS does not enable DNSSEC in v1) * Confirm the chain validates before declaring the migration complete Verify records [#verify-records] Use the Easel dashboard as the source of truth for the required record. Useful external checks: ```bash dig example.com A dig example.com AAAA dig www.example.com CNAME dig example.com NS dig example.com CAA ``` The answer should match the configuration Easel requested. # Domains Every Easel project receives an Easel-provided URL. You can also connect custom domains such as `example.com`, `www.example.com`, or `docs.example.com`. A custom domain gives users a stable address for your application while Easel manages how requests reach the project's current production deployment. Domain concepts [#domain-concepts] A domain setup has several independent parts: * **Registration** determines who owns the domain * **DNS** tells clients where to send requests * **Project assignment** tells Easel which project should receive the request * **TLS** secures requests over HTTPS * **Deployment routing** determines which deployment serves the request Changing one part does not necessarily change the others. For example, assigning a domain to a project does not update DNS at your provider. Supported domain types [#supported-domain-types] | Type | Example | DNS record | | ---------------- | -------------------- | ------------------------------------- | | Apex domain | `example.com` | A record to Easel edge IPs | | Subdomain | `www.example.com` | CNAME to `cname.easelusercontent.com` | | Nested subdomain | `app.eu.example.com` | CNAME to `cname.easelusercontent.com` | Wildcard domains such as `*.example.com` are not supported. Add each hostname you need. The exact record values appear when you add the domain in the dashboard. How domain setup works [#how-domain-setup-works] A typical setup follows this sequence: 1. Add the domain to an Easel project 2. Configure the DNS records shown by Easel 3. Wait for Easel to detect the records 4. Allow Easel to provision a TLS certificate 5. Test the domain over HTTPS 6. Configure a canonical-domain redirect when needed Adding a domain does not create a new deployment. The domain routes to the project's current production deployment. Guides in this section [#guides-in-this-section] Domains and deployments [#domains-and-deployments] Custom domains belong to projects, not individual deployment artifacts. They follow whichever Production deployment is current. See [Domains and deployments](/docs/deployments/domains) for production assignment, promotion, rollback, and domain-routing behavior. # Migrate a domain to Easel A domain migration changes where DNS sends traffic. The safest migration prepares the Easel deployment and certificate path before moving production traffic. Migration checklist [#migration-checklist] Before changing DNS: * Deploy the application to Easel * Test the Production deployment through its Easel URL * Add the custom domain to the Easel project * Inventory existing DNS records * Preserve mail and third-party verification records * Reduce the relevant DNS TTL in advance * Confirm the old hosting configuration remains available * Decide how to roll back the DNS change 1. Inventory DNS [#1-inventory-dns] Export or record all existing DNS entries, especially: * A and AAAA * CNAME * MX * TXT * CAA * SRV * DKIM * DMARC * Verification records Do not replace nameservers without recreating required records at the new DNS provider. 2. Prepare the Easel project [#2-prepare-the-easel-project] Confirm that: * The expected Production deployment is Ready and Current * Static assets load correctly * Functions work * Environment variables are configured * Redirects are correct * The application accepts the intended hostname * Authentication callback URLs include the new domain Use the unique deployment URL for initial testing. See [Deployment URLs](/docs/deployments/urls). When the application requires the real hostname, use a local hosts-file override or a controlled test hostname rather than moving public DNS prematurely. 3. Add the domain [#3-add-the-domain] Add the domain to Easel and leave the old traffic record in place until you are ready to cut over. Easel verifies ownership by detecting the A or CNAME routing record. You cannot fully verify before the cutover unless you temporarily point DNS at Easel or use a hosts override for local checks. 4. Lower TTL [#4-lower-ttl] Lower the TTL of the record that will change, ideally before the migration window. You must wait for the old TTL to expire before the shorter TTL takes effect for previously cached answers. 5. Change DNS [#5-change-dns] Replace the old web-routing record with the record shown by Easel: * Apex: A record to the edge IPs in the dashboard * Subdomain: CNAME to `cname.easelusercontent.com` Do not remove unrelated MX or TXT records. Keep the previous hosting environment available while caches expire. If you use Cloudflare or another proxy, switch to DNS-only until the domain is live on Easel. See [DNS configuration](/docs/domains/dns). 6. Verify the cutover [#6-verify-the-cutover] Test from more than one network and resolver. Check: ```bash dig example.com A dig example.com AAAA dig www.example.com CNAME curl -I https://example.com curl -I https://www.example.com ``` Confirm: * DNS resolves to the intended endpoint * HTTPS uses a valid certificate * The correct Easel project responds * Canonical redirects work * Authentication and APIs work * Email remains operational 7. Monitor [#7-monitor] Watch error rate, TLS failures, 404 responses, redirect loops, login and callback failures, and regional DNS differences. Roll back [#roll-back] To roll back a DNS migration: 1. Restore the previous routing record 2. Keep Easel configured while caches expire 3. Confirm traffic returns to the previous host 4. Diagnose the Easel issue before attempting another cutover DNS rollback is not instantaneous for clients that cached the new answer. Nameserver migration [#nameserver-migration] Changing nameservers is broader and riskier than changing one web record. A nameserver migration affects every DNS record for the domain. Prefer changing only the web-routing record unless moving authoritative DNS is an explicit goal. # Domain redirects Domain redirects send requests from one configured hostname to another. A common setup is: ```text example.com → www.example.com ``` or: ```text www.example.com → example.com ``` Choose one canonical hostname and redirect the other. Configure a domain redirect [#configure-a-domain-redirect] 1. Add both the source and destination domains to the project 2. Wait until both have valid DNS and TLS 3. Open the source domain's settings 4. Set the domain to redirect and choose a destination hostname 5. Choose a redirect status code 6. Save the change 7. Test the redirect over HTTP and HTTPS When you add an apex domain, the dashboard can create `www` as the content host and redirect the apex to `www` with a **307** by default. You can change the code later. Easel provisions TLS for the source domain as well as the destination so HTTPS requests can redirect securely. Redirect status codes [#redirect-status-codes] Easel supports: | Code | Label in the dashboard | | ---- | ------------------------------------ | | 301 | Permanent Redirect | | 302 | Temporary Redirect | | 307 | Temporary Redirect (Preserve Method) | | 308 | Permanent Redirect (Preserve Method) | Use **308** when the canonical hostname is intended to remain stable and you need to preserve the request method and body. Use **307** for temporary changes that must preserve method. **301** and **302** are also available. Preserve path and query [#preserve-path-and-query] Domain redirects preserve both path and query string: ```text https://example.com/docs/start?ref=email ``` becomes: ```text https://www.example.com/docs/start?ref=email ``` Domain redirect versus application redirect [#domain-redirect-versus-application-redirect] Use a domain redirect when every path on one hostname should move to another hostname. Use framework or route configuration when redirect logic depends on path, query parameters, authentication, locale, headers, or application data. Domain redirects execute in edge attribution before WAF rules, cache lookups for application content, and function invocation. Redirect loops [#redirect-loops] Easel does not currently block saving reciprocal redirects such as: ```text example.com → www.example.com www.example.com → example.com ``` Avoid configuring loops. Troubleshoot indirect loops by checking: * Easel domain redirects * Framework redirects * Middleware * Reverse proxies * CDN rules * Browser-cached permanent redirects ```bash curl -I -L --max-redirs 10 https://example.com ``` SEO considerations [#seo-considerations] Use one canonical hostname consistently in application links, sitemap URLs, canonical tags, OAuth callback URLs, webhook URLs, and cookie configuration. A redirect consolidates traffic, but application-generated URLs should still use the canonical hostname. # Domain reference Terminology [#terminology] | Term | Meaning | | ------------------ | ------------------------------------------------------------------- | | Apex domain | Registrable root, such as `example.com` | | Subdomain | Hostname below the apex, such as `www.example.com` | | Wildcard domain | Pattern such as `*.example.com` (not supported for customers today) | | Registrar | Company that manages domain registration | | DNS provider | Service hosting authoritative DNS records | | Nameserver | Server authoritative for a DNS zone | | Assignment | Association between a hostname and an Easel project | | Canonical domain | Preferred public hostname | | Current deployment | Production deployment currently receiving project traffic | DNS targets [#dns-targets] | Domain type | Record | Target | | ----------- | ------ | ------------------------------- | | Apex | A | Edge IPs shown in the dashboard | | Subdomain | CNAME | `cname.easelusercontent.com` | Targets are global. Copy values from the dashboard for the current edge IPs. Common DNS record types [#common-dns-record-types] | Type | Purpose | | ----- | ------------------------------------------------------------------ | | A | Maps a hostname to an IPv4 address | | AAAA | Maps a hostname to an IPv6 address (not used in Easel setup today) | | CNAME | Aliases one hostname to another | | TXT | Stores policy and third-party verification text | | MX | Routes email | | CAA | Restricts certificate authorities | | NS | Delegates authoritative nameservers | Domain statuses [#domain-statuses] | Status | Meaning | | ---------------------------- | ---------------------------------------------- | | Invalid DNS configuration | Required routing record missing or incorrect | | Provisioning SSL certificate | DNS verified; certificate issuance in progress | | Valid Configuration | Live and ready to serve | | Configuration Error | Setup failed | | Removed | Detached from the project | TLS [#tls] | Item | Value | | ------------------------------ | ------------------------------------- | | Certificate authority | Let's Encrypt | | Validation | HTTP-01 for custom domains | | CAA example | `CAA 0 issue "letsencrypt.org"` | | Custom uploaded certificates | Not supported | | Customer wildcard certificates | Not supported | | HTTP to HTTPS | Automatic redirect when TLS is active | Redirect status codes [#redirect-status-codes] | Code | Use | | ---- | --------------------------------------------------------------- | | 301 | Permanent redirect | | 302 | Temporary redirect | | 307 | Temporary redirect; preserve method (apex/`www` helper default) | | 308 | Permanent redirect; preserve method | Path and query are preserved. Hostname validation [#hostname-validation] | Rule | Limit | | ------------------------------- | ------------------------------------ | | Maximum FQDN length | 253 characters | | Maximum label length | 63 characters | | Character set | ASCII letters, digits, and hyphens | | Internationalized domains (IDN) | Not supported as Unicode input today | | Wildcard hostnames | Not supported | Limits and surfaces [#limits-and-surfaces] | Item | Current status | | ---------------------------------- | -------------------------------------------- | | Domains per project or workspace | No published plan cap in product today | | CLI domain commands | Not available | | Public REST domain API | Not shipped; manage domains in the dashboard | | Domain failure email notifications | Not available; watch dashboard status | | Cross-workspace transfer | Not available; remove and re-add | Related guides [#related-guides] * [Domains](/docs/domains) * [Domains and deployments](/docs/deployments/domains) * [CDN HTTPS and TLS](/docs/cdn/https-and-tls) # Remove or move a domain Removing a domain detaches it from an Easel project. It does not cancel the domain registration or automatically change records at an external DNS provider. Before removing a domain [#before-removing-a-domain] Check whether the hostname is used for: * Production traffic * API requests * Authentication callbacks * Webhooks * Email links * Mobile deep links * Customer integrations * Cookies * Search indexing Removing an active domain can interrupt users once Easel's routing update takes effect (edge manifests refresh within about `30s`). Remove a domain [#remove-a-domain] 1. Open the project's **Custom Domains** settings 2. Select the domain 3. Choose **Remove** 4. Confirm removal Also update or remove related redirects that pointed at this hostname. Move a domain between projects [#move-a-domain-between-projects] Easel does not provide an atomic transfer. Use remove-then-add: 1. Prepare the destination project and Production deployment 2. Remove the hostname from the source project 3. Add the hostname to the destination project 4. Keep DNS unchanged when it already points at Easel 5. Confirm the destination project responds 6. Update redirects and application configuration There can be a short window where the hostname is unassigned. During that window, requests that still reach Easel receive a not-found response rather than another customer's project. Move a domain between workspaces [#move-a-domain-between-workspaces] There is no dedicated cross-workspace transfer. Remove the domain from the source workspace's project, then add it in the destination workspace after you have access to both. DNS verification (A or CNAME pointing at Easel) must succeed again on the destination project. After removal [#after-removal] When DNS still points to Easel after the domain is detached, Easel returns a safe unassigned-domain response (not another customer's site). Remove or update the DNS record after the domain no longer needs to route to Easel. # HTTPS and TLS for custom domains Easel serves custom domains over HTTPS using TLS certificates from Let's Encrypt. After DNS points to Easel, Easel requests and installs a certificate for the hostname using HTTP-01 validation. Certificate lifecycle [#certificate-lifecycle] A typical certificate lifecycle is: 1. Domain added 2. DNS configuration detected 3. Certificate requested (HTTP-01) 4. Certificate issued 5. HTTPS enabled 6. Certificate renewed before expiration The dashboard shows DNS and TLS progress through domain statuses such as **Invalid DNS configuration**, **Provisioning SSL certificate**, and **Valid Configuration**. Automatic HTTPS [#automatic-https] When certificate provisioning succeeds, Easel: * Serves the domain over HTTPS * Redirects HTTP to HTTPS (except ACME challenge paths) * Renews the certificate automatically while DNS remains correct * Replaces certificates without requiring a new deployment HTTP-to-HTTPS redirection is enabled for known hosts when TLS is active. Certificate coverage [#certificate-coverage] A certificate must cover each hostname that serves traffic. These are separate names: ```text example.com www.example.com api.example.com ``` Adding `example.com` does not automatically configure `www.example.com`. Add each hostname you need. Wildcard certificates for customer domains are not available. See [Wildcard domains](/docs/domains/wildcards). Provisioning requirements [#provisioning-requirements] Certificate issuance can fail when: * DNS does not point to Easel * A proxy intercepts the HTTP-01 validation response * CAA records block Let's Encrypt * A conflicting domain assignment exists * DNSSEC is broken * Let's Encrypt rate-limits issuance * The hostname fails validation rules Prefer DNS-only mode at your DNS provider until the certificate is live. See [DNS configuration](/docs/domains/dns). CAA records [#caa-records] If the domain has CAA records, permit Let's Encrypt: ```text example.com. CAA 0 issue "letsencrypt.org" ``` Certificate renewal [#certificate-renewal] Renewal runs automatically in the background while: * The domain remains assigned * DNS continues routing correctly * HTTP-01 validation remains possible * CAA records permit Let's Encrypt A domain can continue working temporarily with an existing certificate even after DNS becomes invalid. That does not mean renewal will succeed. Easel does not currently email certificate renewal failures; watch domain status in the dashboard. Custom certificates [#custom-certificates] Easel-managed certificates are required for custom domains. Uploading your own certificate and private key is not supported. Removing a domain [#removing-a-domain] Removing a domain stops Easel from serving it after routing state updates (edge manifests refresh within about `30s`). The certificate may remain in internal systems until cleanup completes, but the hostname is no longer assigned to the project. Related guides [#related-guides] * [Add a custom domain](/docs/domains/add-a-domain) * [DNS configuration](/docs/domains/dns) * [CDN HTTPS and TLS](/docs/cdn/https-and-tls) # Domain troubleshooting A custom domain can fail at several independent layers. Diagnose them in order: 1. DNS resolution 2. Project assignment 3. TLS certificate 4. Deployment routing 5. Application behavior Domain status model [#domain-status-model] The dashboard exposes these primary statuses: | Status | Meaning | | ---------------------------- | ------------------------------------- | | Invalid DNS configuration | A or CNAME missing or incorrect | | Provisioning SSL certificate | DNS verified; certificate in progress | | Valid Configuration | Live and ready to serve | | Configuration Error | Setup failed | | Removed | Detached from the project | Also confirm the project has a Ready **Current** production deployment. A live domain still needs Current to serve application content. The domain does not resolve [#the-domain-does-not-resolve] Check authoritative records: ```bash dig example.com NS dig example.com A dig example.com AAAA dig www.example.com CNAME ``` Possible causes: * Record added at the wrong DNS provider * Typo in the hostname * Missing record * Conflicting records * Broken DNSSEC delegation * Nameserver change still propagating * Provider proxy or flattening behavior Easel cannot verify the domain [#easel-cannot-verify-the-domain] Check: * Apex domains use an A record to an Easel edge IP shown in the dashboard * Subdomains use a CNAME to `cname.easelusercontent.com` * The provider did not alter the value * The record was added to the authoritative zone * A proxy is not rewriting the A record during verification * The Name field was not duplicated with the apex domain DNS is correct but the domain shows an Easel error [#dns-is-correct-but-the-domain-shows-an-easel-error] Check that: * The domain is assigned to the intended project * The project has a Ready Production deployment that is Current * The project is not paused * The workspace is not suspended Test the project's Easel-provided production URL. If that also fails, the problem is probably not DNS. See [Deployment troubleshooting](/docs/deployments/troubleshooting). HTTPS certificate is pending [#https-certificate-is-pending] Check: ```bash dig example.com CAA dig example.com A dig www.example.com CNAME ``` Possible causes: * DNS does not point to Easel * A proxy intercepts HTTP-01 validation * CAA blocks Let's Encrypt (`letsencrypt.org`) * Certificate-authority rate limit * Broken DNSSEC Disable DNS proxy until the certificate is live. See [HTTPS and TLS](/docs/domains/tls). Browser shows a certificate mismatch [#browser-shows-a-certificate-mismatch] A certificate mismatch usually means traffic reached an endpoint that does not have a certificate for the requested hostname. Check: * DNS records (A and AAAA) * Proxy settings * Exact domain assignment * Whether `www` and apex were added separately * Whether an old AAAA record sends IPv6 traffic elsewhere Redirect loop [#redirect-loop] Inspect the response chain: ```bash curl -I -L --max-redirs 10 https://example.com ``` Check for redirects in Easel domain settings, application middleware, framework configuration, Cloudflare or another proxy, authentication middleware, and browser HSTS or cached 301/308 responses. Some users see the old site [#some-users-see-the-old-site] Likely causes: * DNS cache has not expired * Resolver cache differs by network * Old A or AAAA record remains * Proxy cache serves old content * Browser cache or service worker * The domain points to the correct project but an old deployment remains Current Compare answers from multiple resolvers and test the unique deployment URL. Email stopped working [#email-stopped-working] Connecting a website should not remove email records. Check: ```bash dig example.com MX dig example.com TXT dig selector._domainkey.example.com TXT dig _dmarc.example.com TXT ``` Restore missing MX, SPF, DKIM, and DMARC records at the authoritative DNS provider. Domain returns 404 [#domain-returns-404] A 404 can come from different layers: * Easel does not recognize the hostname (unassigned or removed) * The domain is assigned to the wrong project * The current deployment has no matching application route * A proxy rewrites the Host header * The application rejects the hostname Check response headers and compare the root path with a known application path. Information to include with a support request [#information-to-include-with-a-support-request] Include: * Workspace and project * Exact hostname * Domain status shown in Easel * Expected current deployment ID * DNS provider * Relevant `dig` output * `curl -I` output * Approximate time and user region * Whether the issue affects IPv4, IPv6, HTTP, or HTTPS # Domain verification Easel verifies a custom domain by checking that its DNS records point at Easel. Verification is required before Easel provisions a TLS certificate and marks the domain live. There is no separate TXT ownership challenge for customer domains today. How verification works [#how-verification-works] When you add a domain, its status starts as **Invalid DNS configuration** (pending DNS). Easel checks: | Domain type | Expected record | | ----------------------------- | ------------------------------------- | | Apex (`example.com`) | A record matching an Easel edge IP | | Subdomain (`www.example.com`) | CNAME to `cname.easelusercontent.com` | The dashboard refreshes this check automatically while the domain is pending or provisioning TLS. You can also choose **Refresh**. Add the routing record [#add-the-routing-record] 1. Copy the record type and value from **Project settings → Custom Domains** 2. Open the authoritative DNS provider 3. Add the A or CNAME record exactly as shown 4. Save the record 5. Return to Easel and wait for verification, or choose **Refresh** Some providers automatically append the apex domain to the Name field. Avoid creating a duplicated hostname such as `www.example.com.example.com`. Verification and traffic [#verification-and-traffic] Passing DNS verification means Easel can see the correct routing record. It does not by itself mean every visitor already reaches Easel; resolvers may still cache older answers. Keep the A or CNAME record in place after the domain is live. Easel re-checks DNS on refresh. If the record disappears or points elsewhere, the domain returns to a pending DNS state and renewal can fail. Domain conflicts [#domain-conflicts] Each hostname can be assigned to only one project across Easel. If create fails because the hostname is already taken, remove it from the other project first. Soft-removed domains may still occupy the hostname until they are fully deleted. There is no cross-workspace transfer flow and no TXT-based claim that silently takes a domain from another account. Domain statuses [#domain-statuses] | Status | Meaning | | ---------------------------- | ----------------------------------------------- | | Invalid DNS configuration | Required A or CNAME is missing or incorrect | | Provisioning SSL certificate | DNS verified; certificate issuance in progress | | Valid Configuration | Domain is live and ready to serve | | Configuration Error | Setup failed; check DNS and certificate details | | Removed | Domain was detached from the project | DNS verification status is separate from whether the project's current production deployment is healthy. A live domain still needs a Ready Current production deployment to serve application content. # Wildcard domains A wildcard domain matches multiple first-level subdomains. Example: ```text *.example.com ``` Customer wildcard domains are not supported on Easel today. Add each hostname you need as an exact domain, for example: ```text alice.example.com store.example.com preview-123.example.com ``` What is not covered [#what-is-not-covered] * Adding `*.example.com` in **Custom Domains** * Wildcard certificates for customer hostnames * Nameserver delegation for wildcard routing Platform-generated hostnames under `*.preview.easelusercontent.com` are separate from customer wildcards and are managed by Easel. Multi-tenant applications [#multi-tenant-applications] If your application needs per-tenant subdomains, add each tenant hostname individually for now, or terminate those hostnames on your own reverse proxy in front of Easel until native wildcards ship. When you handle many hostnames in application code, validate the `Host` header and map it to a known tenant. Do not treat every arbitrary subdomain as trusted input. Related guides [#related-guides] * [Add a custom domain](/docs/domains/add-a-domain) * [HTTPS and TLS](/docs/domains/tls) # Build configuration Build configuration controls how Easel turns a source revision into a deployment. Framework detection [#framework-detection] Easel inspects the project files to identify a supported framework and select default commands and output handling. Review the detected framework before the first production release. Framework detection does not guarantee that every framework capability is supported. See the matching framework guide for the compatibility contract. Root directory [#root-directory] The root directory identifies the application within the repository. It is especially important for monorepos. Changing the root directory affects framework detection, dependency installation, build execution, and output discovery. Install command [#install-command] The install command restores project dependencies from the committed lockfile. Prefer deterministic installation: ```bash npm ci ``` ```bash pnpm install --frozen-lockfile ``` ```bash yarn install --immutable ``` Use a custom command only when the repository requires it. Build command [#build-command] The build command creates production output. Examples: ```bash npm run build ``` ```bash pnpm --filter web build ``` Framework integrations may provide a default. Override it only when the application needs a custom workspace command or preprocessing step. Output handling [#output-handling] Static frameworks usually produce an output directory such as `dist`. Full-stack framework adapters can produce a deployment manifest containing static assets, Functions, routes, and metadata. Do not configure a static output directory for a full-stack integration unless the framework guide explicitly requires it. Environment variables [#environment-variables] Build-time variables are read while compiling the application. Variables exposed through public framework prefixes can be embedded into browser bundles and must not contain secrets. Preview and Production builds can use different values. Runtime configuration [#runtime-configuration] Function runtime, memory, CPU, and other supported settings apply to dynamic output. Use [Function configuration](/docs/functions/configuration) for current options. Configuration precedence [#configuration-precedence] Documented project settings should take precedence over detected defaults. Framework-emitted metadata may apply at route or Function level only when the current Easel build integration consumes it. Rebuild requirements [#rebuild-requirements] Create a new deployment after changing: * Source code * Lockfiles or dependencies * Root, install, or build commands * Build-time environment variables * Framework or adapter configuration * Runtime configuration embedded in build output Changing traffic assignment between existing Production deployments does not rebuild them. # Creating deployments Easel can create deployments from Git activity, local source code, and the dashboard. The method you choose determines where the source comes from. The deployment environment determines which project configuration Easel uses during the build. Deploy from Git [#deploy-from-git] For a Git-connected project, Easel creates deployments when you push commits to the connected repository. | Git activity | Deployment environment | | ----------------------------- | ---------------------- | | Push to the production branch | Production | | Push to another branch | Preview | The production branch is selected in **Project settings → Environments**. A new commit creates a new deployment. Easel does not modify a previously created deployment in place. When an open pull request exists for the head branch, Easel also associates a pull request preview URL with the Preview deployment. See [Git deployments](/docs/deployments/git) for branch behavior, skip controls, and monorepos. Deploy from the CLI [#deploy-from-the-cli] Use the Easel CLI to create a deployment from a local project directory. ```bash easel deploy ``` CLI deployments default to Preview. Deploy to Production with: ```bash easel deploy --prod ``` In CI, pass `--yes` (or `-y`) with `--prod` to skip the production confirmation prompt. Before deploying, the CLI may ask you to: * Sign in * Select a workspace * Link the directory to an existing project * Create a project The CLI uploads the project source (or a prebuilt output with `--prebuilt`), then prints the deployment URL. Use `--wait` to wait for a terminal status. See [Deploy from the CLI](/docs/cli/deploy) for full options. Deploy from the dashboard [#deploy-from-the-dashboard] From the project overview, **Deploy** creates a Production deployment from the current HEAD of the production branch. Deploy a specific branch or commit [#deploy-a-specific-branch-or-commit] Git-connected projects deploy the revision that triggered the push. To deploy a different revision: * Push the revision to a tracked branch * Check out the revision locally and run `easel deploy` * Promote a Preview deployment to production (rebuilds that commit with Production configuration) First deployment [#first-deployment] There is no special first-deployment rule. The environment follows the usual mapping: * Git: production branch → Production; any other branch → Preview * CLI: Preview by default; `--prod` for Production * Dashboard **Deploy**: Production from the production branch HEAD A project receives generated Easel hostnames when deployments become Ready. Custom domains are not assigned automatically; attach them in **Project settings → Custom Domains**. See [Domains](/docs/domains). A Preview deployment can exist before the first Production deployment. Build configuration [#build-configuration] A deployment snapshots the project settings and environment variables available for its environment when the deployment is created. This includes: * Framework preset * Root directory * Install command * Build command * Output directory * Runtime configuration * Environment variables for that environment Changing project settings or environment variables does not alter an already completed deployment. New settings apply to later deployments. After creating a deployment [#after-creating-a-deployment] Once created, a deployment moves through its lifecycle: 1. **Queued** — waiting for build capacity 2. **Provisioning** — preparing the build environment 3. **Building** — installing dependencies and producing application output 4. **Assigning domains** — publishing output and assigning URLs 5. **Ready** — available through its deployment URL 6. **Failed**, **Canceled**, or **Skipped** — the deployment did not become ready See [Managing deployments](/docs/deployments/manage) for status details and available actions. # Domains and deployments Custom domains are assigned to projects and route to the project's current Production deployment. Adding or removing a domain changes routing configuration. It does not create or modify a deployment artifact. For DNS, TLS, verification, redirects, and migration, see [Domains](/docs/domains). Domain assignment model [#domain-assignment-model] The relationship is: ```text Custom domain ↓ Easel project ↓ Current Production deployment ``` Custom domains cannot be assigned to Preview deployments or Git branches. Preview traffic uses generated Easel hostnames. See [Deployment URLs](/docs/deployments/urls). Production deployment versus current deployment [#production-deployment-versus-current-deployment] A Production deployment is built using Production configuration. The current deployment is the Production deployment receiving traffic from: * The project's Easel-provided production URL * Custom domains assigned to the project as content hosts A Production deployment may be Ready without being Current when **Auto-assign production domains** is off. See [Environments](/docs/deployments/environments). New production deployments [#new-production-deployments] When automatic production assignment is enabled: 1. The existing current deployment continues serving domains 2. Easel creates and builds the new Production deployment 3. The new deployment reaches Ready 4. Easel updates project-domain routing 5. The previous deployment remains in deployment history A failed or canceled deployment does not change domain routing. Promotion [#promotion] Promoting an existing Ready Production deployment updates project-domain routing without changing DNS and without rebuilding. DNS maps the hostname to Easel. Promotion changes Easel's internal routing target. See [Promoting deployments](/docs/deployments/promote). Rollback [#rollback] Instant Rollback reassigns project domains to a previous Ready Production deployment. Rollback does not require DNS changes and does not rebuild the deployment. See [Rolling back deployments](/docs/deployments/rollback). Adding a domain [#adding-a-domain] When a domain is added to a project with an existing current deployment, it begins routing to that deployment after: * DNS is correctly configured * TLS is ready * Easel activates the assignment Adding the domain does not trigger a new build. Removing a domain [#removing-a-domain] Removing a domain detaches it from the project. It does not delete deployments and does not affect other project domains. When DNS still points to Easel, an unassigned domain returns a not-found response rather than another customer's project. Domain redirects [#domain-redirects] A domain configured as a redirect does not invoke the deployment's application output. Easel handles the redirect before WAF, cache, and functions. Both the source and destination hostnames still require valid domain configuration and TLS. See [Domain redirects](/docs/domains/redirects). Cache behavior [#cache-behavior] Promotion and rollback do not automatically purge ISR or runtime caches. Caches are keyed by deployment ID, so traffic to the newly Current deployment uses that deployment's cache namespace. Edge responses follow the newly aliased deployment's routes and assets. Domain routing changes and cache invalidation are separate operations. # Deployment environments Deployment environments let the same project use different configuration for testing and production traffic. Easel provides two deployment environments: * **Preview** * **Production** The environment is selected when the deployment is created. It determines which environment variables and environment-specific settings are used during the build and at runtime. Preview [#preview] Preview deployments are for changes that are not yet serving production traffic. They are commonly created from: * Non-production branches * CLI deployments without `--prod` Preview deployments let you test application behavior using Preview configuration without modifying production domains. A Preview deployment receives a unique deployment URL. Git-connected deployments also update a stable branch preview URL after the deployment reaches Ready, and a pull request preview URL when an open pull request exists for that branch. Production [#production] Production deployments are for live traffic. They are commonly created from: * The configured production branch * `easel deploy --prod` * The dashboard **Deploy** action * Promoting a Preview deployment (rebuilds with Production configuration) Production deployments use Production environment variables and settings. A Production deployment may become the current production deployment automatically or stay **Staged** until you promote it, depending on **Auto-assign production domains**. Environment comparison [#environment-comparison] | | Preview | Production | | ---------------------------- | -------------- | -------------------------- | | Primary purpose | Test changes | Serve live traffic | | Typical Git source | Feature branch | Production branch | | Environment variables | Preview | Production | | Unique deployment URL | Yes | Yes | | Stable branch or PR URL | Yes | Not for production traffic | | Can serve production domains | No | Yes, when Current | Environment variables [#environment-variables] Variables can have different values in Preview and Production. For example: ```text DATABASE_URL Preview: preview database Production: production database ``` When a deployment is created, Easel snapshots the resolved variable values for that environment. The build and runtime use that snapshot. Changing variables later does not alter an existing deployment; create a new deployment to pick up updates. You can also set preview branch overrides in **Project settings → Environment Variables** for branch-specific Preview values. Do not place secrets directly in source control. Configure them in **Project settings → Environment Variables**. Environment and traffic are separate [#environment-and-traffic-are-separate] A deployment's environment describes its configuration. It does not, by itself, describe whether the deployment currently receives production traffic. For example, a Production deployment can be: * Building * Ready but not Current (**Staged**) * Current * Superseded by a newer Current deployment * Restored during Instant Rollback This separation lets Easel finish a production-ready release before changing production routing. Auto-assign production domains [#auto-assign-production-domains] In **Project settings → Environments**, turn **Auto-assign production domains** on or off. It is on by default. * **On**: When a Production deployment becomes Ready, Easel assigns the production hostname and live custom domains to it and marks it Current. * **Off**: Production builds still complete as **Staged**. They do not receive production domains until you [promote](/docs/deployments/promote) them. Use auto-assign off when you want to verify a Production build before it goes live. Instant Rollback also turns auto-assign off until you undo the rollback or promote a newer Production deployment. See [Rolling back deployments](/docs/deployments/rollback). Changing the production branch [#changing-the-production-branch] Configure the production branch in **Project settings → Environments**. Changing the production branch affects future Git-triggered deployments only. It does not rewrite or reclassify deployments that already exist. After changing the production branch: * New pushes to the selected branch create Production deployments. * New pushes to other branches create Preview deployments. * Existing branch URLs and deployment URLs keep their existing identity. * Auto-assign continues to apply to new Ready Production deployments according to the project setting. Related guides [#related-guides] * [Promoting deployments](/docs/deployments/promote) * [Rolling back deployments](/docs/deployments/rollback) * [Deployment URLs](/docs/deployments/urls) * [Domains](/docs/domains) # Git deployments Connecting a Git repository lets Easel create deployments from commits pushed to that repository. Production branch [#production-branch] Each project has one production branch. The default is `main`. A push to the production branch creates a Production deployment. Common production branches include: ```text main master production ``` Change the production branch in **Project settings → Environments**. The change affects future deployments only. Preview branches [#preview-branches] A push to any other branch in the connected repository creates a Preview deployment. Each commit creates a new deployment. A stable branch preview URL moves to the latest Ready deployment for that branch. Pull requests [#pull-requests] Opening or updating a pull request does not by itself create a deployment. A push to the head branch does. When an open pull request exists for that head branch, Easel also assigns a pull request preview URL. Easel can report deployment status to the Git provider and post the preview URL on the pull request and related commits. Merging a pull request [#merging-a-pull-request] The Preview deployment remains a Preview artifact. When the merge commit lands on the production branch: 1. Easel creates a new Production deployment from that commit 2. The new deployment uses Production environment variables and settings 3. It becomes Current if **Auto-assign production domains** is on, or stays Staged if auto-assign is off Production output can differ from the Preview deployment because Production variables and settings can differ. Skipping deployments [#skipping-deployments] Easel supports two skip controls in **Project settings → Build**: Skip unaffected deployments [#skip-unaffected-deployments] Enabled by default. When a push does not change this project's root directory or its workspace dependencies, Easel creates a **Skipped** deployment instead of running a build. Ignored build step [#ignored-build-step] Optional shell command run after clone on Git deploys. Exit code `0` cancels the build (status **Canceled**). Any other exit code continues the build. Example: ```bash npx turbo-ignore ``` A skipped deployment is distinct from a canceled or failed deployment. Monorepos [#monorepos] For monorepo setup, root directories, skip rules, and multi-project repositories, see [Monorepos](/docs/deployments/monorepos). Forks and untrusted code [#forks-and-untrusted-code] Deployments run for pushes to the connected repository. There is no separate fork-approval workflow that withholds secrets for forked pull requests. Keep Production credentials out of Preview variables, and enable Deployment Protection when preview hostnames must not be public. See [Preview deployments](/docs/deployments/previews). Optional: require verified (signed) commits in project Git settings when you want unsigned commits blocked. Disconnecting Git [#disconnecting-git] Disconnecting a repository stops future automatic Git deployments. It does not delete existing deployments or immediately remove their URLs. You can later connect a different repository; new deploys follow the new connection, while historical deployments keep their recorded source metadata. Git provider permissions [#git-provider-permissions] Easel requests access so it can: * Read repository contents for builds * Read commit metadata * Discover open pull requests for preview URLs * Create commit statuses or checks * Receive webhooks for pushes * Post deployment comments with preview URLs If you remove permissions, the matching features stop working: for example, without webhook access Easel no longer creates deployments on push. Related guides [#related-guides] * [Creating deployments](/docs/deployments/create) * [Preview deployments](/docs/deployments/previews) * [Deployment URLs](/docs/deployments/urls) # Deployments A deployment is a version of your application built from a specific source revision and configuration. Each successful deployment has its own URL and contains the static assets, functions, routing configuration, and metadata produced by the build. Deployments can be used for previewing changes or serving production traffic. Easel creates deployments from Git commits and from the CLI. A deployment moves through a lifecycle from creation to build, publication, and, when applicable, assignment to your production domains. How deployments work [#how-deployments-work] A typical deployment follows this sequence: ```text Source change ↓ Queued ↓ Provisioning ↓ Building ↓ Assigning domains ↓ Ready ↓ Previewed or assigned to production ``` A deployment that reaches **Ready** can receive traffic through its deployment URL. Production domains only serve the deployment currently assigned to production. This distinction is important: * A **deployment** is a built version of your application. * An **environment** determines which configuration is used to build it. * A **URL** routes traffic to it. * The **current production deployment** is the deployment serving your production domains. These concepts are related, but they are not interchangeable. Deployment environments [#deployment-environments] Easel provides two deployment environments: | Environment | Typical use | Typical trigger | | ----------- | --------------------------- | ---------------------------------------------------------------- | | Preview | Test changes before release | Push to a non-production branch or open a pull request | | Production | Serve live traffic | Push to the production branch or deploy explicitly to production | Preview and Production deployments can use different environment variables and project settings. Learn more in [Deployment environments](/docs/deployments/environments). Deployment URLs [#deployment-urls] Every successful deployment receives a unique deployment URL. Projects also have stable URLs that move forward as new deployments become ready: * A branch preview URL points to the latest ready deployment for a branch. * A pull request preview URL points to the latest ready deployment associated with a pull request. * A production domain points to the current production deployment. The unique deployment URL identifies one deployment. Stable branch, pull request, and production URLs may point to different deployments over time. Learn more in [Deployment URLs](/docs/deployments/urls). Creating deployments [#creating-deployments] You can create deployments through: * Git pushes * Pull requests associated with those pushes * The Easel CLI * The dashboard **Deploy** action for the production branch Git-connected projects usually deploy automatically. CLI deployments are useful for local testing, CI systems, and workflows that do not originate from a Git provider. Learn more in [Creating deployments](/docs/deployments/create). Managing deployments [#managing-deployments] From a project's deployment history, you can inspect the source revision, build output, logs, environment, status, and URLs associated with each deployment. Depending on the deployment state, you can also: * Cancel an in-progress deployment * Spin down a Ready or Failed deployment * Promote a staged Production deployment * Promote a Preview deployment (rebuilds with Production configuration) * Instant Rollback to a previous Production deployment Learn more in [Managing deployments](/docs/deployments/manage). Production releases [#production-releases] A Production deployment is built with Production configuration. It does not necessarily become the current deployment immediately. When **Auto-assign production domains** is on (the default), a Ready Production deployment becomes Current. When it is off, the deployment stays **Staged** until you promote it. Instant Rollback also turns auto-assign off until you undo the rollback or promote again. Keeping build configuration separate from traffic assignment supports: * Automatically publishing every successful production-branch deployment * Reviewing a Production deployment before assigning production traffic * Returning production traffic to a previous deployment without rebuilding it Learn more in [Production releases](/docs/deployments/production-releases), [Promoting deployments](/docs/deployments/promote), and [Rolling back deployments](/docs/deployments/rollback). Guides in this section [#guides-in-this-section] # Managing deployments The deployment history for a project shows every deployment created from Git, the CLI, or the dashboard. Deployment details [#deployment-details] A deployment detail page includes: * Status * Environment (Preview or Production) * Source branch and commit * Commit author and message * Creation time and build duration * Framework and build configuration * Generated resources * Deployment URLs (commit, branch, pull request, and production when applicable) * Build and runtime logs Use these details to determine exactly what source and configuration produced the deployment. Deployment statuses [#deployment-statuses] | Status | Meaning | | ----------------- | ---------------------------------------------------- | | Queued | Waiting for build capacity | | Provisioning | Preparing the build environment | | Building | Installing dependencies and producing build output | | Assigning domains | Publishing output and assigning URLs | | Ready | Published and able to receive traffic | | Failed | Build or publication did not complete | | Canceled | Stopped before becoming ready | | Skipped | Not built because changes did not affect the project | | Spun down | Taken offline after Ready or Failed | Production deployments that are Ready but not Current appear as **Staged** when auto-assign is off. **Current** marks the Production deployment serving production domains. Cancel a deployment [#cancel-a-deployment] You can cancel a deployment while it is Queued, Provisioning, Building, or Assigning domains. Canceling: * Stops remaining build work * Prevents the deployment from becoming Ready * Leaves existing stable URLs unchanged * Preserves logs generated before cancellation Any workspace member can cancel. A deployment that has already reached Ready cannot be canceled; spin it down instead if you need to take it offline. Spin down a deployment [#spin-down-a-deployment] Spin down takes a Ready or Failed deployment offline. The unique deployment URL stops serving because only Ready deployments are served. Easel blocks or warns when the deployment is required for active production routing. Prefer Instant Rollback or promote another deployment before spinning down Current production. There is no separate delete-deployment action today. Spin down is the way to retire a deployment's serving surface. Creating a new deployment from the same source [#creating-a-new-deployment-from-the-same-source] There is no one-click "redeploy this deployment ID with historical settings" action. To ship the same commit again with current configuration: * Push the commit again, or run `easel deploy` from that revision * Use dashboard **Deploy** for the production branch HEAD * Promote a Preview deployment to production (rebuilds that commit with current Production environment variables and the source deployment's project settings snapshot) New deployments snapshot environment variables and project settings at creation time. They do not mutate the original deployment. Filter deployment history [#filter-deployment-history] In the dashboard, use the deployments list to distinguish: * Latest deployment for a branch * Latest Ready deployment for a branch or pull request * Current production deployment * Staged Production deployments waiting for promote * Previous production deployments eligible for Instant Rollback Retention [#retention] Easel does not enforce plan-based deployment or log retention limits in product today. Deployments remain in history until you spin them down or the project/workspace lifecycle removes access. Unique URLs resolve only while the deployment is Ready. Related guides [#related-guides] * [Promoting deployments](/docs/deployments/promote) * [Rolling back deployments](/docs/deployments/rollback) * [Deployment troubleshooting](/docs/deployments/troubleshooting) # Monorepos A monorepo can contain multiple applications, shared packages, tooling, and infrastructure code. Each deployable application should be represented by its own Easel project. Project root directory [#project-root-directory] Set each project's root directory to the application directory that contains its package manifest and framework configuration. ```text repository/ apps/ web/ ← Easel project: web docs/ ← Easel project: docs packages/ ui/ config/ ``` The build runs with the configured project root while retaining access to repository content required by the package manager and build system. Shared packages [#shared-packages] Commit workspace lockfiles and ensure the package manager can resolve internal packages in a clean checkout. Avoid relying on locally linked packages or uncommitted build output. Build commands [#build-commands] Use the command appropriate for the workspace tool: ```bash pnpm --filter web build ``` ```bash npm run build --workspace apps/web ``` ```bash turbo run build --filter=web ``` The exact command depends on the repository. Framework auto-detection can still apply within the selected project root. Skipping unaffected deployments [#skipping-unaffected-deployments] Configure path-based or command-based skip behavior so a project does not rebuild when unrelated parts of the monorepo change. A safe skip decision must include: * The application directory * Shared packages imported by the application * Root lockfiles and package-manager configuration * Shared build configuration * Framework and deployment adapter packages Do not skip a deployment solely because the application directory itself did not change. Environment variables [#environment-variables] Environment variables belong to an Easel project, not to the repository as a whole. Configure Preview and Production values separately for each application. Preview URLs [#preview-urls] Each project receives its own deployment, branch, and pull-request URLs. A pull request that changes several applications can therefore produce several independent Preview deployments. Troubleshooting [#troubleshooting] When a monorepo build succeeds locally but fails on Easel, verify: 1. The project root directory. 2. The install and build commands. 3. The package-manager version and lockfile. 4. Access to shared workspace packages. 5. Environment variables used during install or build. 6. Skip logic and changed-path detection. # Preview deployments Preview deployments provide isolated versions of your application for testing changes before release. They use Preview environment variables and do not replace the deployment serving your production domains. Branch previews [#branch-previews] When you push to a non-production branch, Easel creates a Preview deployment for that commit. A branch may have two related URLs: * A unique deployment URL for the specific commit * A stable branch preview URL that points to the latest Ready deployment for the branch When a newer deployment reaches Ready, Easel updates the branch preview URL to point to it. The older deployment remains available through its unique deployment URL while that deployment stays Ready. While a newer deployment is building or if it fails, the stable branch URL continues serving the previous Ready deployment. Pull request previews [#pull-request-previews] When you push a branch that has an open pull request in the connected repository, Easel associates a pull request preview URL with the Preview deployment. Branch and pull request URLs are separate aliases. They may point at the same underlying Ready deployment, but they are different hostnames: * Branch: `…-git-{branch}-…` * Pull request: `…-pr-{number}-…` A pull request preview is useful to: * Review UI changes * Test application behavior * Share work with teammates * Run automated checks against a deployed version * Validate integrations before merging The Git provider integration can post the preview URL and deployment status on the pull request and related commits. Updating previews [#updating-previews] Each new commit creates a new deployment. Easel updates stable preview URLs only after the new deployment reaches Ready. Failed or canceled deployments do not replace the last working preview. The unique URL for an older deployment does not move to a newer deployment. Preview environment variables [#preview-environment-variables] Preview deployments use variables configured for the Preview environment. Use Preview variables for services that should not affect production, such as: * Test databases * Sandbox API credentials * Preview-only feature flags * Non-production callback URLs For branch-specific values, set a Preview branch override in **Project settings → Environment Variables**. Forks and untrusted code [#forks-and-untrusted-code] Easel creates Git deployments from pushes to the connected repository. There is no separate fork-approval or secret-stripping gate for forked pull requests. Treat Preview configuration as untrusted-input space: * Keep Production secrets out of Preview variables * Prefer Preview branch overrides only where needed * Enable **Deployment Protection** in project settings when previews must not be public Preview access [#preview-access] By default, preview hostnames are publicly reachable. Enable **Deployment Protection** in project settings so visitors to default Easel hostnames must authenticate before they see the app. Custom production domains can stay public while default hostnames remain gated. Preview responses also send `X-Robots-Tag` so crawlers skip indexing. Closing or deleting a branch [#closing-or-deleting-a-branch] Deleting a Git branch or closing a pull request does not delete existing deployments. * Unique deployment URLs continue to resolve while the deployment remains Ready * Stable branch and pull request aliases stop receiving new updates when no newer Ready deployment is assigned * You can still promote a Ready Preview deployment to production (rebuild) or inspect it in deployment history Related guides [#related-guides] * [Deployment URLs](/docs/deployments/urls) * [Git deployments](/docs/deployments/git) * [Promoting deployments](/docs/deployments/promote) # Production releases A Production release is the workflow that moves validated application output onto your production domains. Creating a Production deployment is not the same as serving Production traffic. Release flow [#release-flow] 1. **Build with Production configuration.** A Production deployment uses Production environment variables and settings. See [Environments](/docs/deployments/environments). 2. **Reach Ready.** A successful build becomes Ready. Ready means the artifact can be assigned; it does not mean it is Current. 3. **Understand Staged vs Current.** When auto-assign is off, a Ready Production deployment stays **Staged** until you promote it. The **Current** deployment is the one serving production domains. 4. **Validate the immutable deployment URL.** Open the deployment URL and check critical paths before promotion. See [Deployment URLs](/docs/deployments/urls). 5. **Promote to production domains.** Instant promote reuses the Ready artifact with no rebuild. See [Promoting deployments](/docs/deployments/promote). 6. **Observe the release.** Confirm requests, errors, and cache behavior in [Observability](/docs/observability). 7. **Roll back without rebuilding.** Instant rollback points production domains at a previous Ready Production deployment. See [Rolling back deployments](/docs/deployments/rollback). Automatic assignment [#automatic-assignment] When **Auto-assign production domains** is on (the default), a new Ready Production deployment becomes Current after build. If the build fails, production traffic stays on the existing Current deployment. Related pages [#related-pages] * [Promoting deployments](/docs/deployments/promote) * [Rolling back deployments](/docs/deployments/rollback) * [Preview deployments](/docs/deployments/previews) * [Managing deployments](/docs/deployments/manage) # Promoting deployments Promotion changes which deployment receives production traffic. Promotion is not the same as creating or rebuilding a deployment. Instant promote reuses an existing Ready Production deployment. You need an **Owner** or **Admin** role. Members can view deployments but cannot promote. If the project is paused or the workspace is suspended, promotion is blocked. Production deployment and current deployment [#production-deployment-and-current-deployment] A **Production deployment** is built with Production configuration. The **current deployment** is the Production deployment currently serving the project's production domains. A Production deployment can be Ready without being Current. That state is **Staged**. Automatic production assignment [#automatic-production-assignment] When **Auto-assign production domains** is on (the default): 1. Easel creates a Production deployment 2. The deployment builds independently 3. Existing production traffic continues using the current deployment 4. The new deployment reaches Ready 5. Easel updates production routing to the new deployment 6. The previous deployment remains in deployment history If the new deployment fails, production traffic remains on the current deployment. Promote a staged Production deployment [#promote-a-staged-production-deployment] When auto-assign is off, Production builds finish as Staged: status Ready, not Current. Promote also restores a chronologically newer former Current after Instant Rollback. 1. Open the staged (or newer) Production deployment 2. Open the deployment actions menu and choose **Promote** 3. Confirm Production domains move to this deployment with no rebuild. If the project was rolled back, promoting also turns auto-assign back on. Promote a Preview deployment [#promote-a-preview-deployment] Preview and Production deployments may use different environment variables, build settings, and secrets. Easel does not route production traffic directly to a Preview artifact. Promoting a Preview deployment: 1. Creates a **new** Production deployment from the same source commit 2. Builds with current Production environment variables 3. Reuses the source deployment's project settings snapshot when available Steps: 1. Open a Ready Preview deployment 2. Open the deployment actions menu and choose **Promote to Production** 3. Confirm. Easel queues a production rebuild 4. Wait until the new deployment is Ready When the rebuild is Ready, production domains update if auto-assign is on. If auto-assign is off, the new build is Staged until you promote it. Call this workflow **Promote to Production** (rebuild), not instant promote. Promotion and caches [#promotion-and-caches] Promotion changes traffic assignment. It does not automatically purge ISR or runtime caches. Caches are keyed by deployment ID, so traffic to the newly Current deployment uses that deployment's cache namespace. Edge responses follow the newly aliased deployment's routes and assets. You can clear ISR or runtime cache for a deployment from the dashboard when you need an explicit purge. Permissions [#permissions] Only **Owner** and **Admin** roles can promote. Activity is recorded when production traffic moves. Related guides [#related-guides] * [Rolling back deployments](/docs/deployments/rollback) * [Environments](/docs/deployments/environments) # Rolling back deployments **Instant Rollback** changes production routing to a previous Ready Production deployment. Use it when a newer release is unhealthy and a previous release is known to work. Instant Rollback does not rebuild the application. You need an **Owner** or **Admin** role. Members can view deployments but cannot roll back. If the project is paused or the workspace is suspended, Instant Rollback is blocked. How Instant Rollback works [#how-instant-rollback-works] Instant Rollback: 1. Identifies a previous Ready Production deployment older than Current 2. Reassigns the production hostname and live custom domains to that deployment 3. Leaves the newer deployment in deployment history 4. Turns off **Auto-assign production domains** so a later production-branch build does not immediately overwrite the rollback 5. Records the rollback in project activity Environment variables and build output stay as they were when that earlier deployment was built. What rolls back [#what-rolls-back] Instant Rollback restores deployment-owned resources such as: * Static assets * Functions * Routing configuration * Framework output * Deployment metadata It does not automatically restore external state, including: * Databases * Third-party service changes * Queues * Object storage mutations * Secrets changed after the original deployment * External migrations Design database changes to remain compatible with both the new and previous application versions when fast rollback is required. Roll back from the dashboard [#roll-back-from-the-dashboard] From the project overview or deployment history: 1. Select a previous Ready Production deployment older than Current 2. Review its source revision and creation time 3. Choose **Instant Rollback** and confirm The deployment becomes Current without entering the build queue. Preview-only deployments are not eligible. After a rollback [#after-a-rollback] New pushes to the production branch still build, but they stay **Staged** and do not replace the rolled-back deployment until you: * Choose **Undo Rollback** on the project overview * Turn **Auto-assign production domains** back on in **Project settings → Environments** * Promote a staged or newer Ready Production deployment (that also re-enables auto-assign when the disable reason was a rollback) Roll forward [#roll-forward] To reverse a rollback without rebuilding, promote the newer Ready Production deployment again. See [Promoting deployments](/docs/deployments/promote). Rollback limitations [#rollback-limitations] Instant Rollback is unavailable when: * The deployment is not Ready Production * The deployment is not older than Current * The deployment has been spun down * The project is paused or the workspace is suspended * You lack Owner or Admin permissions Cache behavior [#cache-behavior] Instant Rollback does not automatically purge ISR or runtime caches. Caches are keyed by deployment ID, so traffic follows the rolled-back deployment's cache namespace. Edge responses follow the newly aliased deployment's routes and assets. Clear cache from the dashboard when you need an explicit purge. Related guides [#related-guides] * [Promoting deployments](/docs/deployments/promote) * [Environments](/docs/deployments/environments) # Deployment troubleshooting Use the deployment status and logs to determine which stage failed. A deployment did not start [#a-deployment-did-not-start] Check: * The repository is still connected * The Git provider webhook is active * The commit was pushed to the connected repository * The project is not blocked for that environment (paused projects block new production deploys; workspace suspension blocks all deploys) * **Skip unaffected deployments** did not mark the push as Skipped because files outside the project root changed only If the Git provider delivered the event but Easel did not create a deployment, include the repository, branch, commit SHA, and webhook delivery time when contacting support. A deployment is stuck in Queued [#a-deployment-is-stuck-in-queued] A deployment may remain queued while waiting for build capacity. Check the Easel status page and whether other builds in the workspace are still running. You can [cancel](/docs/deployments/manage) a queued deployment from the deployment detail page. A build failed [#a-build-failed] Open the build logs and locate the first relevant error. Common causes include: * Dependency installation failure * Unsupported runtime version * Missing environment variables * Invalid build command * Wrong root directory * Framework detection failure * Out-of-memory termination * Build timeout * Ignored build step exiting `0` (shows as Canceled, not Failed) Reproduce the build locally using the same runtime version and build command where possible. The deployment is Ready but returns an error [#the-deployment-is-ready-but-returns-an-error] A successful build does not guarantee successful runtime behavior. Check: * Function logs * Runtime environment variables * Routing configuration * External service connectivity * Domain assignment * Framework adapter compatibility Test the unique deployment URL before testing a custom domain. If the deployment URL works but the custom domain does not, investigate routing, DNS, or TLS rather than the build. See [Domain troubleshooting](/docs/domains/troubleshooting). The preview URL still shows an older version [#the-preview-url-still-shows-an-older-version] Determine which URL you are using: * A unique deployment URL never moves * A stable branch or pull request URL moves only after the latest deployment reaches Ready Check whether the newest deployment: * Is still building * Failed * Was canceled * Was skipped * Belongs to a different branch * Was created in a different project or workspace Production still serves the previous deployment [#production-still-serves-the-previous-deployment] Check whether the new Production deployment is: * Ready * Current * Staged and waiting for [manual promotion](/docs/deployments/promote) * Blocked because Instant Rollback turned auto-assign off * Associated with the expected production branch A Ready Production deployment does not become Current when auto-assign is off. A custom domain points to the wrong deployment [#a-custom-domain-points-to-the-wrong-deployment] Custom domains follow the current production deployment. Check: * The domain is assigned to the correct project * The expected deployment is Current * DNS points to Easel * TLS provisioning completed * No redirect sends traffic elsewhere * Cache behavior is not serving an older response Use the unique deployment URL to separate deployment problems from domain problems. A project is paused [#a-project-is-paused] While a project is paused: * Production hostnames return HTTP `503` with `PROJECT_PAUSED` * Preview hostnames keep working * New production deployments and production promote/rollback are blocked * Preview deployments can still be created An Owner or Admin can turn off **Pause production** in **Project settings**. A workspace is suspended [#a-workspace-is-suspended] Workspace suspension blocks deployments and traffic for every project in the workspace, including previews. Visitors see HTTP `503` with `WORKSPACE_SUSPENDED`. Workspace owners cannot clear suspension themselves; contact support or settle billing when the banner points there. Information to include in a support request [#information-to-include-in-a-support-request] Include: * Workspace and project * Deployment ID * Deployment URL * Git branch and commit SHA * Approximate time * Build or runtime logs * Reproduction steps * Whether the issue affects Preview, Production, or both # Deployment URLs Easel uses different URL types for immutable deployment identity and stable project workflows. All generated hostnames use the suffix `preview.easelusercontent.com` and are reachable over HTTPS. You can find them on a deployment's detail page in the **Domains** section. Unique deployment URL [#unique-deployment-url] Every successful deployment receives a unique URL tied to the Git commit hash (or a fallback identifier for non-Git deploys). A unique deployment URL identifies one specific deployment and never moves to a different deployment. Use it to: * Inspect a particular build * Compare two deployments * Link directly to a historical version * Diagnose behavior from a specific commit Pattern: ```text https://{project}-{commit9}-{scope}.preview.easelusercontent.com ``` Example for project **My App**, commit `a1b2c3d4e`, and workspace scope **acme**: ```text https://my-app-a1b2c3d4e-acme.preview.easelusercontent.com ``` The commit segment is the first 9 alphanumeric characters of the Git commit hash. Branch preview URL [#branch-preview-url] A branch preview URL is a stable alias for the latest Ready Preview deployment on a branch. Pattern: ```text https://{project}-git-{branch}-{scope}.preview.easelusercontent.com ``` Example for project **My App**, branch **feature/login**, and scope **acme**: ```text https://my-app-git-feature-login-acme.preview.easelusercontent.com ``` When a new commit on the branch deploys successfully, the branch URL moves to the new deployment. The branch URL does not move when the new deployment fails or is canceled. Pull request preview URL [#pull-request-preview-url] A pull request preview URL is a stable alias associated with a pull request number. Pattern: ```text https://{project}-pr-{number}-{scope}.preview.easelusercontent.com ``` Example for project **My App**, pull request **42**, and scope **acme**: ```text https://my-app-pr-42-acme.preview.easelusercontent.com ``` Branch and pull request URLs are separate aliases. They may point at the same Ready deployment when both apply, but they have different hostnames. Closing or merging the pull request does not delete existing deployments. The PR hostname stops receiving new updates when no newer Ready deployment is assigned to it. Production project URL [#production-project-url] Each project receives a default Easel production hostname. Pattern: ```text https://{project}-{scope}.preview.easelusercontent.com ``` Example for project **My App** and scope **acme**: ```text https://my-app-acme.preview.easelusercontent.com ``` The production project URL points to the current production deployment. When auto-assign is off, a Ready Production deployment can exist without updating this URL until you promote it. Custom domains [#custom-domains] Custom domains also point to the current production deployment. Examples: ```text https://example.com https://www.example.com ``` Assigning a custom domain changes routing. It does not create a new deployment. Configure domains in **Project settings → Custom Domains**. See [Domains](/docs/domains). URL behavior [#url-behavior] | URL type | Stable address | Target can change | Identifies one deployment | | ------------------------ | -------------- | ----------------- | ------------------------- | | Unique deployment URL | Yes | No | Yes | | Branch preview URL | Yes | Yes | No | | Pull request preview URL | Yes | Yes | No | | Production project URL | Yes | Yes | No | | Custom domain | Yes | Yes | No | URL updates [#url-updates] Stable URLs update after the target deployment reaches the assigning-domains stage and becomes Ready. During a new build: * The old Ready deployment continues serving traffic * The new deployment builds independently * Easel updates the stable URL only after the new deployment is publishable * A failed deployment leaves the stable URL unchanged Edge manifests refresh on a short TTL, so there can be a brief propagation period after routing changes. URL naming [#url-naming] Easel builds hostnames from slugified components: * Uppercase characters become lowercase * Non-alphanumeric characters become hyphens (`feature/login` → `feature-login`) * Consecutive hyphens collapse * Scope segments are capped for length * Subdomains are capped at 63 characters; Easel truncates the scope segment first so the project name stays readable If the slugified project name looks like a domain (for example `www-mycompany-com`), Easel shortens it to avoid browser anti-phishing warnings. Branch, commit, and scope segments are unchanged. Retention [#retention] A URL resolves only while its deployment remains Ready and the project remains available. * There is no plan-based deployment URL expiration today * Spinning down a deployment stops serving its unique URL * Pausing a project stops production URLs; preview URLs keep working * Suspending a workspace stops all project traffic, including previews See [Managing deployments](/docs/deployments/manage) and [Deployment troubleshooting](/docs/deployments/troubleshooting). Related guides [#related-guides] * [Preview deployments](/docs/deployments/previews) * [Environments](/docs/deployments/environments) * [Domains](/docs/domains) # Framework guides Deploy frontend and full-stack frameworks to Easel. Each guide documents the required setup, what the framework build produces, how that output maps to Easel infrastructure, supported capabilities, and known limitations. Full-stack frameworks [#full-stack-frameworks] Full-stack frameworks produce both static assets and server-side application code. Easel deploys each part of the build to the appropriate platform resource. Next.js [#nextjs] App Router and Pages Router applications with support for server rendering, streaming, Server Actions, ISR, middleware, and image optimization. **Production-ready** · **Automatic setup** [Read the Next.js guide](/docs/framework-guides/nextjs) TanStack Start [#tanstack-start] Full-stack React applications with server functions, streaming, API routes, and prerendering through Nitro. **Production-ready** · **Nitro plugin required** [Read the TanStack Start guide](/docs/framework-guides/tanstack-start) Nuxt [#nuxt] Vue applications with server rendering, prerendering, Nitro server routes, middleware, and route rules. **Production-ready** · **Automatic setup** [Read the Nuxt guide](/docs/framework-guides/nuxt) React Router [#react-router] React Router v7 framework-mode applications with loaders, actions, server rendering, resource routes, and streaming. **Production-ready** · **Deployment preset required** [Read the React Router guide](/docs/framework-guides/react-router) SvelteKit [#sveltekit] Svelte applications with server rendering, endpoints, form actions, hooks, streaming, and prerendering. **Production-ready** · **Adapter required** [Read the SvelteKit guide](/docs/framework-guides/sveltekit) Static and client-rendered applications [#static-and-client-rendered-applications] Static applications produce HTML, JavaScript, CSS, and other browser-ready assets without a persistent application server. Vite [#vite] Static sites and client-rendered single-page applications built with React, Vue, Preact, Solid, Svelte, and other Vite integrations. **Production-ready** · **Automatic setup** [Read the Vite guide](/docs/framework-guides/vite) Compare frameworks [#compare-frameworks] | Framework | Rendering | Required setup | Server runtime | Support | | ------------------------------------------------------- | -------------------------------- | ----------------- | -------------- | ---------------- | | [Next.js](/docs/framework-guides/nextjs) | Static, SSR, streaming, ISR, PPR | None | Node.js | Production-ready | | [TanStack Start](/docs/framework-guides/tanstack-start) | SSR, streaming, prerendering | Nitro plugin | Node.js | Production-ready | | [Nuxt](/docs/framework-guides/nuxt) | Static, SSR, Nitro routes | None | Node.js | Production-ready | | [React Router](/docs/framework-guides/react-router) | SSR, loaders, actions | Deployment preset | Node.js | Production-ready | | [SvelteKit](/docs/framework-guides/sveltekit) | Static, SSR, endpoints | Adapter | Node.js | Production-ready | | [Vite](/docs/framework-guides/vite) | Static and client-rendered | None | None | Production-ready | The table describes the primary deployment path for each framework. Individual guides provide the complete capability matrix, version baseline, runtime behavior, and known limitations. What framework support means [#what-framework-support-means] Framework support on Easel covers more than running a build command. A documented framework integration defines how the framework’s generated output maps to Easel’s CDN, functions, routing, cache, image, and observability systems. Build integration [#build-integration] Easel detects the framework, selects the appropriate build path, runs the production build, and validates that deployable output was generated. When an adapter or preset is required, the framework guide identifies the exact package and configuration. Runtime integration [#runtime-integration] Dynamic framework features are mapped to the appropriate Easel runtime. Depending on the framework, this can include: * Server-rendered routes * API handlers * Server Actions * Loaders and actions * Form actions * Middleware * Streaming responses * Background work registered by the framework Platform integration [#platform-integration] Framework-native behavior connects to Easel platform services where supported. This can include: * CDN caching * Runtime caching * Path-based and tag-based revalidation * Image optimization * Environment variables * Preview deployments * Logs, metrics, and traces * WAF and attack protection Compatibility testing [#compatibility-testing] Each framework guide identifies: * The most recently tested framework version * Required packages and configuration * Supported capabilities * Features that require additional setup * Partial or experimental behavior * Known unsupported features How framework builds map to Easel [#how-framework-builds-map-to-easel] Different frameworks produce different combinations of static files, server handlers, routing metadata, and cached output. Easel analyzes that output and deploys each component to the appropriate part of the platform. ```text Framework build │ ├── Static assets ─────────▶ Easel CDN ├── Server routes ─────────▶ Easel Functions ├── Middleware ────────────▶ Request pipeline ├── Cached output ─────────▶ Runtime Cache ├── Image requests ────────▶ Image optimization └── Telemetry ─────────────▶ Observability ``` The exact deployment topology depends on the framework. A Vite application may deploy entirely to the CDN. A Next.js application may also produce server functions, middleware, cached route output, and image-optimization requests. Each framework guide includes a **What Easel deploys** section that documents this mapping explicitly. Support levels [#support-levels] Framework-level support uses the following statuses. | Status | Meaning | | -------------------- | --------------------------------------------------------------------------------------------------------- | | **Production-ready** | Easel maintains and tests the documented deployment path for production use | | **Preview** | The integration is available, but some capabilities or interfaces may still change | | **Community** | The framework can deploy through a compatible output, but Easel does not maintain a dedicated integration | | **Not documented** | Detection or deployment may work, but Easel does not currently provide a compatibility commitment | Individual framework capabilities use a separate status vocabulary. | Capability status | Meaning | | -------------------------------- | ------------------------------------------------------------------------- | | **Supported** | The capability is expected to work without framework-specific limitations | | **Supported with configuration** | The capability works after applying the documented setup | | **Partial** | Only the documented subset or behavior is supported | | **Experimental** | The capability works through an evolving framework or platform interface | | **Unsupported** | The capability is not available through the current deployment path | A feature is not marked partial merely because it requires configuration. Using an unlisted framework [#using-an-unlisted-framework] Easel can deploy many projects that are not listed on this page. Static sites and applications that produce browser-ready HTML, JavaScript, CSS, and assets can often use the standard static deployment path. Frameworks that emit a compatible server deployment format may also build successfully. However, an unlisted framework does not currently have: * A documented compatibility contract * A maintained capability matrix * A tested version baseline * Framework-specific runtime guarantees * Published known limitations For a project that produces only static output, start with the [Vite guide](/docs/framework-guides/vite). For a framework that produces server output, review the generated deployment artifacts and contact Easel before relying on the integration for production workloads. [Request a framework guide](/support) Moving from another platform [#moving-from-another-platform] Existing applications can often move to Easel without changing their framework architecture. Easel supports common framework adapters, presets, build conventions, and compatible deployment configuration from other platforms. The migration guide covers: * Importing an existing repository * Build and install settings * Environment variables * Domains and DNS * Redirects and rewrites * Functions and runtime behavior * Caching and revalidation * Preview deployments * Observability * Rollback and production cutover [Read the migration guide](/docs/migration) · [Migrate from Vercel](/docs/migration/vercel) · [Migrate from Netlify](/docs/migration/netlify) Compatibility policy [#compatibility-policy] Easel tests each documented integration against representative applications covering the framework capabilities listed in its guide. The version shown on an individual guide is the most recently verified baseline. Nearby stable releases may also work, but newly released or experimental framework features can require an Easel runtime or integration update. Before upgrading a production application: 1. Create a preview deployment with the new framework version. 2. Verify builds, dynamic routes, caching, middleware, and environment variables. 3. Review the framework guide for new limitations or configuration changes. 4. Promote the deployment only after application-specific testing. Material compatibility changes are documented in Easel release notes and reflected in the corresponding framework guide. # Next.js on Easel At a glance [#at-a-glance] | | | | ----------------------------- | ----------------------------------------------------- | | **Support level** | Production-ready | | **Routers** | App Router and Pages Router | | **Rendering** | Static, SSR, streaming, ISR, and Partial Prerendering | | **Server runtime** | Node.js | | **Adapter required** | No | | **Automatic detection** | Yes | | **Most recently tested with** | Next.js 16.2.x | Deploy a Next.js application [#deploy-a-nextjs-application] Easel detects Next.js projects automatically. In most cases, an existing application can be deployed without changing its framework configuration. [Create a project](/docs/getting-started) and connect the project’s Git repository, or [deploy with the CLI](/docs/cli/deploy): ```bash easel deploy ``` Easel detects the package manager, installs dependencies, runs the Next.js production build, and provisions the infrastructure required by the build output. The default build command is: ```bash next build ``` You can override the install command, build command, root directory, Node.js version, and environment variables in your project settings. Why run Next.js on Easel? [#why-run-nextjs-on-easel] Easel provides a Vercel-compatible deployment path while keeping the underlying infrastructure visible and portable. A Next.js deployment includes: * Immutable static assets served through the Easel CDN * Regional functions for dynamic rendering and server-side code * Distributed storage for the Next.js route and data caches * Path-based and tag-based cache revalidation * Image optimization for `next/image` * Preview deployments for every branch and pull request * Built-in logs, metrics, traces, WAF, and attack protection You can inspect function execution, cache behavior, request routing, and resource usage from the Easel dashboard. What Easel deploys [#what-easel-deploys] Easel analyzes the Next.js build output and maps each part of the application to the appropriate platform resource. | Next.js output | Easel resource | | ---------------------------------------------- | ---------------------------- | | Static pages | CDN assets | | JavaScript, CSS, fonts, and other static files | CDN assets | | Server Components and SSR routes | Easel Functions | | Route Handlers and API Routes | Easel Functions | | Server Actions | Easel Functions | | Middleware | Edge request pipeline | | ISR and cached route output | Runtime Cache and CDN | | `next/image` requests | Image optimization service | | OpenTelemetry instrumentation | Easel observability pipeline | Static assets are deployed immutably and can remain available across deployments. Dynamic application code runs in functions and scales independently from the CDN. Supported features [#supported-features] | Feature | Support | Notes | | -------------------------------- | ------------- | ------------------------------------------------------------------------- | | App Router | Supported | Includes layouts, Server Components, loading states, and error boundaries | | Pages Router | Supported | Includes pages, API Routes, and data-fetching methods | | Static Site Generation | Supported | Generated pages are deployed to the CDN | | Server-Side Rendering | Supported | Dynamic routes run in Easel Functions | | React Server Components | Supported | Supported through the App Router | | Streaming | Supported | Dynamic responses are streamed without waiting for the complete render | | Server Actions | Supported | Executed by the application’s server function | | Route Handlers | Supported | Includes standard and streaming responses | | API Routes | Supported | Pages Router API Routes run in functions | | Incremental Static Regeneration | Supported | Includes time-based and on-demand revalidation | | Cache Components | Supported | Backed by Easel’s distributed Runtime Cache | | Partial Prerendering | Supported | Static shells and dynamic regions are deployed together | | `revalidatePath` | Supported | Invalidates matching route-cache entries | | `revalidateTag` | Supported | Invalidates data by cache tag | | `updateTag` | Supported | Expires tagged data for read-your-own-writes behavior | | `after()` | Supported | Work may continue after the response within the function duration limit | | Middleware | Supported | Runs in Easel’s request pipeline before the application route | | Node.js runtime | Supported | Used for server-rendered routes and server code | | Edge runtime APIs | Supported | API compatibility does not imply execution at every CDN location | | `next/image` | Supported | Requests use Easel’s image optimization service | | Redirects, rewrites, and headers | Supported | Includes rules defined in `next.config` | | Internationalized routing | Supported | Includes domain and subpath routing | | Draft Mode | Supported | Preview cookies are forwarded to the application runtime | | OpenTelemetry | Supported | Framework telemetry can be exported through Easel | | Turbopack builds | Supported | Used when enabled by the selected Next.js version | | Static export | Supported | Deploys the exported application as a static site | | WebSockets | Not supported | Function requests must use HTTP request-response semantics | Experimental Next.js features may require additional platform work when their deployment contract changes. Test experimental features in a preview deployment before promoting them to production. Rendering [#rendering] Static rendering [#static-rendering] Routes generated during `next build` are deployed as immutable CDN assets. Requests for these routes do not invoke a function unless Next.js requires runtime revalidation or dynamic behavior. This includes: * App Router routes rendered statically * Pages Router routes using `getStaticProps` * Dynamic routes generated with `generateStaticParams` * Fully static metadata and assets Server rendering [#server-rendering] Routes that require request-time data run in Easel Functions. This includes routes that use request APIs such as: ```ts await cookies() await headers() ``` It also includes Pages Router routes using `getServerSideProps`, dynamic Route Handlers, API Routes, and uncached server-side data access. Server Components and streaming responses run through Easel’s Next.js production runtime. Response chunks are forwarded as they are produced rather than buffered until rendering finishes. Partial Prerendering [#partial-prerendering] Partial Prerendering combines a static application shell with dynamic content behind Suspense boundaries. Easel deploys the generated static shell to its caching layer and retains the postponed rendering state required by Next.js. On a request, Easel can begin returning the shell while the application function resumes and streams the dynamic portions of the route. The shell and its corresponding rendering state are updated together when the route is revalidated. Caching and revalidation [#caching-and-revalidation] Easel provides shared cache storage for Next.js applications running across multiple function instances. The integration supports: * Full Route Cache * Data Cache * Cache Components * Time-based revalidation * Path-based revalidation * Tag-based revalidation * Background cache regeneration Cached data is not limited to the local filesystem of a single function instance. See [Store and expire data in Runtime Cache](/docs/runtime-cache) and [Invalidate and revalidate cached content](/docs/cdn/revalidation). Time-based revalidation [#time-based-revalidation] Use the standard Next.js APIs: ```ts const products = await fetch("https://api.example.com/products", { next: { revalidate: 300, }, }); ``` The cached response can be reused for five minutes before Next.js regenerates it. You can also configure a route-level revalidation interval: ```ts export const revalidate = 300; ``` Tag-based revalidation [#tag-based-revalidation] Attach tags to cached data: ```ts const products = await fetch("https://api.example.com/products", { next: { tags: ["products"], }, }); ``` Invalidate the data from a Server Action or Route Handler: ```ts import { revalidateTag } from "next/cache"; export async function POST() { revalidateTag("products"); return Response.json({ revalidated: true, }); } ``` Easel coordinates invalidation across active CDN and function instances. Path-based revalidation [#path-based-revalidation] Invalidate a route with `revalidatePath`: ```ts "use server"; import { revalidatePath } from "next/cache"; export async function updateProduct() { await saveProduct(); revalidatePath("/products"); } ``` Work after the response [#work-after-the-response] Next.js supports scheduling work with `after()`: ```ts import { after } from "next/server"; export async function POST(request: Request) { const event = await request.json(); after(async () => { await recordAnalyticsEvent(event); }); return Response.json({ accepted: true, }); } ``` Easel keeps the invocation active while registered work is running. The response can be returned before that work finishes, but the invocation continues consuming function duration until the work completes or the route reaches its maximum duration. Use a durable background-work system for jobs that must survive function termination, require retries, or may exceed the request’s execution limit. Middleware [#middleware] Next.js middleware runs in Easel’s edge request pipeline before the matching application route. ```ts import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; export function middleware(request: NextRequest) { if (!request.cookies.has("session")) { return NextResponse.redirect(new URL("/login", request.url)); } return NextResponse.next(); } export const config = { matcher: ["/dashboard/:path*"], }; ``` Middleware can perform rewrites, redirects, authentication checks, header changes, and other request-routing logic. Middleware compatibility describes the Next.js runtime APIs available to your code. It does not mean that every middleware request executes inside every CDN point of presence. See [Functions](/functions) for execution-location details. Image optimization [#image-optimization] The Next.js `` component works without additional configuration: ```tsx import Image from "next/image"; export default function Page() { return ( Mountains ); } ``` Image transformation requests are handled by Easel’s image optimization service and cached through the CDN. For remote images, configure the allowed sources in `next.config.ts`: ```ts import type { NextConfig } from "next"; const nextConfig: NextConfig = { images: { remotePatterns: [ { protocol: "https", hostname: "images.example.com", }, ], }, }; export default nextConfig; ``` Environment variables [#environment-variables] Configure [environment variables](/docs/deployments/environments) in the Easel dashboard or CLI. Variables without a `NEXT_PUBLIC_` prefix are available only during the build and in server-side code: ```ts const databaseUrl = process.env.DATABASE_URL; ``` Variables prefixed with `NEXT_PUBLIC_` can be included in browser bundles: ```ts const apiOrigin = process.env.NEXT_PUBLIC_API_ORIGIN; ``` Public variables are embedded during the build. Changing one requires a new deployment. Use separate values for production, preview, and development environments when a variable differs between deployment contexts. OpenTelemetry [#opentelemetry] Next.js instrumentation hooks can export telemetry through Easel’s observability pipeline. Create `instrumentation.ts` in the project root or `src` directory: ```ts export async function register() { if (process.env.NEXT_RUNTIME === "nodejs") { await import("./instrumentation.node"); } } ``` Use standard OpenTelemetry SDKs and instrumentation packages inside your Node.js registration module. Easel can collect application traces alongside request logs, function metrics, and platform spans. See [OpenTelemetry](/docs/observability/opentelemetry) for how Easel collects application spans into request Traces. Project configuration [#project-configuration] Easel detects the conventional Next.js settings automatically: | Setting | Default | | ------------------- | --------------------------------- | | Install command | Detected from the package manager | | Build command | Package script or `next build` | | Output directory | Detected from the Next.js build | | Development command | Package script or `next dev` | | Node.js version | Project or platform default | For monorepos, set the project root to the directory containing the Next.js application’s `package.json`. Do not set `output: "export"` unless the application is intentionally static. Easel supports the standard Next.js server output and deploys its dynamic routes automatically. Route duration [#route-duration] Set per-route maximum duration with Next.js route segment config: ```ts export const maxDuration = 60; ``` Easel reads `maxDuration` from the build output. The default is 30s when unset. The maximum is 800s. See [Function limits](/docs/functions/limits). Local development [#local-development] Continue using the framework’s normal development server: ```bash npm run dev ``` This provides the fastest development loop for framework behavior. Use Easel preview deployments to validate platform-specific behavior, including: * Production builds * Function execution * Middleware routing * Distributed caching * Image optimization * Environment variables * Function duration and resource limits * WAF and security rules A preview deployment uses the same deployment path as production and receives its own immutable URL. Known limitations [#known-limitations] WebSockets [#websockets] Next.js routes deployed to Easel Functions cannot accept long-lived WebSocket connections. Use an external WebSocket service or a dedicated persistent service for bidirectional connections. Standard streaming HTTP responses and Server-Sent Events remain separate from WebSockets and can be supported within function execution limits. Local filesystem [#local-filesystem] Function filesystems are ephemeral. Files written during an invocation are not guaranteed to exist in later invocations or on another instance. Use object storage, a database, or another durable service for persistent application data. Experimental features [#experimental-features] Next.js experimental APIs can change without preserving their deployment contracts. A feature available in the framework may require an Easel runtime or adapter update before all of its semantics are supported. The compatibility table reflects the most recently verified stable framework release. Troubleshooting [#troubleshooting] The project deploys as a static site [#the-project-deploys-as-a-static-site] Confirm that the application is running `next build` and that `output: "export"` is not set in `next.config` unless you intend to create a fully static deployment. A server environment variable is undefined [#a-server-environment-variable-is-undefined] Confirm that the variable is configured for the current deployment environment. Preview and production deployments can use different values. Variables added after a build require a new deployment. A public environment variable has the old value [#a-public-environment-variable-has-the-old-value] Variables beginning with `NEXT_PUBLIC_` are embedded into the browser bundle during the build. Redeploy the application after changing them. A remote image is rejected [#a-remote-image-is-rejected] Add the image host to `images.remotePatterns` in `next.config`. Cached content does not update [#cached-content-does-not-update] Confirm that the cached request or function has the expected path, tag, or revalidation interval. Also verify that the invalidation call runs successfully before the response completes. Code works locally but fails in production [#code-works-locally-but-fails-in-production] Run a preview deployment and inspect the build output and function logs. Production builds can expose differences involving environment variables, case-sensitive paths, native dependencies, dynamic route behavior, and access to the local filesystem. Compatibility policy [#compatibility-policy] Easel tests its Next.js integration against representative applications covering the App Router, Pages Router, server rendering, Server Components, Server Actions, streaming, middleware, caching, revalidation, images, and route handlers. Stable Next.js releases may work beyond the version listed at the top of this page, but the listed version is the most recently verified baseline. For an experimental Next.js feature or a newly released framework version, deploy a preview before upgrading the production application. Next steps [#next-steps] Shared getCache() across isolates Cache headers and edge behavior Move an existing Vercel app to Easel # Nuxt on Easel At a glance [#at-a-glance] | | | | ----------------------------- | --------------------------------------------------- | | **Support level** | Production-ready | | **Rendering** | Static, client-rendered, SSR, streaming, and hybrid | | **Server runtime** | Node.js | | **Server engine** | Nitro | | **Adapter required** | No | | **Automatic detection** | Yes | | **Most recently tested with** | Nuxt 3.15.x | Deploy a Nuxt application [#deploy-a-nuxt-application] Easel detects Nuxt projects automatically. An existing application can be deployed without changing its Nuxt configuration. Connect the project’s Git repository to Easel or deploy it with the CLI: ```bash easel deploy ``` Easel detects the package manager, installs dependencies, runs the Nuxt production build, and provisions the infrastructure required by the generated Nitro output. The default build command is: ```bash nuxt build ``` You can override the install command, build command, project root, Node.js version, and environment variables in your Easel project settings. Why run Nuxt on Easel? [#why-run-nuxt-on-easel] Easel deploys Nuxt’s portable Nitro output while keeping the resulting infrastructure visible and configurable. A Nuxt deployment includes: * Immutable public assets served through the Easel CDN * Regional functions for server-rendered pages and Nitro handlers * Support for Nuxt server routes and server middleware * Hybrid rendering through Nitro route rules * Distributed caching for cached server responses * Preview deployments for every branch and pull request * Built-in logs, metrics, traces, WAF, and attack protection You can inspect function execution, cache activity, request routing, and resource usage from the Easel dashboard. What Easel deploys [#what-easel-deploys] Nuxt uses Nitro as its server engine. During the production build, Nitro converts your application into a portable server entry, route metadata, prerendered pages, and public assets. Easel maps that output to the corresponding platform resources: | Nuxt output | Easel resource | | -------------------------- | ----------------------------- | | Files in `public/` | CDN assets | | Nuxt client bundles | CDN assets | | Prerendered pages | CDN assets | | Server-rendered pages | Easel Functions | | Routes in `server/api/` | Easel Functions | | Routes in `server/routes/` | Easel Functions | | Server middleware | Application function | | Nitro plugins | Application function | | Cached Nitro responses | Runtime Cache and CDN | | Redirects and headers | Request-routing configuration | Static files are deployed immutably. Dynamic rendering and server handlers run through the generated Nitro server entry. Nitro deployment preset [#nitro-deployment-preset] Do not manually select a Nitro deployment preset for an Easel production build. Easel selects the compatible hosting output when it runs the Nuxt build. Setting `nitro.preset`, `NITRO_PRESET`, or `SERVER_PRESET` yourself may produce output intended for another platform or runtime. A standard Easel configuration does not require a Nitro preset: ```ts title="nuxt.config.ts" export default defineNuxtConfig({ // No Nitro preset is required. }); ``` You can continue using other Nitro configuration options, including route rules, plugins, prerender settings, storage configuration, and server assets. Supported features [#supported-features] | Feature | Support | Notes | | --------------------------- | ------------- | -------------------------------------------------------------------------------------------------- | | Nuxt pages and layouts | Supported | Includes file-based routing, nested layouts, and error pages | | Server-side rendering | Supported | Dynamic pages run in Easel Functions | | Client-side rendering | Supported | Includes applications configured with `ssr: false` | | Static generation | Supported | Generated output is deployed to the CDN | | Hybrid rendering | Supported | Controlled through Nitro route rules | | Streaming responses | Supported | Nitro and h3 response streams are forwarded incrementally | | Server API routes | Supported | Includes handlers in `server/api/` | | Server routes | Supported | Includes handlers in `server/routes/` | | Server middleware | Supported | Runs inside the application function | | Nitro plugins | Supported | Loaded when the generated server entry starts | | Prerendering | Supported | Includes build-time route crawling and explicit prerender routes | | Route redirects | Supported | Includes redirects configured through route rules | | Route headers | Supported | Includes headers configured through route rules | | Route caching | Supported | Cached responses use Easel’s distributed Runtime Cache | | SWR route rules | Supported | Stale content may be served while regeneration runs | | ISR route rules | Supported | Cached route output can be regenerated after expiration | | Runtime configuration | Supported | Server and public runtime configuration are available | | Cookies and sessions | Supported | Standard Nitro and h3 cookie APIs are available | | Server-Sent Events | Supported | Subject to function duration and connection limits | | `@nuxt/image` | Partial | Static assets work normally; dynamic provider compatibility depends on the selected image provider | | Nitro storage | Partial | Ephemeral and supported external drivers work; local persistent storage does not | | Scheduled tasks | Not supported | Use an external scheduler to invoke an HTTP route | | WebSockets | Not supported | Easel Functions do not accept long-lived WebSocket upgrades | | Persistent local filesystem | Not supported | Function filesystems are ephemeral | Experimental Nuxt and Nitro capabilities can change their deployment contract. Validate experimental features in a preview deployment before promoting them to production. Rendering modes [#rendering-modes] Nuxt supports several rendering strategies within the same application. Easel deploys each route according to the output generated by Nuxt and Nitro. Server-side rendering [#server-side-rendering] Nuxt renders pages on the server by default: ```vue title="pages/products.vue" ``` Routes that require request-time execution run through the generated Nitro server entry in an Easel Function. Response streams are forwarded as Nitro produces them rather than waiting for the entire page to finish rendering. Client-side rendering [#client-side-rendering] To create a client-rendered application, disable server rendering: ```ts title="nuxt.config.ts" export default defineNuxtConfig({ ssr: false, }); ``` The generated HTML, JavaScript, CSS, and other assets are deployed to the CDN. Easel configures the deployment so application routes can fall back to the generated entry page where required. Server routes can still be deployed separately when they are included in the Nitro build. Static generation [#static-generation] Use Nuxt’s generation command for a static deployment: ```bash nuxt generate ``` Nuxt prerenders the application and writes the static output under `.output/public`. Easel deploys this output directly to the CDN. A static deployment does not include request-time server rendering. Any Nitro server routes required by the application must be deployed through the standard `nuxt build` path instead. Hybrid rendering [#hybrid-rendering] Use route rules to choose rendering and caching behavior for individual paths: ```ts title="nuxt.config.ts" export default defineNuxtConfig({ routeRules: { "/": { prerender: true, }, "/blog/**": { swr: 3600, }, "/dashboard/**": { ssr: true, }, "/admin/**": { ssr: false, }, }, }); ``` This allows one Nuxt application to combine static pages, cached dynamic pages, request-time rendering, and client-rendered sections. Route rules [#route-rules] Easel supports the principal Nitro and Nuxt route rules used for rendering, caching, redirects, and response headers. Prerender a route [#prerender-a-route] ```ts title="nuxt.config.ts" export default defineNuxtConfig({ routeRules: { "/about": { prerender: true, }, }, }); ``` The page is rendered during the build and deployed as a static CDN asset. Disable server rendering [#disable-server-rendering] ```ts title="nuxt.config.ts" export default defineNuxtConfig({ routeRules: { "/app/**": { ssr: false, }, }, }); ``` The route is rendered by the browser rather than by the Nuxt server. Add response headers [#add-response-headers] ```ts title="nuxt.config.ts" export default defineNuxtConfig({ routeRules: { "/assets/**": { headers: { "cache-control": "public, max-age=31536000, immutable", }, }, }, }); ``` Redirect a route [#redirect-a-route] ```ts title="nuxt.config.ts" export default defineNuxtConfig({ routeRules: { "/old-page": { redirect: { to: "/new-page", statusCode: 308, }, }, }, }); ``` Cache a server route [#cache-a-server-route] ```ts title="nuxt.config.ts" export default defineNuxtConfig({ routeRules: { "/api/products": { cache: { maxAge: 300, }, }, }, }); ``` Easel stores eligible cached responses in its distributed [Runtime Cache](/docs/runtime-cache) rather than relying on the local filesystem of one function instance. Serve stale content while revalidating [#serve-stale-content-while-revalidating] ```ts title="nuxt.config.ts" export default defineNuxtConfig({ routeRules: { "/blog/**": { swr: 3600, }, }, }); ``` Easel can return a cached response immediately after it becomes stale while Nitro regenerates the route in the background. The invocation performing regeneration remains subject to the route’s function duration limit. Incremental regeneration [#incremental-regeneration] ```ts title="nuxt.config.ts" export default defineNuxtConfig({ routeRules: { "/products/**": { isr: 600, }, }, }); ``` The generated route output can be cached and regenerated after the configured interval. Use preview deployments to verify caching behavior when combining multiple route rules or using experimental Nitro options. Server routes [#server-routes] Create an API endpoint under `server/api`: ```ts title="server/api/products.get.ts" export default defineEventHandler(async () => { return { products: [ { id: "starter", name: "Starter", }, { id: "pro", name: "Pro", }, ], }; }); ``` The route is available at: ```text /api/products ``` Nuxt server routes are bundled into the generated Nitro server application and run in Easel Functions. Dynamic routes [#dynamic-routes] Use a filename parameter for dynamic routes: ```ts title="server/api/products/[id].get.ts" export default defineEventHandler(async (event) => { const id = getRouterParam(event, "id"); return { id, }; }); ``` Request bodies [#request-bodies] Read a JSON body with Nitro’s standard h3 utilities: ```ts title="server/api/products.post.ts" export default defineEventHandler(async (event) => { const product = await readBody(event); return { created: true, product, }; }); ``` Response headers [#response-headers] ```ts title="server/api/products.get.ts" export default defineEventHandler((event) => { setResponseHeader(event, "cache-control", "public, max-age=60"); return { generatedAt: new Date().toISOString(), }; }); ``` Server middleware [#server-middleware] Place server middleware in `server/middleware`: ```ts title="server/middleware/request-id.ts" export default defineEventHandler((event) => { const requestId = getRequestHeader(event, "x-request-id") ?? crypto.randomUUID(); setResponseHeader(event, "x-request-id", requestId); event.context.requestId = requestId; }); ``` Server middleware runs as part of the Nitro application function before the matching server route or page render. Unlike CDN routing rules or Easel WAF rules, Nuxt server middleware requires application-function execution. Use Easel’s request-routing and security configuration for logic that should run before the application function is invoked. Nitro plugins [#nitro-plugins] Nitro plugins run when a new application function instance initializes: ```ts title="server/plugins/instrumentation.ts" export default defineNitroPlugin((nitroApp) => { nitroApp.hooks.hook("request", (event) => { event.context.startedAt = performance.now(); }); nitroApp.hooks.hook("afterResponse", (event) => { const startedAt = event.context.startedAt; if (typeof startedAt === "number") { console.log({ path: event.path, duration: performance.now() - startedAt, }); } }); }); ``` Initialization can run again whenever Easel starts a new function instance. Do not depend on a plugin running exactly once across the deployment. Connections and in-memory state created by a plugin belong to the individual function instance and are not shared globally. Runtime configuration [#runtime-configuration] Define private and public configuration in `nuxt.config.ts`: ```ts title="nuxt.config.ts" export default defineNuxtConfig({ runtimeConfig: { databaseUrl: "", apiSecret: "", public: { apiOrigin: "", }, }, }); ``` Set the production values using environment variables: ```text NUXT_DATABASE_URL NUXT_API_SECRET NUXT_PUBLIC_API_ORIGIN ``` Private runtime configuration is available only to server-side code: ```ts title="server/api/config.get.ts" export default defineEventHandler((event) => { const config = useRuntimeConfig(event); return { configured: Boolean(config.databaseUrl), }; }); ``` Public runtime configuration is also exposed to browser code: ```vue title="pages/index.vue" ``` Configure values separately for development, preview, and production environments in Easel. Do not place secrets under `runtimeConfig.public`. Prerendering [#prerendering] Configure Nitro’s prerenderer in `nuxt.config.ts`: ```ts title="nuxt.config.ts" export default defineNuxtConfig({ nitro: { prerender: { routes: ["/", "/about", "/pricing"], crawlLinks: true, }, }, }); ``` Prerendered routes are produced during the build and deployed as CDN assets. Routes discovered through link crawling must be reachable from another prerendered page. Add routes explicitly when they cannot be discovered during the build. You can also add routes programmatically: ```ts title="nuxt.config.ts" export default defineNuxtConfig({ hooks: { async "nitro:config"(nitroConfig) { const posts = await fetchPostSlugs(); nitroConfig.prerender?.routes?.push( ...posts.map((slug) => `/blog/${slug}`), ); }, }, }); ``` Build-time data sources must be reachable from Easel’s build environment. Caching [#caching] Nuxt and Nitro support multiple caching mechanisms. Easel provides distributed storage for supported server-response caches so cached values are not restricted to one function instance. Cached event handlers [#cached-event-handlers] ```ts title="server/api/products.get.ts" export default defineCachedEventHandler( async () => { return fetchProducts(); }, { maxAge: 300, name: "products", }, ); ``` Cached functions [#cached-functions] ```ts const getProducts = defineCachedFunction( async () => { return fetchProductsFromDatabase(); }, { maxAge: 300, name: "products", }, ); ``` Route-rule caching [#route-rule-caching] ```ts title="nuxt.config.ts" export default defineNuxtConfig({ routeRules: { "/api/catalog": { cache: { maxAge: 300, }, }, }, }); ``` Cache entries can be shared between active function instances when the generated Nitro integration uses Easel’s [Runtime Cache](/docs/runtime-cache) driver. Do not use the function’s local filesystem as a persistent cache. Nitro storage [#nitro-storage] Nitro exposes a storage abstraction that can use memory, the filesystem, Redis, object storage, and other drivers. In-memory storage is scoped to one active function instance: ```ts const storage = useStorage("cache"); ``` Values stored only in memory may disappear when an instance is stopped and are not automatically visible to other instances. Local filesystem storage is also ephemeral in Easel Functions. Use a supported external storage driver for durable application data: ```ts title="nuxt.config.ts" export default defineNuxtConfig({ nitro: { storage: { sessions: { driver: "redis", url: process.env.REDIS_URL, }, }, }, }); ``` Use Easel’s Runtime Cache for framework response caching and an external database or storage service for durable application state. Images [#images] Static image files in `public/` are deployed directly to the Easel CDN: ```vue ``` Nuxt Image can be added with: ```bash npx nuxt module add image ``` Then use `` or ``: ```vue ``` `@nuxt/image` supports several built-in and third-party providers. Compatibility depends on the provider selected by the application. Static source assets and remote providers that generate external image URLs do not require Easel-side transformation. Providers that expect a persistent local image server or provider-specific runtime behavior must be validated in a preview deployment. Consult the image provider’s documentation before relying on dynamic image transformation in production. Streaming [#streaming] Nitro server routes can return streaming responses: ```ts title="server/api/stream.get.ts" export default defineEventHandler(() => { const encoder = new TextEncoder(); return new ReadableStream({ async start(controller) { controller.enqueue(encoder.encode("Starting\n")); await new Promise((resolve) => { setTimeout(resolve, 500); }); controller.enqueue(encoder.encode("Finished\n")); controller.close(); }, }); }); ``` Easel forwards response chunks as they are produced. Streaming keeps the function invocation active until the stream closes. Long-running streams remain subject to the function’s maximum duration and connection limits. Use a persistent service for indefinite connections. Scheduled tasks [#scheduled-tasks] Nitro scheduled tasks are not currently registered automatically as Easel schedules. Expose the work through an authenticated server route: ```ts title="server/api/internal/cleanup.post.ts" export default defineEventHandler(async (event) => { const authorization = getRequestHeader(event, "authorization"); if (authorization !== `Bearer ${process.env.CRON_SECRET}`) { throw createError({ statusCode: 401, statusMessage: "Unauthorized", }); } await removeExpiredRecords(); return { completed: true, }; }); ``` Invoke that route using an external scheduler. The handler should be idempotent and should authenticate every scheduled request. WebSockets [#websockets] Easel Functions do not currently accept long-lived WebSocket upgrades. Do not rely on Nitro WebSocket handlers for application traffic deployed through the standard Nuxt function path. Use a dedicated WebSocket service or persistent application service for bidirectional connections. Streaming HTTP responses and Server-Sent Events use a different transport and can work within function duration and connection limits. Local filesystem [#local-filesystem] The function filesystem is ephemeral. Reading files bundled into the deployment is supported: ```ts const contents = await useStorage("assets:server").getItem( "templates/email.html", ); ``` Files created during an invocation are not guaranteed to exist on a later invocation or on another function instance. Use object storage, a database, or another durable service for: * User uploads * Generated documents * Session data * Application state * Persistent caches Project configuration [#project-configuration] Easel detects conventional Nuxt settings automatically: | Setting | Default | | ------------------- | --------------------------------- | | Install command | Detected from the package manager | | Build command | Package script or `nuxt build` | | Output directory | Detected from Nitro output | | Development command | Package script or `nuxt dev` | | Node.js version | Project or platform default | For monorepos, set the project root to the directory containing the Nuxt application’s `package.json` and `nuxt.config` file. Do not commit `.output` as a substitute for running the production build unless your deployment workflow intentionally uses a prebuilt artifact. Local development [#local-development] Continue using Nuxt’s normal development server: ```bash npm run dev ``` Nuxt development uses Nitro’s development runtime and provides hot module replacement for pages, components, server routes, and middleware. Use an Easel preview deployment to validate platform-specific behavior, including: * Production Nitro output * Function execution * Route-rule caching * Prerendered routes * Streaming * Runtime environment variables * Function duration and resource limits * WAF and security rules A preview deployment uses the same build and deployment path as production and receives its own immutable URL. Known limitations [#known-limitations] WebSockets [#websockets-1] Long-lived WebSocket upgrades are not supported by the standard Easel Functions deployment path. Scheduled tasks [#scheduled-tasks-1] Nitro tasks are not converted automatically into Easel schedules. Use an authenticated server route and an external scheduler. Persistent local storage [#persistent-local-storage] Memory and local filesystem storage are scoped to individual function instances and may be discarded at any time. Image providers [#image-providers] `@nuxt/image` provider compatibility varies. Static images work normally, but dynamic providers that depend on platform-specific transformation infrastructure must be tested individually. Provider-specific Nitro features [#provider-specific-nitro-features] Features designed specifically for another Nitro deployment provider may not behave the same way on Easel. Do not manually select another provider’s Nitro preset unless you are intentionally producing a prebuilt artifact whose output Easel supports. Experimental features [#experimental-features] Experimental Nuxt and Nitro features can change without maintaining the same deployment output. Validate them with a preview deployment before using them in production. Troubleshooting [#troubleshooting] Easel cannot find the server entry [#easel-cannot-find-the-server-entry] Make sure the project runs `nuxt build`, not a custom command that replaces or moves the `.output` directory. Remove manually configured `NITRO_PRESET`, `SERVER_PRESET`, or `nitro.preset` values unless they are specifically required by your deployment workflow. The project deploys without server rendering [#the-project-deploys-without-server-rendering] Confirm that the build command is `nuxt build` rather than `nuxt generate`. Also check that `ssr: false` is not set globally or for the affected route. A route rule does not apply [#a-route-rule-does-not-apply] Confirm that the path pattern matches the deployed URL and that a more specific rule is not overriding it. When using inline route rules, verify that the Nuxt experimental option required by your framework version is enabled. Define the rule in `nuxt.config` when the page path cannot be translated into a route-rule pattern. A server environment variable is undefined [#a-server-environment-variable-is-undefined] Confirm that the variable is configured for the current Easel environment. For Nuxt runtime configuration, use the corresponding `NUXT_` environment-variable name. Preview and production environments can have different values. Cached content does not update [#cached-content-does-not-update] Check the applicable `cache`, `swr`, or `isr` route rule and confirm that the route is being served through the intended rendering path. Deploy a preview and inspect the response headers, function logs, and cache activity. Prerendering misses a route [#prerendering-misses-a-route] Routes must either be listed explicitly, discovered through crawler links, or added programmatically during the build. Dynamic routes that are not linked from another prerendered page generally need to be supplied explicitly. Code works locally but fails after deployment [#code-works-locally-but-fails-after-deployment] Run a preview deployment and inspect the production build and function logs. Common causes include: * Missing runtime environment variables * Case-sensitive file paths * Native dependencies incompatible with the function runtime * Reliance on persistent local files * A manually selected Nitro preset * Build-time services that are inaccessible from Easel * Provider-specific Nuxt modules Compatibility policy [#compatibility-policy] Easel tests its Nuxt integration against representative applications covering: * Server-side rendering * Client-side rendering * Static generation * Hybrid route rules * API routes * Server middleware * Nitro plugins * Streaming * Runtime configuration * Prerendering * Response caching * Redirects and headers Stable Nuxt releases may work beyond the version listed at the top of this page, but the listed version is the most recently verified baseline. For a newly released Nuxt version, Nitro version, module, image provider, or experimental feature, create a preview deployment before upgrading the production application. # React Router on Easel At a glance [#at-a-glance] | | | | ----------------------------- | --------------------------------------------- | | **Support level** | Production-ready | | **Supported versions** | React Router 7 and 8 | | **Mode** | Framework Mode | | **Rendering** | Static, SPA, SSR, streaming, and prerendering | | **Server runtime** | Node.js | | **Required preset** | `@vercel/react-router` | | **Automatic detection** | Yes | | **Most recently tested with** | React Router 8.3.x and 7.18.x | React Router 8 is a non-breaking framework upgrade from React Router 7, but it raises the minimum versions of Node.js, React, and Vite. Existing React Router 7 applications can deploy without upgrading. Version requirements [#version-requirements] React Router 8 [#react-router-8] React Router 8 requires: * Node.js 22.22.0 or newer * React 19.2.7 or newer * Vite 7 or newer * An ESM project React Router 8 enables the behaviors previously introduced through React Router 7’s `future.v8_*` flags by default. React Router 7 [#react-router-7] React Router 7 supports older Node.js, React, and Vite baselines. Some newer framework behaviors, including stable middleware behavior, require future flags depending on the version you use. For the most consistent migration path to React Router 8, enable the available `future.v8_*` flags and resolve any compatibility issues before upgrading. Deploy a React Router application [#deploy-a-react-router-application] Easel supports React Router applications using Framework Mode. Install the deployment preset: ```bash npm install -D @vercel/react-router ``` Add the preset to `react-router.config.ts`: ```ts title="react-router.config.ts" import type { Config } from "@react-router/dev/config"; import { vercelPreset } from "@vercel/react-router/vite"; export default { presets: [vercelPreset()], } satisfies Config; ``` Then [create a project](/docs/getting-started) and connect the project’s Git repository, or [deploy with the CLI](/docs/cli/deploy): ```bash easel deploy ``` Easel detects the React Router project, runs its production build, and provisions the static and server resources described by the generated deployment output. You can override the install command, build command, root directory, Node.js version, and environment variables in your project settings. Why run React Router on Easel? [#why-run-react-router-on-easel] Easel provides a Vercel-compatible React Router deployment path with visible infrastructure, portable application conventions, and integrated edge services. A React Router deployment includes: * Immutable browser assets served through the Easel CDN * Regional functions for server rendering, loaders, and actions * Streaming HTTP responses * Route-aware server bundles * CDN caching controlled through standard response headers * Preview deployments for branches and pull requests * Built-in logs, metrics, traces, WAF, and attack protection You can inspect deployed functions, request execution, cache behavior, and resource usage from the Easel dashboard. What Easel deploys [#what-easel-deploys] The React Router preset exposes the application’s route structure and generates the deployment output Easel consumes. | React Router output | Easel resource | | -------------------------- | ----------------------------- | | Browser JavaScript and CSS | CDN assets | | Public files | CDN assets | | Prerendered pages | CDN assets | | Server-rendered routes | Easel Functions | | Loaders and actions | Easel Functions | | Resource routes | Easel Functions | | Server middleware | Application function pipeline | | Streaming responses | Streaming function responses | | SPA fallback | CDN routing configuration | The preset allows server bundles to be split according to the application’s route structure rather than placing the entire server application in one undifferentiated function bundle. Function CPU, memory, and duration remain controlled by Easel project settings and framework route metadata (such as `maxDuration`). Functions run in US East. Supported features [#supported-features] | Feature | Support | Notes | | ------------------------ | ------------- | --------------------------------------------------------------- | | Framework Mode | Supported | Uses the React Router Vite plugin | | React Router 8 | Supported | Requires the React Router 8 runtime baseline | | React Router 7 | Supported | Future flags may be required for newer behavior | | Server-side rendering | Supported | Dynamic document requests run in Easel Functions | | Client-side navigation | Supported | Data requests are handled by the application function | | Loaders | Supported | Run during document and applicable client-navigation requests | | Actions | Supported | Handles mutations and form submissions | | Nested routes | Supported | Includes nested layouts and data loading | | Route error boundaries | Supported | Framework error responses are preserved | | Streaming | Supported | Includes Suspense and deferred route data | | Resource routes | Supported | Return JSON, files, feeds, and other non-UI responses | | Server middleware | Supported | Stable in v8. Version-dependent setup in v7 | | Client middleware | Supported | Runs in the browser during client navigations | | Route context | Supported | Includes type-safe `RouterContextProvider` behavior | | Static prerendering | Supported | Generated pages deploy to the CDN | | SPA mode | Supported | Deploys a client-rendered application with route fallback | | HTTP redirects | Supported | Includes redirects returned by loaders, actions, and middleware | | Response headers | Supported | Includes route `headers` exports | | Cookie sessions | Supported | Standard `Cookie` and `Set-Cookie` headers are preserved | | File uploads | Supported | Subject to function request and duration limits | | OpenTelemetry | Supported | Instrument application server code using standard SDKs | | Custom server entry | Supported | Must expose the expected Web API request handler | | WebSockets | Not supported | Function routes use HTTP request-response semantics | | Persistent local storage | Not supported | Function filesystems are ephemeral | Server rendering [#server-rendering] Server rendering is enabled by default in Framework Mode. A route can load data on the server and pass it to the component through generated route types: ```tsx title="app/routes/product.tsx" import type { Route } from "./+types/product"; export async function loader({ params }: Route.LoaderArgs) { const product = await getProduct(params.productId); if (!product) { throw new Response("Not found", { status: 404, }); } return { product, }; } export default function Product({ loaderData, }: Route.ComponentProps) { return (

{loaderData.product.name}

{loaderData.product.description}

); } ``` For an initial document request, Easel invokes the application function and streams the rendered response to the client. After hydration, React Router can request loader data without rerendering the complete document. Those data requests are routed to the same deployed application runtime. Actions and forms [#actions-and-forms] React Router actions handle mutations and form submissions: ```tsx title="app/routes/new-project.tsx" import { Form, redirect, } from "react-router"; import type { Route } from "./+types/new-project"; export async function action({ request, }: Route.ActionArgs) { const formData = await request.formData(); const project = await createProject({ name: String(formData.get("name")), }); return redirect(`/projects/${project.id}`); } export default function NewProject() { return (
); } ``` The browser submits the form to the Easel Function running the React Router server application. Redirects, validation responses, cookies, and error boundaries continue through the framework’s normal response flow. Streaming [#streaming] React Router can begin returning a document before all asynchronous data is available. A loader can return unresolved promises: ```tsx title="app/routes/dashboard.tsx" import { Await, } from "react-router"; import { Suspense } from "react"; import type { Route } from "./+types/dashboard"; export async function loader() { return { account: await getAccount(), activity: getRecentActivity(), }; } export default function Dashboard({ loaderData, }: Route.ComponentProps) { return (

{loaderData.account.name}

Loading activity…

}> {(activity) => ( )}
); } ``` Easel forwards response chunks as React Router produces them rather than waiting for the entire render to finish. The function remains active until the stream closes or the invocation reaches its maximum duration. Middleware [#middleware] React Router middleware can run before and after the matched route’s handlers. Use middleware for authentication, request context, logging, instrumentation, response headers, and session handling. ```tsx title="app/routes/dashboard.tsx" import { createContext, redirect, } from "react-router"; import type { Route } from "./+types/dashboard"; type User = { id: string; email: string; }; export const userContext = createContext(); const requireUser: Route.MiddlewareFunction = async ({ request, context }, next) => { const user = await authenticateRequest(request); if (!user) { throw redirect("/login"); } context.set(userContext, user); const response = await next(); response.headers.set("X-Authenticated", "true"); return response; }; export const middleware: Route.MiddlewareFunction[] = [ requireUser, ]; export async function loader({ context, }: Route.LoaderArgs) { return { user: context.get(userContext), }; } ``` Server middleware runs inside the deployed React Router application function. It is distinct from Easel’s platform-level edge request pipeline and WAF. Middleware in React Router 8 [#middleware-in-react-router-8] Middleware behavior is enabled by default in React Router 8. No `future.v8_middleware` flag is required. Middleware in React Router 7 [#middleware-in-react-router-7] React Router 7 versions that expose middleware behind a future flag require it in `react-router.config.ts`: ```ts title="react-router.config.ts" import type { Config } from "@react-router/dev/config"; import { vercelPreset } from "@vercel/react-router/vite"; export default { presets: [vercelPreset()], future: { v8_middleware: true, }, } satisfies Config; ``` The exact future flags available depend on the installed React Router 7 release. Request context [#request-context] Use React Router’s context APIs to make request-specific dependencies available to middleware, loaders, and actions. A custom server entry can seed the request context before React Router handles the request: ```ts title="app/entry.server.ts" import { createRequestHandler, RouterContextProvider, } from "react-router"; import { userContext } from "./context"; import * as build from "virtual:react-router/server-build"; const handleRequest = createRequestHandler(build); export default async function handler( request: Request, ) { const context = new RouterContextProvider(); const user = await authenticateRequest(request); if (user) { context.set(userContext, user); } return handleRequest(request, context); } ``` Use request context for dependencies that belong to one invocation, such as authenticated users, database transaction handles, request identifiers, or platform metadata. Do not use module-level mutable state as a substitute for request context. A function instance may process multiple requests during its lifetime. Resource routes [#resource-routes] A route that does not export a default component can return a non-HTML response: ```ts title="app/routes/api.health.ts" import type { Route } from "./+types/api-health"; export async function loader({ request, }: Route.LoaderArgs) { return Response.json({ ok: true, timestamp: new Date().toISOString(), }); } ``` Resource routes can return: * JSON APIs * XML and RSS feeds * Generated files * Redirects * Streaming responses * Webhook acknowledgements They run within the same function execution model as document loaders and actions. Response caching [#response-caching] React Router does not impose one application caching policy. Routes can control browser and CDN behavior using standard HTTP response headers. Export a `headers` function from a route: ```ts title="app/routes/products.tsx" import type { Route } from "./+types/products"; export function headers( _: Route.HeadersArgs, ) { return { "Cache-Control": "public, s-maxage=60, stale-while-revalidate=300", }; } export async function loader() { return { products: await listProducts(), }; } ``` In this example: * Browsers follow the browser caching directives in the response * Easel’s CDN can reuse the response for 60 seconds * A stale response can remain available while it is refreshed for up to five minutes Do not publicly cache responses containing user-specific or session-specific data. For personalized routes, use: ```http Cache-Control: private, no-store ``` A route’s caching policy should account for every header, cookie, query parameter, and authentication state that can change its response. See [Cache responses at the edge](/docs/cdn/caching) and [Invalidate and revalidate cached content](/docs/cdn/revalidation). Static prerendering [#static-prerendering] React Router can generate selected routes during the production build. Configure prerendered paths in `react-router.config.ts`: ```ts title="react-router.config.ts" import type { Config } from "@react-router/dev/config"; import { vercelPreset } from "@vercel/react-router/vite"; export default { presets: [vercelPreset()], async prerender() { return [ "/", "/about", "/pricing", ]; }, } satisfies Config; ``` Easel deploys the generated HTML and browser assets to the CDN. Prerendered routes do not require a function invocation unless the application later navigates to server-backed data or another dynamic route. React Router 8 uses its Vite environment-based prerendering flow. React Router 7 may use a version-dependent implementation, but the resulting static output is deployed through the same Easel path. SPA mode [#spa-mode] Set `ssr` to `false` to deploy a client-rendered single-page application: ```ts title="react-router.config.ts" import type { Config } from "@react-router/dev/config"; import { vercelPreset } from "@vercel/react-router/vite"; export default { ssr: false, presets: [vercelPreset()], } satisfies Config; ``` Easel deploys the generated browser application to the CDN and routes application paths to its SPA entry document. In SPA mode: * Route components run in the browser * Server loaders and actions are unavailable * `clientLoader` and `clientAction` remain available * Static application assets are served without invoking a function Use the [Vite guide](/docs/framework-guides/vite) instead when the application uses React Router only in Data Mode or Declarative Mode and does not use the Framework Mode build system. Route module splitting [#route-module-splitting] React Router can split route-module exports into smaller browser chunks so data and middleware code can load separately from component code. React Router 8 enables its updated route-module behavior through top-level configuration and defaults inherited from the v8 release. In compatible React Router 7 versions, the behavior may be enabled through a future flag: ```ts title="react-router.config.ts" import type { Config } from "@react-router/dev/config"; import { vercelPreset } from "@vercel/react-router/vite"; export default { presets: [vercelPreset()], future: { v8_splitRouteModules: true, }, } satisfies Config; ``` This is primarily a browser-bundle optimization. Easel also uses the route information exposed by the deployment preset when constructing server bundles. Sessions and cookies [#sessions-and-cookies] React Router’s cookie and session APIs work through standard HTTP headers. ```ts title="app/sessions.server.ts" import { createCookieSessionStorage, redirect, } from "react-router"; const sessions = createCookieSessionStorage({ cookie: { name: "__session", httpOnly: true, path: "/", sameSite: "lax", secrets: [process.env.SESSION_SECRET!], secure: process.env.NODE_ENV === "production", }, }); export async function action({ request, }: { request: Request; }) { const session = await sessions.getSession( request.headers.get("Cookie"), ); session.set("userId", "user_123"); return redirect("/dashboard", { headers: { "Set-Cookie": await sessions.commitSession(session), }, }); } ``` Cookie-backed sessions do not require server storage, but their contents are sent with each applicable request. Use a database or durable key-value store for session state that is too large for a cookie, must be revoked centrally, or must be shared with other services. Environment variables [#environment-variables] Configure environment variables in the Easel dashboard or CLI. Server-side variables are available to loaders, actions, middleware, resource routes, and server entry code: ```ts const databaseUrl = process.env.DATABASE_URL; ``` Do not expose `process.env` directly to browser code. When browser code needs public configuration, return an explicit allowlist from the root loader: ```tsx title="app/root.tsx" import type { Route } from "./+types/root"; export async function loader() { return { env: { PUBLIC_API_ORIGIN: process.env.PUBLIC_API_ORIGIN, }, }; } export default function App({ loaderData, }: Route.ComponentProps) { return (

Welcome, {data.user.name}

``` The load function and page render execute in an Easel Function. The rendered HTML is returned to the browser and hydrated according to the application’s SvelteKit configuration. Prerendering [#prerendering] Routes that do not require request-time data can be generated during the build: ```ts title="src/routes/about/+page.ts" export const prerender = true; ``` Prerendered pages are deployed as static files and served through the Easel CDN without invoking a function. You can enable prerendering for a group of routes from a layout: ```ts title="src/routes/(marketing)/+layout.ts" export const prerender = true; ``` Child routes can override the inherited value: ```ts title="src/routes/(marketing)/pricing/+page.server.ts" export const prerender = false; ``` Use `prerender = "auto"` when some known route entries should be generated during the build while other parameter values remain available through server rendering: ```ts title="src/routes/blog/[slug]/+page.server.ts" import type { EntryGenerator } from "./$types"; export const prerender = "auto"; export const entries: EntryGenerator = () => { return [ { slug: "introducing-easel" }, { slug: "deploying-sveltekit" }, ]; }; ``` Routes fully removed from the dynamic manifest can reduce the size of the application’s server bundle. Client-side rendering [#client-side-rendering] Disable server rendering for a route when it must run entirely in the browser: ```ts title="src/routes/dashboard/+page.ts" export const ssr = false; ``` This produces a client-rendered application shell for that route. Browser requests for the route still require the appropriate fallback and routing metadata generated by the adapter. You can also disable hydration for content that should remain static HTML: ```ts title="src/routes/legal/+page.ts" export const csr = false; ``` Use these options deliberately. Prefer server rendering when you want better initial HTML, progressive enhancement, and resilience if client-side JavaScript fails to load. Streaming [#streaming] SvelteKit server load functions can return promises that resolve after the initial page data. ```ts title="src/routes/products/[id]/+page.server.ts" import type { PageServerLoad } from "./$types"; export const load: PageServerLoad = async ({ params, fetch }) => { const product = await loadProduct(params.id); return { product, reviews: fetch(`/api/products/${params.id}/reviews`).then((response) => response.json(), ), }; }; ``` The page can render the required product information immediately and update when the reviews resolve: ```svelte title="src/routes/products/[id]/+page.svelte"

{data.product.name}

{#await data.reviews}

Loading reviews…

{:then reviews}
    {#each reviews as review}
  • {review.body}
  • {/each}
{:catch}

Reviews could not be loaded.

{/await} ``` Easel forwards the response as SvelteKit produces it rather than waiting for every streamed promise to settle. Once a response has started streaming, its status and headers can no longer be changed. Do not attempt to redirect or call `setHeaders` from inside a promise that resolves after streaming begins. Streaming load data requires browser JavaScript. Without JavaScript, the browser waits for the complete server-rendered response. Form actions [#form-actions] SvelteKit form actions execute in the application’s server function. ```ts title="src/routes/contact/+page.server.ts" import { fail } from "@sveltejs/kit"; import type { Actions } from "./$types"; export const actions = { default: async ({ request }) => { const data = await request.formData(); const email = data.get("email"); if (typeof email !== "string" || !email.includes("@")) { return fail(400, { email, invalid: true, }); } await saveContactRequest({ email, }); return { success: true, }; }, } satisfies Actions; ``` Use a standard HTML form for progressive enhancement: ```svelte title="src/routes/contact/+page.svelte"
{#if form?.invalid}

Enter a valid email address.

{/if} {#if form?.success}

Your request was received.

{/if} ``` The form remains functional without browser JavaScript. With `use:enhance`, SvelteKit submits the request without a full-page reload and updates the action data in place. API endpoints [#api-endpoints] Create HTTP endpoints with `+server` files: ```ts title="src/routes/api/health/+server.ts" import { json } from "@sveltejs/kit"; import type { RequestHandler } from "./$types"; export const GET: RequestHandler = () => { return json({ ok: true, timestamp: new Date().toISOString(), }); }; ``` Endpoint handlers can return any standard `Response`, including JSON, redirects, files, Server-Sent Events, and streaming bodies. A streaming endpoint can return a `ReadableStream`: ```ts title="src/routes/api/events/+server.ts" import type { RequestHandler } from "./$types"; const encoder = new TextEncoder(); export const GET: RequestHandler = () => { const stream = new ReadableStream({ start(controller) { controller.enqueue( encoder.encode('data: {"status":"connected"}\n\n'), ); controller.close(); }, }); return new Response(stream, { headers: { "content-type": "text/event-stream", "cache-control": "no-cache", }, }); }; ``` Long-lived responses remain subject to the route’s function execution limit. Use a persistent service for indefinite connections. Hooks [#hooks] Server hooks run inside the SvelteKit server function. Use `handle` to authenticate requests, populate `event.locals`, modify responses, or bypass normal route handling: ```ts title="src/hooks.server.ts" import type { Handle } from "@sveltejs/kit"; export const handle: Handle = async ({ event, resolve }) => { const session = event.cookies.get("session"); event.locals.user = session ? await findUserBySession(session) : null; const response = await resolve(event); response.headers.set("x-application", "easel-sveltekit"); return response; }; ``` The populated value is available to server load functions, form actions, and endpoints: ```ts title="src/routes/account/+page.server.ts" import { redirect } from "@sveltejs/kit"; import type { PageServerLoad } from "./$types"; export const load: PageServerLoad = ({ locals }) => { if (!locals.user) { redirect(303, "/login"); } return { user: locals.user, }; }; ``` Use `handleFetch` when you need to modify server-side requests made through SvelteKit’s enhanced `fetch` implementation. Cookies and sessions [#cookies-and-sessions] SvelteKit’s `cookies` API is available in server load functions, actions, endpoints, and hooks: ```ts title="src/routes/login/+page.server.ts" import { redirect } from "@sveltejs/kit"; import type { Actions } from "./$types"; export const actions = { default: async ({ request, cookies }) => { const data = await request.formData(); const session = await createSession(data); cookies.set("session", session.id, { path: "/", httpOnly: true, sameSite: "lax", secure: true, maxAge: 60 * 60 * 24 * 30, }); redirect(303, "/account"); }, } satisfies Actions; ``` Cookies are sent through Easel’s request pipeline to the application function and returned through standard `Set-Cookie` response headers. Store durable session state in a database or shared session store rather than in function memory. Cache control [#cache-control] Set cache headers from server load functions with `setHeaders`: ```ts title="src/routes/blog/[slug]/+page.server.ts" import type { PageServerLoad } from "./$types"; export const load: PageServerLoad = async ({ params, setHeaders, }) => { const post = await loadPost(params.slug); setHeaders({ "cache-control": "public, max-age=0, s-maxage=300, stale-while-revalidate=86400", }); return { post, }; }; ``` For endpoint responses, set headers directly: ```ts title="src/routes/api/catalog/+server.ts" import { json } from "@sveltejs/kit"; import type { RequestHandler } from "./$types"; export const GET: RequestHandler = async () => { const products = await loadProducts(); return json(products, { headers: { "cache-control": "public, max-age=0, s-maxage=300, stale-while-revalidate=3600", }, }); }; ``` Easel’s CDN interprets supported HTTP cache directives for eligible responses. See [Cache responses at the edge](/docs/cdn/caching) and [Invalidate and revalidate cached content](/docs/cdn/revalidation). Do not publicly cache responses containing user-specific data, authentication state, private cookies, or personalized content. Environment variables [#environment-variables] SvelteKit distinguishes private from public variables and static from dynamic variables. Private variables [#private-variables] Private variables are available only to server-side code: ```ts title="src/lib/server/database.ts" import { DATABASE_URL } from "$env/static/private"; export const database = connect(DATABASE_URL); ``` SvelteKit prevents private environment modules from being imported into browser code. Public variables [#public-variables] Public variables use the configured public prefix, which is `PUBLIC_` by default: ```ts import { PUBLIC_API_ORIGIN } from "$env/static/public"; ``` Public values can be included in browser bundles and must not contain secrets. Static variables [#static-variables] Static variables are replaced during the build: ```ts import { FEATURE_FLAG } from "$env/static/private"; ``` Changing a static variable requires a new deployment. Dynamic variables [#dynamic-variables] Dynamic variables are read from the function environment when the application handles a request: ```ts import { env } from "$env/dynamic/private"; export function loadDatabaseConfig() { return { url: env.DATABASE_URL, }; } ``` Dynamic access is useful when a value should remain outside the generated server bundle or can vary without being statically imported. Explicit environment variables [#explicit-environment-variables] Recent SvelteKit 2 releases allow applications to opt into the explicit environment-variable system planned for SvelteKit 3. When enabled, variables are declared in `src/env.ts` and imported through `$app/env/private` or `$app/env/public`. Easel supplies the underlying build-time and runtime environment variables. The SvelteKit version and application configuration determine which framework modules expose them. Adapter configuration [#adapter-configuration] The Vercel adapter accepts options designed for Vercel’s infrastructure. Not every provider-specific option changes an Easel deployment. The adapter’s primary purpose on Easel is to generate: * Server function output * Static and prerendered assets * Routing configuration * Build Output API metadata Easel project settings remain authoritative for platform resources such as: * Function memory * CPU allocation * Maximum execution duration (framework `maxDuration` when emitted) * Environment variables * Scaling limits Do not rely on adapter memory, duration, or region settings unless the corresponding option is explicitly documented as supported by Easel. Functions run in US East. Image optimization [#image-optimization] Static images imported or referenced by the application deploy normally as client assets. SvelteKit itself does not prescribe a single runtime image-transformation service. Image behavior depends on the component, preprocessor, or image package used by the application. Build-time image processing works when it produces ordinary static assets during `vite build`. For runtime transformation, use Easel’s image service or another documented image provider. Verify framework-specific image plugins in a preview deployment before relying on them in production. Base paths [#base-paths] Configure an application mounted below the domain root through `kit.paths.base`: ```js title="svelte.config.js" import adapter from "@sveltejs/adapter-vercel"; export default { kit: { adapter: adapter(), paths: { base: "/app", }, }, }; ``` Use SvelteKit’s path helpers when generating links and asset URLs so the configured base path is preserved. Trailing slashes [#trailing-slashes] Configure trailing-slash behavior from a layout or page: ```ts title="src/routes/+layout.ts" export const trailingSlash = "always"; ``` Supported values are: * `"never"` * `"always"` * `"ignore"` The setting also affects paths generated during prerendering. Service workers [#service-workers] SvelteKit service workers are built as static application assets and deployed through the CDN. Create one at `src/service-worker.js` or `src/service-worker.ts`: ```ts title="src/service-worker.ts" import { build, files, version } from "$service-worker"; const cacheName = `cache-${version}`; const assets = [...build, ...files]; self.addEventListener("install", (event) => { event.waitUntil( caches.open(cacheName).then((cache) => cache.addAll(assets)), ); }); ``` A service worker executes in the visitor’s browser, not inside an Easel Function. Ensure caching rules account for immutable deployment assets and application-version changes. Project configuration [#project-configuration] Easel detects conventional SvelteKit project settings automatically: | Setting | Default | | ------------------- | --------------------------------- | | Install command | Detected from the package manager | | Build command | Package script or `vite build` | | Output directory | Read from the adapter output | | Development command | Package script or `vite dev` | | Node.js version | Project or platform default | For monorepos, set the project root to the directory containing the SvelteKit application’s `package.json` and `svelte.config.js`. The project must include the production adapter in its dependencies and configuration. Local development [#local-development] Continue using SvelteKit’s normal development server: ```bash npm run dev ``` The Vite development server provides SvelteKit routing, server rendering, load functions, actions, hooks, and endpoints locally. Use an Easel preview deployment to validate behavior that depends on the production platform: * Production adapter output * Function packaging * Streaming through the request pipeline * CDN caching * Runtime environment variables * Function duration and resource limits * WAF and security rules * Platform resource limits Every preview deployment uses the production build path and receives its own immutable URL. Known limitations [#known-limitations] WebSockets [#websockets] SvelteKit routes deployed to Easel Functions cannot accept long-lived WebSocket connections. Use an external WebSocket provider or a dedicated persistent service for bidirectional communication. HTTP streaming and Server-Sent Events are separate from WebSockets, but remain subject to function execution limits. Persistent local filesystem [#persistent-local-filesystem] The function filesystem is ephemeral. Files written during an invocation are not guaranteed to exist in a later request or on another function instance. Use object storage, a database, or another durable service for persistent data. Durable background work [#durable-background-work] Do not start untracked asynchronous work after returning a response. Function execution may end once the request lifecycle is complete. Use a queue, scheduled function, or durable background-work system for tasks that require retries or must survive function termination. Adapter-specific infrastructure settings [#adapter-specific-infrastructure-settings] The Vercel adapter contains options tied specifically to Vercel’s function products. Easel consumes the portable deployment output, but does not necessarily reproduce every Vercel-specific control. Configure Easel infrastructure from the Easel project settings unless a particular adapter option is documented as supported. Runtime image plugins [#runtime-image-plugins] Build-time image output works as static assets. Runtime image packages may depend on provider-specific APIs and should be tested independently. Troubleshooting [#troubleshooting] Easel does not detect the server application [#easel-does-not-detect-the-server-application] Confirm that `@sveltejs/adapter-vercel` is installed and selected in `svelte.config.js`. Do not rely on `adapter-auto` for the production deployment. The deployment contains only static files [#the-deployment-contains-only-static-files] Check whether `prerender = true` is exported from the root layout or inherited by the affected routes. Routes that are fully prerendered do not remain in the dynamic server manifest. A route fails during prerendering [#a-route-fails-during-prerendering] A prerendered route cannot depend on request-specific cookies, headers, authentication state, or runtime-only data. Set `prerender = false` for the route or move the request-dependent work to a dynamically rendered route. A dynamic route is missing from the prerendered output [#a-dynamic-route-is-missing-from-the-prerendered-output] SvelteKit must know which parameter values to generate. Export an `entries` function or make the route discoverable through links crawled from another prerendered page. An environment variable is undefined [#an-environment-variable-is-undefined] Confirm that the variable exists in the current Easel environment. Also verify that the application uses the correct SvelteKit module: * Static or dynamic * Private or public * Legacy `$env` modules or the explicit `$app/env` system Static variables require a new deployment after their values change. A public variable is unavailable in browser code [#a-public-variable-is-unavailable-in-browser-code] By default, public environment variables must use the `PUBLIC_` prefix. Variables without the public prefix remain server-only. A streamed value never appears early [#a-streamed-value-never-appears-early] Ensure the promise is returned from a server load function rather than awaited before the function returns. Also confirm that the surrounding application code and any upstream services do not buffer the complete response. Headers fail inside streamed data [#headers-fail-inside-streamed-data] Headers and status codes cannot change after the response starts. Set headers before returning the initial load result, and do not redirect from inside a streamed promise. Cookies are not being set [#cookies-are-not-being-set] Confirm that the cookie includes an appropriate `path`, and verify the `secure` and `sameSite` settings for the deployment environment. Code works locally but fails after deployment [#code-works-locally-but-fails-after-deployment] Inspect the production build logs and function logs from a preview deployment. Common causes include: * Missing runtime environment variables * Native packages built for the wrong environment * Case-sensitive import paths * Provider-specific adapter options * Access to persistent local files * Code that uses browser APIs during server rendering Compatibility policy [#compatibility-policy] Easel tests its SvelteKit integration against representative applications covering: * Server-side rendering * Prerendering * Server and universal load functions * Streaming promises * Form actions * Hooks * API endpoints * Cookies and sessions * Static and dynamic environment variables * Base paths * Redirects * Error handling Stable SvelteKit releases may work beyond the version listed at the top of this page, but the listed version is the most recently verified baseline. For a newly released SvelteKit version, an experimental framework feature, or a provider-specific adapter option, create a preview deployment before upgrading the production application. # TanStack Start on Easel At a glance [#at-a-glance] | | | | ----------------------------- | --------------------------------------------------- | | **Support level** | Production-ready | | **Rendering** | Static, SSR, streaming, selective SSR, and SPA mode | | **Server runtime** | Node.js | | **Deployment integration** | Nitro | | **Adapter required** | Yes | | **Automatic detection** | Yes | | **Most recently tested with** | TanStack Start RC | Deploy a TanStack Start application [#deploy-a-tanstack-start-application] TanStack Start applications use Nitro to produce the server entry and static assets Easel deploys. Install Nitro: ```bash npm install nitro ``` Add the Nitro Vite plugin after `tanstackStart()` and before the React plugin: ```ts title="vite.config.ts" import { tanstackStart } from "@tanstack/react-start/plugin/vite"; import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [ tanstackStart(), nitro(), react(), ], }); ``` Do not manually configure a Nitro deployment preset for Easel. During the platform build, Easel selects the hosting output it expects. Connect the project’s Git repository to Easel or deploy it with the CLI: ```bash easel deploy ``` Easel detects TanStack Start, installs dependencies, runs the production build, and deploys the generated client and server output. The default build command is: ```bash vite build ``` You can override the install command, build command, root directory, Node.js version, and environment variables in your project settings. Why run TanStack Start on Easel? [#why-run-tanstack-start-on-easel] TanStack Start keeps routing, data loading, and server boundaries inside the application while allowing the deployment target to remain portable. Easel preserves that model. Your application uses standard TanStack Start and Nitro integrations rather than an Easel-specific framework fork. A TanStack Start deployment includes: * Immutable client assets served through the Easel CDN * Regional functions for server rendering and server-side code * Streaming responses from routes and server functions * Type-safe server functions callable from loaders and components * Server routes for external HTTP endpoints * Middleware for authentication, context, logging, and request policy * Preview deployments for every branch and pull request * Built-in logs, metrics, traces, WAF, and attack protection You can inspect function execution, request routing, cache behavior, and resource usage from the Easel dashboard. What Easel deploys [#what-easel-deploys] The Nitro build produces a client application and a server entry that implements TanStack Start’s request handling. Easel maps that output to platform resources: | Application output | Easel resource | | ---------------------------------------- | ---------------------------- | | JavaScript, CSS, fonts, and public files | CDN assets | | Prerendered pages | CDN assets | | Server-rendered routes | Easel Functions | | Route loaders requiring server execution | Easel Functions | | Server functions | Application server function | | Server routes | Function-backed HTTP routes | | Start middleware | Application server function | | Streaming responses | Function response stream | | Application telemetry | Easel observability pipeline | Static files are deployed immutably to the CDN. Requests requiring TanStack Start’s server runtime are forwarded to the generated server entry. Server functions are part of the application’s server bundle. They are not deployed as individually configurable Easel Functions unless the build output explicitly separates them. Supported features [#supported-features] | Feature | Support | Notes | | ------------------------------- | -------------------- | --------------------------------------------------------------------------- | | File-based routing | Supported | Routes generated by TanStack Router are included in the application build | | Full-document SSR | Supported | Routes render in Easel Functions | | Streaming SSR | Supported | HTML is forwarded as the framework produces it | | Client-side navigation | Supported | Client assets and route manifests are served through the CDN | | Route loaders | Supported | Loaders run in the environment selected by the route | | `beforeLoad` | Supported | Runs as part of TanStack Router’s route lifecycle | | Server functions | Supported | Includes validated, type-safe calls from client and server code | | Streaming server functions | Supported | Includes `ReadableStream` and async-generator responses | | Server routes | Supported | Expose external HTTP endpoints from the Start application | | Middleware | Supported | Includes request, function, and server-route middleware | | Selective SSR | Supported | Routes can opt into full, data-only, or client-only rendering | | SPA mode | Supported | The application server provides the required document fallback | | Static prerendering | Supported with setup | Routes configured for prerendering are emitted as static output | | Incremental Static Regeneration | Experimental | Depends on the framework and Nitro output used by the application | | Redirects and headers | Supported | Return them through standard framework response APIs | | Cookies and sessions | Supported | Standard request and response cookie behavior is preserved | | Environment variables | Supported | Server variables remain outside the client bundle unless explicitly exposed | | Request cancellation | Supported | Long-running server work can observe the request’s `AbortSignal` | | Early Hints | Runtime-dependent | Verify behavior in a preview deployment | | OpenTelemetry | Supported | Standard Node.js instrumentation can export through Easel | | WebSockets | Not supported | Function requests use HTTP request-response semantics | | Durable background jobs | Not supported | Use a dedicated queue or background-work service | TanStack Start and `nitro/vite` are evolving rapidly. Experimental framework behavior should be validated in a preview deployment before it is promoted to production. Rendering [#rendering] TanStack Start allows each route to select how much work happens on the server. Full SSR [#full-ssr] Full SSR sends both the route’s data and rendered markup from the server. ```tsx title="src/routes/products.tsx" import { createFileRoute } from "@tanstack/react-router"; export const Route = createFileRoute("/products")({ loader: async () => { return getProducts(); }, component: ProductsPage, }); function ProductsPage() { const products = Route.useLoaderData(); return (
    {products.map((product) => (
  • {product.name}
  • ))}
); } ``` The initial request runs through the TanStack Start server entry in an Easel Function. The generated HTML can begin streaming while the route completes its work. The browser hydrates the returned document and handles later navigations through TanStack Router. Data-only SSR [#data-only-ssr] A route can run its loader on the server without rendering its component markup there: ```tsx export const Route = createFileRoute("/dashboard")({ ssr: "data-only", loader: async () => { return getDashboardData(); }, component: DashboardPage, }); ``` The server returns the route data required by the client, while the component itself renders in the browser. Client-only rendering [#client-only-rendering] Disable server rendering for a route when it depends entirely on browser APIs or should only render after hydration: ```tsx export const Route = createFileRoute("/editor")({ ssr: false, component: EditorPage, }); ``` The route remains part of the TanStack Start application, but its component does not render during the server response. Streaming [#streaming] Easel forwards response chunks as TanStack Start produces them rather than waiting for the entire document to finish rendering. This allows the initial document shell and completed route regions to reach the browser while slower data is still resolving. Streaming remains subject to the function’s maximum execution duration. A disconnected client can also cancel request-bound work when the application observes the request’s abort signal. Server functions [#server-functions] Server functions define server-only logic that can be imported and called from loaders, components, hooks, and other server functions. ```tsx title="src/data/products.functions.ts" import { createServerFn } from "@tanstack/react-start"; import { z } from "zod"; const ProductInput = z.object({ id: z.string(), }); export const getProduct = createServerFn({ method: "GET", }) .inputValidator(ProductInput) .handler(async ({ data }) => { return db.product.findUnique({ where: { id: data.id, }, }); }); ``` Call the function from application code: ```tsx const product = await getProduct({ data: { id: "product_123", }, }); ``` TanStack Start converts client-side calls into HTTP requests and dispatches them to the corresponding server implementation. The server handler can access: * Databases and private services * Server-only environment variables * Request headers and cookies * Authentication context * The request’s cancellation signal * Node.js runtime APIs supported by Easel Functions Server function identifiers and dispatch behavior are generated by TanStack Start during the production build. Keep server-only code isolated [#keep-server-only-code-isolated] Use `.server.ts` files for implementation details that must never enter a client bundle: ```ts title="src/data/products.server.ts" import { db } from "~/db"; export async function findProduct(id: string) { return db.product.findUnique({ where: { id, }, }); } ``` Import the server-only helper inside the server function handler: ```tsx title="src/data/products.functions.ts" import { createServerFn } from "@tanstack/react-start"; export const getProduct = createServerFn({ method: "GET", }) .inputValidator((id: string) => id) .handler(async ({ data: id }) => { const { findProduct } = await import("./products.server"); return findProduct(id); }); ``` TanStack Start’s build-time environment separation prevents server implementation code from being included in the browser bundle. Streaming from server functions [#streaming-from-server-functions] Server functions can return streaming data using a `ReadableStream` or async generator. ```tsx import { createServerFn } from "@tanstack/react-start"; export const generateReport = createServerFn({ method: "POST", }).handler(async function* () { yield { status: "starting", }; for await (const section of buildReport()) { yield { status: "progress", section, }; } yield { status: "complete", }; }); ``` Easel keeps the HTTP response open and forwards chunks as the server function yields them. The invocation remains active and billable until the stream closes, the client disconnects, or the function reaches its execution limit. For durable work that must continue after a disconnect or survive function termination, submit the job to a queue and stream or poll its persisted state separately. Server routes [#server-routes] Use server routes for endpoints intended to be called outside the TanStack Start application. ```tsx title="src/routes/api/health.ts" import { createFileRoute } from "@tanstack/react-router"; export const Route = createFileRoute("/api/health")({ server: { handlers: { GET: async () => { return Response.json({ ok: true, checkedAt: new Date().toISOString(), }); }, }, }, }); ``` Server routes can return: * JSON responses * HTML * Redirects * Custom headers and status codes * `ReadableStream` responses * Server-Sent Events Server routes and server functions are both handled by the generated Start server entry, but they serve different purposes: | Primitive | Intended use | | --------------- | ------------------------------------------------------- | | Server function | Type-safe calls made by your TanStack Start application | | Server route | Public or externally called HTTP endpoint | Use a server route for webhooks, public APIs, health checks, file responses, and integrations that do not call through the generated Start client. Middleware [#middleware] TanStack Start middleware can add shared request behavior without duplicating it across routes or server functions. Common uses include: * Authentication and authorization * Request context * Logging * Rate-limit metadata * Security policy * Response headers * Error handling ```tsx import { createMiddleware, getRequestHeader, } from "@tanstack/react-start"; export const authMiddleware = createMiddleware().server( async ({ next }) => { const authorization = getRequestHeader("authorization"); const user = await authenticate(authorization); if (!user) { throw new Response("Unauthorized", { status: 401, }); } return next({ context: { user, }, }); }, ); ``` This middleware runs inside the TanStack Start application function. It should not be confused with Easel’s CDN or WAF request pipeline. Use Easel security rules for infrastructure-level filtering and TanStack Start middleware for application-level behavior. Static prerendering [#static-prerendering] TanStack Start can prerender selected routes during the production build. Prerendered HTML and its associated client assets are deployed to the Easel CDN. Requests for those pages do not need to invoke the application function unless the route later requires dynamic behavior. Use prerendering for content that can be generated without request-specific data, such as: * Marketing pages * Documentation * Blog posts * Public product pages * Terms and policy pages The precise configuration depends on the TanStack Start version used by the project. Easel deploys the static output generated by Start and Nitro rather than discovering routes independently. When adding or changing prerender configuration, verify the generated routes in a preview deployment. Caching [#caching] TanStack Start does not impose one universal application-cache model. Caching behavior can come from: * Browser cache headers * Easel’s CDN * Application-level caches * Nitro caching utilities * Database or external cache services * Prerendered output * Experimental ISR support CDN caching [#cdn-caching] Set standard cache headers on server-route responses: ```tsx export const Route = createFileRoute("/api/catalog")({ server: { handlers: { GET: async () => { const catalog = await getCatalog(); return Response.json(catalog, { headers: { "Cache-Control": "public, s-maxage=300, stale-while-revalidate=60", }, }); }, }, }, }); ``` Shared-cache directives allow eligible responses to be cached by Easel’s CDN. Do not publicly cache responses containing user-specific or authenticated data. Server function caching [#server-function-caching] Server functions may set response headers through TanStack Start’s server request utilities. ```tsx import { createServerFn, setResponseHeaders, } from "@tanstack/react-start"; export const getPublicCatalog = createServerFn({ method: "GET", }).handler(async () => { setResponseHeaders( new Headers({ "Cache-Control": "public, s-maxage=300, stale-while-revalidate=60", }), ); return getCatalog(); }); ``` Server functions are frequently user-specific. Cache them publicly only when their output is safe to share across callers. Nitro storage and caching [#nitro-storage-and-caching] Nitro provides storage and caching abstractions, but their durability depends on the configured storage driver. Do not rely on in-memory or local-filesystem storage to persist across Easel Function instances. Configure an external durable driver when cached data must be shared between instances or survive instance replacement. Environment variables [#environment-variables] Configure environment variables in the Easel dashboard or CLI. Server-only variables can be accessed in server functions, server routes, middleware, and route loaders that execute on the server: ```ts const databaseUrl = process.env.DATABASE_URL; ``` Only expose a value to browser code when it is intentionally public. Vite statically replaces client-visible variables during the build: ```ts const apiOrigin = import.meta.env.VITE_PUBLIC_API_ORIGIN; ``` Any value available through `import.meta.env` in client code should be treated as public. Changing a build-time client variable requires a new deployment. Use separate values for production, preview, and development environments when necessary. OpenTelemetry [#opentelemetry] TanStack Start applications running in Easel Functions can use standard Node.js OpenTelemetry instrumentation. Instrument: * Server-rendered requests * Route loaders * Server functions * Server routes * Database queries * External API requests * Application-defined operations Easel can collect application spans alongside function metrics, request logs, and platform traces. See [OpenTelemetry](/docs/observability/opentelemetry) for how Easel collects application spans into request Traces. Project configuration [#project-configuration] Easel detects conventional TanStack Start settings automatically: | Setting | Default | | ------------------- | --------------------------------- | | Install command | Detected from the package manager | | Build command | Package script or `vite build` | | Development command | Package script or `vite dev` | | Client output | Detected from the Nitro build | | Server output | Detected from the Nitro build | | Node.js version | Project or platform default | For monorepos, set the project root to the directory containing the TanStack Start application’s `package.json` and `vite.config`. Do not manually set a different Nitro preset or `NITRO_PRESET` for the Easel production build. Overriding the preset may produce a server shape that Easel cannot deploy. Local development [#local-development] Continue using TanStack Start’s normal Vite development server: ```bash npm run dev ``` The Nitro plugin integrates the application server into the Vite development environment. Local development is best for application logic, routes, loaders, server functions, and middleware. Use an Easel preview deployment to validate platform-specific behavior, including: * Production bundling * Function execution * CDN caching * Streaming through the production request path * Environment variables * Function duration and resource limits * WAF and security rules * Production observability A preview deployment follows the same deployment path as production and receives its own immutable URL. Known limitations [#known-limitations] Nitro integration maturity [#nitro-integration-maturity] The `nitro/vite` integration uses Vite’s environment APIs and remains under active development. TanStack Start, Nitro, or Vite updates can change the generated server output. Review framework release notes and create a preview deployment before upgrading production dependencies. Pin framework and Nitro versions when deployment stability is more important than receiving updates immediately. WebSockets [#websockets] TanStack Start applications deployed to Easel Functions cannot accept long-lived WebSocket connections. Use an external WebSocket provider or a persistent service for bidirectional connections. Streaming HTTP responses and Server-Sent Events are separate from WebSockets and can operate within the function’s execution limits. Local filesystem [#local-filesystem] Function filesystems are ephemeral. Files written during one invocation are not guaranteed to exist in later invocations or on another instance. Use object storage, a database, or another durable service for persistent data. In-memory state [#in-memory-state] Global variables and in-memory caches may survive across requests handled by the same warm instance, but they are not shared across instances and can disappear at any time. Do not use process memory as the source of truth for sessions, locks, queues, rate limits, or application data. Background work [#background-work] Work started during a request is not guaranteed to continue after the function invocation ends. Use a durable queue or background-work service for jobs that require retries, exceed request duration limits, or must survive process termination. Incremental Static Regeneration [#incremental-static-regeneration] TanStack Start and Nitro’s incremental regeneration capabilities are evolving. Treat ISR behavior as experimental unless the specific route configuration has been verified against the framework and Nitro versions used by the project. For predictable static output, use build-time prerendering. For dynamic responses, use explicit cache headers or an application cache. Troubleshooting [#troubleshooting] The build does not produce a server entry [#the-build-does-not-produce-a-server-entry] Confirm that Nitro is installed: ```bash npm install nitro ``` Then confirm that `nitro()` appears in `vite.config.ts` after `tanstackStart()`: ```ts plugins: [ tanstackStart(), nitro(), react(), ] ``` A standard TanStack Start client build without a compatible server output does not contain the production entry Easel expects. Easel cannot deploy the generated output [#easel-cannot-deploy-the-generated-output] Remove any manually configured Nitro preset or `NITRO_PRESET` environment variable. Easel selects its supported hosting output during the platform build. A manually selected Node, Bun, or provider-specific preset may produce a different directory structure or server interface. A server function is unavailable [#a-server-function-is-unavailable] Confirm that the function is created with `createServerFn` and exported from a module included in the production build. Keep server-only implementation details in `.server.ts` modules and import them from inside the server handler. Server-only code appears in the client build [#server-only-code-appears-in-the-client-build] Move private implementation code into a `.server.ts` file. Do not export secrets or database clients from modules that can run in the browser. Also verify that environment-variable names intended for the server are not exposed through Vite’s public client-variable prefix. A route works locally but fails after deployment [#a-route-works-locally-but-fails-after-deployment] Create a preview deployment and inspect the build and function logs. Common causes include: * Missing production environment variables * Case-sensitive file paths * Native dependencies unavailable for the selected runtime * Reliance on the local filesystem * A manually selected Nitro preset * Code that assumes one long-lived server process * Browser-only APIs executing during SSR A stream is interrupted [#a-stream-is-interrupted] Confirm that the application closes the stream normally and handles client cancellation. Long-running streams remain subject to the function’s maximum duration. Use a durable job model when the underlying operation may outlive the request. Cached data is shared between users [#cached-data-is-shared-between-users] Do not mark authenticated or personalized responses as public. Use `private` or `no-store` for user-specific responses, and include appropriate `Vary` headers where cache behavior depends on request headers. Compatibility policy [#compatibility-policy] Easel tests its TanStack Start integration against representative applications covering: * Full-document SSR * Streaming * Route loaders * Server functions * Streaming server functions * Server routes * Middleware * Selective SSR * SPA mode * Static assets * Static prerendering * Environment variables * Production Nitro output Stable framework releases may work beyond the version listed at the top of this page, but the listed version is the most recently verified baseline. Because TanStack Start and `nitro/vite` are under active development, deploy dependency upgrades to a preview environment before promoting them to production. # Vite on Easel Easel supports React, Vue, Preact, Solid, Svelte, and other client-side applications built with Vite. Frameworks that use Vite for server rendering, such as SvelteKit, Nuxt, React Router, and TanStack Start, should use their [dedicated framework guide](/docs/framework-guides). At a glance [#at-a-glance] | | | | ----------------------------- | ------------------------------- | | **Support level** | Production-ready | | **Rendering** | Static and client-side rendered | | **Server runtime** | None | | **Adapter required** | No | | **Automatic detection** | Yes | | **Default output directory** | `dist` | | **Most recently tested with** | Vite 7.x | Deploy a Vite application [#deploy-a-vite-application] Easel detects Vite projects automatically. [Create a project](/docs/getting-started) and connect the project’s Git repository, or [deploy with the CLI](/docs/cli/deploy): ```bash easel deploy ``` Easel detects the package manager, installs dependencies, runs the production build, and deploys the generated files to the Easel CDN. The default build command is: ```bash vite build ``` Most projects define this through a package script: ```json { "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" } } ``` The default output directory is: ```text dist ``` You can override the install command, build command, root directory, output directory, Node.js version, and environment variables in your project settings. Why run Vite on Easel? [#why-run-vite-on-easel] Vite applications deploy to Easel as immutable static assets distributed through the global CDN. A Vite deployment includes: * Automatic framework and build-command detection * Immutable JavaScript, CSS, image, font, and media assets * Global CDN delivery * Configurable redirects, rewrites, and response headers * Single-page application routing * Preview deployments for every branch and pull request * Built-in traffic analytics, logs, WAF, and attack protection * Atomic deployments with instant rollback Static Vite applications do not invoke Easel Functions during page requests. This keeps request handling fast and avoids function compute charges. What Easel deploys [#what-easel-deploys] Vite transforms your source code into browser-ready static files. Easel uploads those files to its CDN and serves them directly. | Vite output | Easel resource | | ------------------------- | --------------------------- | | `index.html` | CDN asset | | JavaScript bundles | Immutable CDN assets | | CSS bundles | Immutable CDN assets | | Imported images and fonts | Immutable CDN assets | | Files from `public` | CDN assets | | Source maps | CDN assets when included | | Client-side routes | CDN rewrite to `index.html` | Assets with content hashes in their filenames can be cached for long periods because a changed asset receives a new URL. HTML files use shorter cache lifetimes so new deployments can become visible without requiring users to clear their browser cache. Supported features [#supported-features] | Capability | Support | Notes | | ---------------------------- | ------------------------------ | ------------------------------------------------------ | | Static sites | Supported | Build output is served through the Easel CDN | | Single-page applications | Supported with configuration | Requires a fallback rewrite to `index.html` | | React | Supported | Includes standard Vite React projects | | Vue | Supported | Includes standard Vite Vue projects | | Preact | Supported | Includes standard Vite Preact projects | | Solid | Supported | Includes standard Vite Solid projects | | Svelte | Supported | For client-rendered Svelte applications | | TypeScript | Supported | Compiled through the Vite build | | CSS modules | Supported | Processed during the build | | Static asset imports | Supported | Emitted into the build output | | Files from `public` | Supported | Copied to the output root | | Custom base paths | Supported | Configure with Vite’s `base` option | | Client environment variables | Supported | Variables must use the `VITE_` prefix | | Monorepos | Supported | Configure the project root directory | | Custom output directory | Supported | Configure the same directory in Vite and Easel | | Redirects and rewrites | Supported | Defined in project configuration | | Custom response headers | Supported | Defined in project configuration | | Server-Side Rendering | Not provided by plain Vite | Use a supported full-stack framework | | API routes | Not provided by plain Vite | Deploy an Easel Function or use a full-stack framework | | Middleware | Not provided by plain Vite | Use Easel routing and security configuration | | WebSockets | Not provided by static hosting | Requires a persistent external service | Single-page applications [#single-page-applications] Client-side routers use browser history APIs to render multiple routes from one HTML entry point. For example, a React application may handle all of these routes in the browser: ```text /account /account/settings /projects/123 ``` When a visitor opens `/account/settings` directly, Easel must serve `index.html` rather than look for a file at that path. Add a fallback rewrite in `vercel.json` at the project root. Easel accepts this portable routing format: ```json title="vercel.json" { "$schema": "https://openapi.vercel.sh/vercel.json", "rewrites": [ { "source": "/(.*)", "destination": "/index.html" } ] } ``` Easel applies the rewrite only when handling the request. The browser URL remains unchanged, allowing the client-side router to render the requested route. Avoid rewriting static assets [#avoid-rewriting-static-assets] A broad SPA fallback should run after Easel checks for a matching static file. Requests for JavaScript, CSS, images, fonts, and other generated assets continue to resolve to their files. A missing application route receives `index.html`, while a missing asset should still return a `404` response. Framework routers [#framework-routers] The same fallback model works with common client-side routers, including: * React Router in declarative or data mode * Vue Router * TanStack Router * Solid Router * Preact Router * Svelte SPA routers React Router framework mode includes a server build and should instead use the [React Router framework guide](/docs/framework-guides/react-router). Static sites with multiple HTML pages [#static-sites-with-multiple-html-pages] Not every Vite application is a single-page application. Vite can produce multiple HTML entry points: ```ts import { resolve } from "node:path"; import { defineConfig } from "vite"; export default defineConfig({ build: { rollupOptions: { input: { home: resolve(__dirname, "index.html"), about: resolve(__dirname, "about/index.html"), pricing: resolve(__dirname, "pricing/index.html"), }, }, }, }); ``` Easel deploys each generated HTML file at its corresponding path: | Generated file | URL | | ------------------------- | ----------- | | `dist/index.html` | `/` | | `dist/about/index.html` | `/about/` | | `dist/pricing/index.html` | `/pricing/` | Do not add an SPA fallback rewrite when each route has its own generated HTML file. Environment variables [#environment-variables] Vite exposes variables prefixed with `VITE_` to application code through `import.meta.env`. Configure the variable in the Easel dashboard or CLI. See [deployment environments](/docs/deployments/environments). ```text VITE_API_ORIGIN=https://api.example.com ``` Read it in the application: ```ts const apiOrigin = import.meta.env.VITE_API_ORIGIN; ``` Public variables [#public-variables] Every variable exposed through `import.meta.env` is included in the browser bundle. Do not place secrets in variables prefixed with `VITE_`, including: * Database credentials * Private API keys * Signing keys * Service-account credentials * Authentication secrets A visitor can inspect these values in downloaded JavaScript. Use an Easel Function or another server-side service when an operation requires a secret. Build-time behavior [#build-time-behavior] Vite replaces client environment variables during the production build. Changing a `VITE_` variable requires a new deployment. Use separate environment values for production, preview, and development when the application connects to different services in each environment. Built-in variables [#built-in-variables] Vite also provides built-in environment values: ```ts import.meta.env.MODE; import.meta.env.BASE_URL; import.meta.env.PROD; import.meta.env.DEV; import.meta.env.SSR; ``` For a static Easel deployment, `import.meta.env.SSR` is false in browser application code. Base paths [#base-paths] Vite assumes the application is served from `/` unless you configure a different base path. For an application deployed at: ```text https://example.com/dashboard/ ``` set `base` in `vite.config.ts`: ```ts import { defineConfig } from "vite"; export default defineConfig({ base: "/dashboard/", }); ``` Vite prefixes generated JavaScript, CSS, and asset URLs with that path. The Easel deployment route and Vite base path must agree. A mismatch commonly causes the HTML page to load while JavaScript or CSS requests return `404`. For a root-domain deployment, leave the default base: ```ts import { defineConfig } from "vite"; export default defineConfig({ base: "/", }); ``` Asset handling [#asset-handling] Imported assets [#imported-assets] Vite processes assets imported from source code: ```ts import logoUrl from "./logo.svg"; ``` Small assets may be embedded into JavaScript or CSS. Larger assets are emitted into the output directory with content-hashed filenames. Easel treats these hashed files as immutable CDN assets. Public directory [#public-directory] Files placed in Vite’s `public` directory are copied to the root of the build output without content hashing. For example: ```text public/favicon.ico ``` becomes: ```text dist/favicon.ico ``` and is available at: ```text /favicon.ico ``` Because public files do not receive content hashes, update their filenames when you need to guarantee immediate cache invalidation. Source maps [#source-maps] Vite does not emit production source maps by default. Enable them in `vite.config.ts`: ```ts import { defineConfig } from "vite"; export default defineConfig({ build: { sourcemap: true, }, }); ``` Source maps may contain original application source. Upload them to your error-monitoring provider or restrict their public availability when exposing source code is a concern. Cache behavior [#cache-behavior] Vite generates content-hashed filenames such as: ```text assets/index-Cg4kP8nD.js assets/index-D7hA2xLm.css ``` Because the filename changes whenever its contents change, Easel can cache these assets aggressively. See [Cache responses at the edge](/docs/cdn/caching). A Vite deployment uses different policies for different resources: | Resource | Behavior | | ------------------------- | -------------------------------------------- | | Hashed JavaScript and CSS | Long-lived immutable caching | | Hashed images and fonts | Long-lived immutable caching | | HTML entry points | Shorter caching with revalidation | | Files from `public` | Configurable, depending on filename strategy | Easel deployments are atomic. A new deployment publishes a complete new set of assets rather than updating files in place. Older deployment URLs continue pointing to their original asset set, which prevents a preview or rollback from accidentally loading bundles from another deployment. Redirects, rewrites, and headers [#redirects-rewrites-and-headers] Vite does not define production routing behavior itself. Configure application-level routing in project configuration. Common use cases include: * Redirecting an old route to a new route * Rewriting SPA routes to `index.html` * Adding security headers * Defining cache behavior * Proxying a path to another service * Redirecting between apex and `www` domains For example, redirect an old documentation path: ```json title="vercel.json" { "$schema": "https://openapi.vercel.sh/vercel.json", "redirects": [ { "source": "/docs-old/:path*", "destination": "/docs/:path*", "status": 308 } ] } ``` Add security headers: ```json title="vercel.json" { "$schema": "https://openapi.vercel.sh/vercel.json", "headers": [ { "source": "/(.*)", "headers": [ { "key": "X-Content-Type-Options", "value": "nosniff" }, { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" } ] } ] } ``` When importing an existing project, Easel can also interpret supported configuration formats from compatible platforms. APIs and server-side code [#apis-and-server-side-code] Plain Vite applications contain browser code and static assets. Vite does not create an application server or API runtime. When the application requires server-side behavior, you can: * Deploy an Easel Function * Connect to an external API * Use a supported full-stack framework * Use a managed backend or database service Do not place private credentials directly in the Vite application to call a protected upstream API. Browser users can inspect those credentials and make requests independently of the application. An Easel Function can keep the credential server-side: ```ts export default async function handler(request: Request) { const response = await fetch("https://api.example.com/private-data", { headers: { Authorization: `Bearer ${process.env.PRIVATE_API_KEY}`, }, }); return new Response(response.body, { status: response.status, headers: { "Content-Type": response.headers.get("Content-Type") ?? "application/json", }, }); } ``` The Vite application can then call the function using a relative URL: ```ts const response = await fetch("/api/private-data"); const data = await response.json(); ``` Monorepos [#monorepos] Set the Easel project root to the directory containing the Vite application. For example: ```text apps/ dashboard/ package.json vite.config.ts api/ package.json packages/ ui/ package.json ``` The Vite project root would be: ```text apps/dashboard ``` Easel runs the install and build commands from the configured project context while preserving access to workspace dependencies. Ensure the package manager’s lockfile is available from that context. Depending on the workspace layout, the install command may need to run from the repository root even when the build command targets a nested application. Custom output directories [#custom-output-directories] Vite writes production files to `dist` by default. To use another directory: ```ts import { defineConfig } from "vite"; export default defineConfig({ build: { outDir: "build", }, }); ``` Set the Easel output directory to the same value: ```text build ``` If these settings differ, the build can succeed while Easel reports that it cannot find deployable output. Avoid writing the build output outside the project workspace unless the Easel build environment explicitly permits that path. Local development [#local-development] Continue using Vite’s development server: ```bash npm run dev ``` Vite’s development server provides hot module replacement and framework-specific development behavior. Use Easel preview deployments to validate production platform behavior, including: * Production minification and bundling * SPA fallback rewrites * Redirects and custom headers * CDN caching * Environment-specific variables * Base paths * WAF and security rules * Custom domains A preview deployment runs the same static deployment process as production and receives its own immutable URL. Do not use `vite preview` as a production server. It is intended only for locally previewing the generated build output. Static export from other frameworks [#static-export-from-other-frameworks] Some frameworks can emit fully static output through Vite or a related build process. A static export can be deployed through the Vite path when the final output consists only of HTML, JavaScript, CSS, and other static files. However, using a static export removes server-dependent framework features such as: * Server-Side Rendering * Server Actions * API routes * Middleware * Runtime cache revalidation * Request-time personalization Use the framework’s dedicated Easel guide when the application requires any server runtime behavior. Known limitations [#known-limitations] No server runtime [#no-server-runtime] A plain Vite deployment does not include a Node.js or edge server. Server-only modules and Node.js APIs cannot run in browser code. For example, the following cannot execute in the deployed frontend: ```ts import fs from "node:fs"; ``` Move server-side logic into an Easel Function or a supported full-stack framework. No hidden environment variables [#no-hidden-environment-variables] Variables included in the Vite client bundle are public. The `VITE_` prefix controls browser exposure. It is not a secret-storage mechanism. Client-side routing requires a rewrite [#client-side-routing-requires-a-rewrite] History-based routes return `404` on direct navigation unless an SPA fallback is configured. Hash-based routing does not require a server rewrite because the URL fragment is not sent in the HTTP request, but history-based routing produces cleaner URLs. Filesystem paths are case-sensitive [#filesystem-paths-are-case-sensitive] The Easel build environment and CDN treat file paths as case-sensitive. An import that works on a case-insensitive local filesystem may fail during deployment: ```ts // File is named Button.tsx import Button from "./button"; ``` Ensure import casing exactly matches the filename. Troubleshooting [#troubleshooting] The build succeeds, but Easel cannot find the output [#the-build-succeeds-but-easel-cannot-find-the-output] Confirm that the Easel output directory matches Vite’s `build.outDir`. The default for both should be: ```text dist ``` A client-side route returns 404 [#a-client-side-route-returns-404] Configure an SPA fallback rewrite to `index.html`. Do not add this rewrite to a multipage site where each route has its own generated HTML file. The page loads without JavaScript or styles [#the-page-loads-without-javascript-or-styles] Check the browser network panel for asset requests returning `404`. The most common cause is a mismatch between Vite’s `base` setting and the application’s deployment path. An environment variable is undefined [#an-environment-variable-is-undefined] Confirm that: 1. The variable begins with `VITE_`. 2. It is configured for the current deployment environment. 3. The application was rebuilt after the value was added or changed. 4. The code reads it through `import.meta.env`. For example: ```ts const apiOrigin = import.meta.env.VITE_API_ORIGIN; ``` A variable still has its previous value [#a-variable-still-has-its-previous-value] Vite embeds client environment variables during the build. Trigger a new deployment after changing the value. An import works locally but fails during deployment [#an-import-works-locally-but-fails-during-deployment] Check filename casing and verify that the dependency is declared in the appropriate `package.json`. Also confirm that the package is not relying on Node.js APIs in browser code. Refreshing a route returns the home page incorrectly [#refreshing-a-route-returns-the-home-page-incorrectly] A broad SPA fallback sends all unmatched routes to `index.html`. Ensure APIs, static files, and other special paths are handled before the fallback rule. The application calls the wrong API in preview deployments [#the-application-calls-the-wrong-api-in-preview-deployments] Use separate production and preview values for `VITE_API_ORIGIN`, or call a same-origin relative path when the backend is deployed with the application. Compatibility policy [#compatibility-policy] Easel tests its Vite integration against representative applications using React, Vue, TypeScript, static assets, client-side routing, custom output directories, base paths, environment variables, and monorepo layouts. Stable Vite releases may work beyond the version listed at the top of this page, but the listed version is the most recently verified baseline. Vite plugins that only transform the build output work without platform-specific support. Plugins that expect a persistent server, custom development middleware, or a specific hosting runtime may require an Easel integration or a dedicated framework guide. Next steps [#next-steps] Cache headers and edge behavior First deploy from Git CLI deploy options # Function configuration Function configuration controls the runtime, resources, environment, and duration used by dynamic application routes. Easel derives defaults from the framework build and project settings. Override them only when the application has a specific requirement. Configuration sources [#configuration-sources] Function behavior may be influenced by: 1. Easel project settings 2. Environment-specific project settings 3. Supported framework build metadata (including route `maxDuration`) 4. Platform defaults Easel project settings remain authoritative for platform-managed resources such as CPU tier and Node.js version. Project-level controls [#project-level-controls] Project settings that affect functions include: * Node.js version (22 or 24) * CPU tier (Standard or Performance) * Environment variables * Root directory * Build command * Install command There is no project UI for function duration or regions. Duration comes from framework route config when present. Functions run in US East. See [Function regions](/docs/functions/regions). Runtime version [#runtime-version] Select Node.js **22** or **24** in project build settings. Easel does not resolve the runtime from `package.json` `engines.node`. Set the version in the project so builds and functions use the same Node.js line. Before changing runtime versions: * Review framework compatibility * Rebuild native dependencies * Test preview deployments * Confirm database and SDK support * Review deprecated runtime APIs CPU and memory [#cpu-and-memory] CPU and memory determine the resources available to each active function instance. Easel currently offers two project-level tiers: | Tier | Memory | CPU | | ----------- | ------ | ------ | | Standard | 2 GB | 1 vCPU | | Performance | 4 GB | 2 vCPU | Increase resources when the function: * Performs CPU-intensive work * Uses large framework bundles * Processes large responses * Requires substantial in-memory data * Encounters memory errors * Needs lower latency for compute-bound work Do not increase memory merely to compensate for: * Unbounded caches * Memory leaks * Loading unnecessary dependencies * Buffering large streams * Retaining request state globally See [Function limits](/docs/functions/limits) for related constraints. Maximum duration [#maximum-duration] Maximum duration controls how long an invocation may remain active. Duration includes: * Application execution * Awaited network calls * Streaming * Registered post-response work (`waitUntil` / `after()`) * Other work performed before the invocation ends The default maximum duration is **30s**. Frameworks that emit `maxDuration` in build output (for example Next.js route config) can raise it up to **800s**. Next.js example: ```ts export const runtime = "nodejs"; export const maxDuration = 60; ``` Easel reads the value from `.vc-config.json` produced by the build. Missing or invalid values default to 30s. Values outside 1–800 are clamped. Choose a duration that allows normal requests to complete without masking unexpectedly slow operations. Use a durable background-job system for work that exceeds practical HTTP request duration. Environment variables [#environment-variables] Configure environment variables separately for supported deployment environments. Typical environments include: * Production * Preview * Development Example server-only variables: ```text DATABASE_URL AUTH_SECRET API_KEY ``` Example public variables: ```text NEXT_PUBLIC_API_ORIGIN VITE_PUBLIC_API_ORIGIN ``` Public variables can be embedded into browser code during the build. They must not contain secrets. See [Deployment environments](/docs/deployments/environments). Build-time and runtime variables [#build-time-and-runtime-variables] Some variables are used during the build, while others are read when the function runs. A variable may need to be available in both phases. Framework behavior differs, so consult the relevant [framework guide](/docs/framework-guides). Secret changes [#secret-changes] A runtime variable change may require a new deployment depending on how the framework packages or reads the value. Public build-time variables require a rebuild. Framework-generated configuration [#framework-generated-configuration] Framework adapters can emit metadata describing: * Function entry points * Route patterns * Runtime requirements * Route-level duration * Static assets * Cache behavior Easel consumes supported metadata from the build output. Platform-level settings can override adapter resource hints where documented. Root directory [#root-directory] For monorepos, set the project root to the directory containing the application. Example: ```text repository/ ├── apps/ │ ├── web/ │ │ ├── package.json │ │ └── next.config.ts │ └── admin/ └── packages/ ``` For the web application: ```text Root directory: apps/web ``` The root affects: * Dependency installation * Framework detection * Build commands * Output discovery * Environment files * Application configuration Install command [#install-command] Easel normally detects the package manager and install command. Examples: ```bash npm install pnpm install yarn install bun install ``` Override the command only when the repository requires custom behavior. Use lockfiles to keep dependency resolution reproducible. Build command [#build-command] Easel normally uses the framework’s production build command. Examples: ```bash next build nuxt build vite build ``` The build must produce a supported deployment output. Do not start a long-running server from the build command. Native dependencies [#native-dependencies] Packages with native components must be compatible with the Easel runtime environment. Potential issues include: * Operating-system compatibility * CPU architecture * C library requirements * Post-install scripts * Prebuilt binary availability * Runtime version compatibility Test native dependencies in a preview deployment. Configuration by environment [#configuration-by-environment] Production and preview deployments may need different: * Database connections * Authentication origins * API keys * Logging levels * External service environments * Resource allocations Avoid branching on hostnames when a deployment environment variable is available. Safe defaults [#safe-defaults] Start with platform defaults when: * The application is new * Traffic is low * Workloads are mostly I/O-bound * The framework has no special resource requirement Adjust settings after observing: * Duration * Memory * CPU * Startup latency * Concurrent load * Downstream latency Configuration changes [#configuration-changes] A change to runtime, resources, or environment variables may require a new deployment. Use a preview deployment to validate changes before promoting them to production. Troubleshooting [#troubleshooting] The configured runtime is ignored [#the-configured-runtime-is-ignored] Check whether: * The Node.js version is set in project build settings (not only `engines`) * The runtime version is supported * The setting requires a new deployment * The configuration is applied to the correct environment A function runs out of memory [#a-function-runs-out-of-memory] Check for: * Large in-memory buffers * Unbounded collections * Module-level caches * Large response generation * Native-library memory usage * Excessive concurrency * Memory leaks Increasing memory may help, but inspect application behavior first. The function times out [#the-function-times-out] Use traces and logs to identify: * Slow database queries * External API delays * Retry loops * Unclosed streams * Post-response work * Long-running CPU tasks Raise route `maxDuration` only when the operation is appropriate for a request-driven function, and stay within the 800s maximum. Environment variables are missing [#environment-variables-are-missing] Confirm that: * The variable exists in the correct environment * The deployment was created after the variable changed * The framework exposes it at runtime * Public prefixes are used only for non-secret client values * The variable name matches exactly Related documentation [#related-documentation] * [Functions](/docs/functions) * [Function runtime](/docs/functions/runtime) * [Function regions](/docs/functions/regions) * [Function limits](/docs/functions/limits) * [Framework guides](/docs/framework-guides) * [Deployment URLs](/docs/deployments/urls) * [Deployment environments](/docs/deployments/environments) # Functions Easel Functions run the server-side portions of your application, including server-rendered pages, API routes, server actions, loaders, framework endpoints, and application-defined handlers. Functions are created automatically from supported framework builds. Easel deploys each function with the runtime and resources required by the project. Functions run in US East; see [Function regions](/docs/functions/regions). What runs in a function [#what-runs-in-a-function] Depending on the framework and application, Easel Functions can handle: * Server-rendered pages * API routes * Route handlers * Server Actions * Loaders and actions * Webhooks * Authentication callbacks * Cache regeneration * Scheduled post-response work within the invocation lifecycle * Application-defined HTTP endpoints Static pages and build assets do not require a function. They are served through Easel’s CDN. How requests reach functions [#how-requests-reach-functions] A dynamic request enters through Easel’s delivery network and is routed to an eligible function instance. A function is not invoked when Easel can complete the request through: * A firewall rule * A redirect * A fresh cached response * A static deployment asset See [CDN](/docs/cdn) for request delivery and response caching. Framework integration [#framework-integration] Supported frameworks generate the function entry points Easel deploys. Examples include: | Framework behavior | Function workload | | ------------------------------ | ----------------------- | | Next.js Server Component route | Server rendering | | Next.js Route Handler | HTTP endpoint | | SvelteKit `+page.server` load | Server data loading | | SvelteKit form action | Form submission | | Nuxt server route | Nitro server handler | | React Router loader or action | Server data or mutation | | TanStack Start server function | Server handler | The exact number and structure of deployed functions depend on the framework adapter and build output. See [Framework guides](/docs/framework-guides) for framework-specific deployment behavior. Scaling [#scaling] Easel creates additional function capacity as request volume increases. Applications must not assume that: * The same visitor reaches the same instance * Requests run sequentially * Local memory is shared across instances * Files written by one invocation exist during another * An instance remains active indefinitely Use a database, object storage, or the [Runtime Cache](/docs/runtime-cache) for shared or durable data. In-function concurrency [#in-function-concurrency] An active Easel Function can process more than one request concurrently when the runtime and application permit it. This can improve: * Throughput per active instance * Resource utilization * Connection reuse * Latency during traffic bursts * Performance for applications that spend time waiting on databases or APIs Application code must therefore be safe for overlapping requests. Avoid storing request-specific state in module-level mutable variables. Unsafe: ```ts let currentUserId: string | undefined; export async function handler(request: Request) { currentUserId = await getUserId(request); return Response.json({ userId: currentUserId, }); } ``` Safer: ```ts export async function handler(request: Request) { const userId = await getUserId(request); return Response.json({ userId, }); } ``` Module-level read-only objects and reusable clients are appropriate when their libraries support concurrent use. Streaming [#streaming] Functions can return streaming HTTP responses when supported by the framework and runtime. ```ts export async function GET() { const stream = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode("Starting\n")); controller.enqueue(new TextEncoder().encode("Complete\n")); controller.close(); }, }); return new Response(stream, { headers: { "Content-Type": "text/plain; charset=utf-8", }, }); } ``` Easel forwards response chunks as they become available. The function invocation remains active until application execution finishes, the response closes, or the invocation reaches its duration limit. Streaming does not automatically make a response cacheable. See [Caching](/docs/cdn/caching). Work after the response [#work-after-the-response] Supported framework APIs may register work that continues after the response is ready. That work still counts against Function duration and does not outlive the invocation. See [Background work](/docs/runtime/background-work) for patterns, limits, and when to use a durable job system instead. Environment variables [#environment-variables] Functions receive environment variables configured for the project and deployment environment. See [Environment variables](/docs/runtime/environment-variables) for scoping, secrets, and build-time versus runtime values. ```ts const databaseUrl = process.env.DATABASE_URL; ``` Production and preview deployments can use different values. Public framework variables such as `NEXT_PUBLIC_*` or `VITE_*` may be embedded into browser bundles during the build and must not contain secrets. See [Function configuration](/docs/functions/configuration). Observability [#observability] Function requests can produce: * Request logs * Application logs * Errors * Duration and resource metrics * Distributed traces * Cache diagnostics * Regional execution metadata Use the request ID returned by Easel to correlate a client response with logs and traces: ```http X-Easel-Id: 550e8400-e29b-41d4-a716-446655440000 ``` Avoid logging authorization tokens, session cookies, passwords, API keys, or other secrets. See [Observability](/docs/observability) and [Request details](/docs/observability/request-details). Functions and caching [#functions-and-caching] Functions can interact with two distinct cache layers. | CDN cache | Runtime Cache | | -------------------------------------- | ------------------------------------- | | Stores complete HTTP responses | Stores application values | | Evaluated before a function is invoked | Accessed from function code | | Controlled through HTTP cache headers | Controlled through an application API | | Best for pages and API responses | Best for data and computed results | A function can read application data from the Runtime Cache and return a response that is subsequently stored in the CDN cache. See: * [Caching](/docs/cdn/caching) * [Revalidation and purging](/docs/cdn/revalidation) * [Runtime Cache](/docs/runtime-cache) Function configuration [#function-configuration] Project settings control function behavior such as: * Node.js version * CPU and memory allocation * Environment variables * Build and output settings Framework adapters may also emit route-level metadata such as `maxDuration`. Functions run in US East. See [Function regions](/docs/functions/regions). See [Function configuration](/docs/functions/configuration). Regions [#regions] Functions run in US East (`iad`). The CDN may still serve cacheable responses from other edge locations. See [Function regions](/docs/functions/regions). Limits [#limits] Functions are subject to limits including: * Maximum duration * Memory * CPU * Request body size * Response size * Header size * Deployment size * Concurrent executions See [Function limits](/docs/functions/limits). Related documentation [#related-documentation] * [Runtime behavior](/docs/functions/runtime) * [Function configuration](/docs/functions/configuration) * [Function regions](/docs/functions/regions) * [Function limits](/docs/functions/limits) * [Environment variables](/docs/runtime/environment-variables) * [Background work](/docs/runtime/background-work) * [Image optimization](/docs/runtime/image-optimization) * [Runtime Cache](/docs/runtime-cache) * [Framework guides](/docs/framework-guides) * [CDN](/docs/cdn) * [Observability](/docs/observability) # Function limits Easel Functions are subject to resource and request limits that protect platform reliability and define the supported execution model. Current limits [#current-limits] | Limit | Value | Notes | | ----------------------------- | ---------------- | ----------------------------------------- | | Default maximum duration | 30s | Includes streaming and post-response work | | Maximum configurable duration | 800s | Upper bound for customer `maxDuration` | | Memory | 2 GB or 4 GB | Standard or Performance CPU tier | | CPU | 1 vCPU or 2 vCPU | Paired with the memory tier | | Request body size | 10 MB | Larger uploads must use object storage | | Deployment bundle size | 250 MB | Packaged function zip | | Environment variables | 4 KB total | Combined size of names and values | | Incoming WebSockets | Not supported | Use streaming HTTP or SSE where needed | In-function concurrency, response size, header size, URL length, temporary filesystem capacity, and outbound connection counts are enforced by the platform and can change with runtime configuration. Design applications so they remain safe under concurrent requests on a shared instance. Duration [#duration] Maximum duration is the total time an invocation can remain active. It includes: * Application execution * Database and API requests * Streaming response bodies * Post-response work such as `waitUntil` / `after()` * Framework rendering * Cache regeneration Returning response headers does not end the invocation. Duration continues until the handler finishes, the stream closes, and registered post-response work completes, or the limit is reached. When the duration limit is reached, Easel terminates the invocation. Stalled invokes are aborted earlier, without waiting for the full `maxDuration`: * Response headers must arrive within **60 seconds** of invoke start (or sooner when remaining `maxDuration` plus a small transport slack is smaller). * After headers, each body chunk and completion signal must arrive within **30 seconds** of the previous progress (again capped by the remaining duration budget). Set route-level duration through your framework when supported (for example Next.js `export const maxDuration = 60`). Easel reads the value from build output. The default is 30s when unset. Values outside 1–800s are clamped. Use a queue or durable worker system for work that cannot reliably complete within the limit. Memory [#memory] Memory includes application and runtime usage such as: * JavaScript heap * Native allocations * Framework code * Buffers * Loaded files * Database clients * Concurrent request state An invocation or instance that exceeds its memory allowance may terminate. Common causes include: * Unbounded arrays or maps * Large body buffering * Image processing * Loading large datasets * Retaining request state globally * Memory leaks * Excessive concurrency Stream large data where possible rather than loading it entirely into memory. CPU [#cpu] CPU allocation affects compute-bound work. Examples include: * Rendering * Compression * Cryptography * Serialization * Image processing * Large data transformations I/O-bound applications may benefit more from concurrency and connection reuse than from increased CPU. CPU and memory are configured together through the Standard and Performance tiers. See [Function configuration](/docs/functions/configuration). In-function concurrency [#in-function-concurrency] Concurrency limits the number of simultaneous requests one active instance can process. Higher concurrency can improve efficiency for I/O-heavy workloads but increases aggregate pressure on: * Memory * CPU * Database pools * External API limits * Shared application clients Applications must remain concurrency-safe even below the platform maximum. The platform may use fewer concurrent requests depending on traffic and instance conditions. Request body size [#request-body-size] Requests larger than the supported body limit may be rejected before or during function execution. For large uploads, prefer direct browser-to-object-storage uploads using signed URLs. This avoids sending the complete file through the function. Response size [#response-size] Buffered and streaming responses may have different practical limits. Large buffered responses consume memory and delay the first byte. Prefer streaming when: * Generating large exports * Forwarding large upstream responses * Producing incremental output For static or generated files that are reused, store the file in object storage or deploy it as a static asset instead of regenerating it on each request. Headers [#headers] Requests with oversized headers can be rejected before application code runs. Common causes include: * Large cookies * Excessive authentication claims * Many tracing headers * Repeated proxy headers * Large custom metadata Keep cookies small and avoid storing complete application state in them. Responses can also fail when custom headers exceed platform limits. URL length [#url-length] Long URLs commonly result from: * Large query payloads * Encoded state * Complex filters * Tracking data * Redirect chains Use a request body for large structured input rather than encoding it into the URL. Remember that `GET` request bodies are not a reliable alternative. Temporary filesystem [#temporary-filesystem] Temporary storage is ephemeral and capacity-limited. Use it only for bounded intermediate work. Do not use it for: * Persistent uploads * Durable databases * Shared application state * Long-term caches Files can disappear when the invocation or instance ends. Bundle size [#bundle-size] Function bundles include: * Application code * Framework runtime * Dependencies * Generated server code * Packaged files Large bundles can increase: * Build time * Deployment size * Startup latency * Memory usage Reduce bundle size by: * Removing unused dependencies * Avoiding large optional packages * Using framework output tracing * Loading assets from object storage * Separating unrelated workloads * Reviewing duplicated dependencies Environment variable limits [#environment-variable-limits] Environment variables have limits on count, name size, value size, and aggregate size. The combined size of environment variable names and values must stay within 4 KB. Do not store large files, certificates, or datasets directly in environment variables. Use a secret manager or secure storage for larger configuration objects where supported. Logs [#logs] Large or excessively frequent logs can be truncated, sampled, or rejected. Avoid logging: * Complete request bodies * Large response payloads * Binary data * Secrets * Session cookies * Authorization headers Prefer structured, concise events. Database connections [#database-connections] Function concurrency and autoscaling can create many database connections. For example: ```text instances × concurrent requests × connections per request ``` can exceed the database limit quickly. Use: * Connection pools designed for serverless workloads * Database proxies * Shared clients within an instance * Bounded pool sizes * Query timeouts * Runtime Cache * CDN caching Do not create a new unbounded pool for every request. External service limits [#external-service-limits] Easel’s function limits do not replace limits imposed by: * Databases * APIs * Authentication providers * Email services * Object storage * Payment services Autoscaling can increase downstream request volume rapidly. Use rate limits, bounded concurrency, retries with backoff, and circuit breakers where appropriate. Cacheable response limits [#cacheable-response-limits] CDN caching may impose separate constraints on: * Response size * Status codes * Headers * Streaming * TTL * Tags See [Caching](/docs/cdn/caching) for CDN-specific behavior. Limits and framework behavior [#limits-and-framework-behavior] Frameworks can introduce their own limits and defaults. Examples include: * Body parser limits * Image optimization limits * Route-level duration * Static generation limits * Middleware restrictions * Adapter bundle constraints The effective limit is the strictest applicable framework or platform constraint. Limit errors [#limit-errors] When a limit is exceeded, the request may fail with: * An HTTP error status * A platform error page * A function termination * A truncated log or response * A build failure Use logs, traces, and the request ID (`X-Easel-Id`) to identify which limit was exceeded, then adjust configuration or application behavior. Related documentation [#related-documentation] * [Functions](/docs/functions) * [Function runtime](/docs/functions/runtime) * [Function configuration](/docs/functions/configuration) * [Function regions](/docs/functions/regions) * [Caching](/docs/cdn/caching) * [Runtime Cache](/docs/runtime-cache) # Function regions Easel Functions run in a single compute region: US East (`iad` / `us-east-1`). The CDN can still serve cached and static responses from edge locations near visitors. Dynamic requests that need application code run in US East. Delivery network and compute [#delivery-network-and-compute] Easel’s delivery network and function compute are related but distinct. A visitor can enter the CDN near their location while the application function runs in US East. Cached responses and static assets may complete without invoking a function. Available region [#available-region] | Region | Identifier | Location | | ------- | ---------- | -------------------------------- | | US East | `iad` | Northern Virginia, United States | Use `iad` as the public region identifier in diagnostics and application code that reads platform headers. Additional function regions are not available yet. Latency and databases [#latency-and-databases] Place databases and other stateful dependencies close to US East when request-driven work depends on them. Example: ```text User in London ↓ Easel CDN (near the user) ↓ Function in US East (iad) ↓ Database in US East ``` Colocating the function and database usually matters more than placing compute near the user when every request queries a single primary database. Reduce round trips through: * Query batching * Joins * Transactions * Connection pooling * Runtime Cache * CDN response caching Reading the region on requests [#reading-the-region-on-requests] Easel attaches the function region on the request your application code receives: ```http X-Easel-Function-Region: iad ``` Read it from the framework request API or raw headers: ```ts export async function GET(request: Request) { const region = request.headers.get("x-easel-function-region"); return Response.json({ region }); } ``` Treat this as platform metadata on the **request**, not as a value you set from the client. Client-supplied `X-Easel-Function-Region` values do not control routing. See [Request and response headers](/docs/cdn/headers). Sessions and local state [#sessions-and-local-state] Do not store sessions only in local function memory. Use: * Signed stateless cookies * A shared session store * A database * Another durable authentication system Instances remain ephemeral and independently scalable even within one region. Caching before compute [#caching-before-compute] CDN caching can reduce the impact of function-region distance for cacheable routes. A cached response can be served without invoking the function. Use CDN caching for: * Public pages * Product catalogs * Documentation * Public API responses * Generated content that tolerates controlled staleness Use the Runtime Cache to reduce repeated application-data work within functions. Related documentation [#related-documentation] * [Functions](/docs/functions) * [Function runtime](/docs/functions/runtime) * [Function configuration](/docs/functions/configuration) * [Function limits](/docs/functions/limits) * [Caching](/docs/cdn/caching) * [Runtime Cache](/docs/runtime-cache) * [Request and response headers](/docs/cdn/headers) # Function runtime The function runtime provides the execution environment for server-side application code. This page describes instance lifecycle, concurrency, reuse, streaming, filesystem behavior, networking, and shutdown semantics. Runtime environment [#runtime-environment] Easel Functions run application code produced by supported frameworks or application-defined handlers. A runtime environment includes: * The application bundle * Runtime dependencies * Configured environment variables * Allocated CPU and memory * Temporary filesystem space * Network access * Request and observability integrations Supported runtime versions are listed in [Function configuration](/docs/functions/configuration). Instance lifecycle [#instance-lifecycle] A function instance moves through a lifecycle similar to: Easel may create, reuse, suspend, or terminate instances based on traffic and platform conditions. The application must not depend on a specific instance remaining available. Initialization [#initialization] Initialization happens before an instance begins handling application traffic. Typical initialization work includes: * Loading application modules * Initializing framework code * Creating database clients * Reading configuration * Preparing reusable application state Module-level initialization can reduce repeated work across requests: ```ts const database = createDatabaseClient({ url: process.env.DATABASE_URL, }); export async function handler(request: Request) { const users = await database.query("SELECT * FROM users"); return Response.json(users); } ``` Initialization code must remain reasonably fast. Large dependency graphs, expensive synchronous work, and unnecessary network requests increase startup latency. Instance reuse [#instance-reuse] An initialized instance may handle multiple requests over its lifetime. Reuse allows applications to preserve: * Loaded modules * Database connection pools * HTTP clients * Parsed configuration * Read-only lookup data * In-memory caches used only as optional optimizations Reuse is not guaranteed. Code must continue working when an instance starts with no previous in-memory state. A warm Fluid instance may handle more than one request during a single platform wake, but only while enough wake time remains for the route’s `maxDuration`. Customer `maxDuration` bounds each request (including streaming and post-response work). It is not the AWS session Timeout; that is a platform setting that keeps the wake open long enough to multiplex eligible requests. The platform aborts a request that makes no progress well before the full duration budget: headers must start within 60 seconds, and body chunks must arrive no more than 30 seconds apart (see [Function limits](/docs/functions/limits)). The per-request wait tracks `maxDuration` rather than the longer wake Timeout. In-function concurrency [#in-function-concurrency] An instance may process overlapping requests. This differs from a model in which each instance handles exactly one request at a time. Concurrency is especially useful for workloads that spend significant time waiting on: * Databases * External APIs * Object storage * Network streams * Other asynchronous services Concurrency safety [#concurrency-safety] Do not place request-specific state in shared mutable variables. Avoid: ```ts const requestContext: { organizationId?: string; } = {}; export async function handler(request: Request) { requestContext.organizationId = request.headers.get("x-organization-id") ?? undefined; return renderOrganization(requestContext.organizationId); } ``` Instead, keep request state local: ```ts export async function handler(request: Request) { const organizationId = request.headers.get("x-organization-id") ?? undefined; return renderOrganization(organizationId); } ``` Libraries used as shared clients must support concurrent requests. CPU and asynchronous work [#cpu-and-asynchronous-work] Concurrency does not create unlimited CPU. CPU-intensive JavaScript or native work can delay other requests handled by the same instance. Examples include: * Large JSON transformations * Image or video processing * Compression * Cryptographic operations * Synchronous filesystem access * Large template compilation * Machine-learning inference Move unusually expensive tasks to an appropriate worker or service when they interfere with request latency. Request lifecycle [#request-lifecycle] An invocation begins when Easel assigns a request to the function runtime. It ends when: * Application execution completes * The response and registered post-response work complete * The application fails * The client disconnect causes execution to terminate, where applicable * The maximum duration is reached * The platform terminates the instance Billing and duration measurement follow the function invocation rather than only the time until the first response byte. Streaming responses [#streaming-responses] A function can return a streaming response when supported by the runtime. ```ts export async function GET() { const encoder = new TextEncoder(); const stream = new ReadableStream({ async start(controller) { controller.enqueue(encoder.encode("First chunk\n")); await performAsyncWork(); controller.enqueue(encoder.encode("Second chunk\n")); controller.close(); }, }); return new Response(stream, { headers: { "Content-Type": "text/plain", }, }); } ``` Easel forwards response headers as soon as they are available, then forwards body chunks as the runtime produces them. The invocation remains active while the response stream is open and until registered post-response work finishes. Streaming and `waitUntil` / `after()` work count toward the route’s maximum duration (default 30s, max 800s). Applications must: * Handle cancellation where supported * Close streams reliably * Avoid retaining unbounded buffers * Respect the maximum duration * Send periodic data only when appropriate for the protocol Some paths that need a complete body for CDN or ISR admit still buffer before caching. Client delivery for ordinary dynamic responses streams. Client disconnection [#client-disconnection] A client can disconnect before the function finishes. Applications must not assume that a successfully started request will retain an active client connection until completion. Use abort signals when supported: ```ts export async function handler(request: Request) { const result = await performWork({ signal: request.signal, }); return Response.json(result); } ``` Whether execution continues after a client disconnect may depend on the runtime, framework, and response state. Do not use client-connected requests as a substitute for durable job processing. Work after the response [#work-after-the-response] Framework integrations may support post-response APIs. Registered work: * Runs within the original invocation * Counts toward duration * Uses the function’s CPU and memory * Can be interrupted if the invocation ends * Does not provide durable retries by itself Use this capability for short, non-critical work such as: * Best-effort analytics * Log enrichment * Small cache updates * Non-critical notifications Do not use it for: * Financial transactions * Durable workflow steps * Long-running imports * Tasks requiring retries * Jobs that must complete exactly once Filesystem [#filesystem] The function filesystem is ephemeral. Applications can use temporary storage during an invocation where supported, but files are not guaranteed to remain available: * Across instances * Across deployments * After an instance shuts down * For any specific retention period Do not use the local filesystem for durable uploads, application databases, or persistent user data. Use object storage or another durable service. Read-only application files [#read-only-application-files] Files packaged into the application bundle can be read at runtime where supported. Avoid modifying packaged application files. Temporary files [#temporary-files] Temporary files may be useful for: * Parsing uploads * Generating intermediate output * Working with libraries that require file paths * Buffering bounded data Delete temporary files when possible and remain within storage limits. Networking [#networking] Functions can make outbound network requests to databases, APIs, and other services. ```ts const response = await fetch("https://api.example.com/data"); ``` Application latency depends on the network distance between the function region (US East) and its dependencies. Prefer persistent, reusable clients where supported. Connections and pooling [#connections-and-pooling] An active instance can reuse outbound connections across requests. Examples include: * Database pools * HTTP keep-alive connections * TLS sessions * SDK clients Create reusable clients outside the request handler: ```ts const client = createApiClient({ apiKey: process.env.API_KEY, }); export async function handler() { return Response.json(await client.listItems()); } ``` Client libraries must tolerate: * Concurrent requests * Idle connection closure * Instance shutdown * Network interruption * Reconnection Do not assume that a connection remains open for the lifetime of the deployment. Incoming connections [#incoming-connections] Functions handle HTTP requests through Easel’s request interface. Long-lived incoming WebSocket connections are not supported. Streaming HTTP and Server-Sent Events are separate from WebSockets and remain subject to function duration and connection limits. Signals and shutdown [#signals-and-shutdown] Easel may notify the runtime before termination where supported, but applications must not depend on receiving enough time to complete important work. Shutdown cleanup must be best effort. Examples include: * Flushing buffered telemetry * Closing clients * Releasing temporary resources * Stopping internal timers Durable state must be committed before the response or through a durable external system. Timers and background loops [#timers-and-background-loops] Do not start unbounded background loops from a request handler. Avoid: ```ts setInterval(() => { refreshSomething(); }, 60_000); ``` The instance may stop at any time, and repeated initialization can create multiple overlapping loops. Use a scheduled job or durable task system for recurring work. Short timers used during an active invocation are acceptable when they complete within the invocation lifecycle. Environment variables [#environment-variables] Environment variables are available to application code according to the deployment environment. Do not mutate `process.env` as a way to share request state. Treat secret variables as sensitive and avoid writing them to logs. Time and locale [#time-and-locale] Do not assume that the runtime’s system timezone matches the user or project location. Store and process timestamps in UTC unless application requirements specify otherwise. Format dates and times explicitly using the appropriate user locale and timezone. Error handling [#error-handling] Unhandled errors fail the invocation and may return a platform or framework error response. Applications must: * Catch expected failures * Return appropriate HTTP status codes * Avoid exposing stack traces or secrets * Include request IDs in support workflows * Use structured application logs Example: ```ts export async function GET() { try { const data = await loadData(); return Response.json(data); } catch (error) { console.error("Failed to load data", { error, }); return Response.json( { error: "Unable to load data", }, { status: 500, }, ); } } ``` Retries [#retries] Do not assume an HTTP invocation will be retried automatically. Clients, proxies, queues, or framework integrations may retry some requests independently. Application mutations must use idempotency keys when duplicate execution would be harmful. Runtime observability [#runtime-observability] Runtime telemetry can include: * Initialization duration * Invocation duration * Active requests * Memory usage * CPU usage * Errors * Region * Instance identifiers * Downstream spans Instance identifiers are diagnostic and must not be used for application routing or durable state. See [Observability](/docs/observability) and [Function observability](/docs/observability/functions). Related documentation [#related-documentation] * [Functions](/docs/functions) * [Function configuration](/docs/functions/configuration) * [Function regions](/docs/functions/regions) * [Function limits](/docs/functions/limits) * [Runtime Cache](/docs/runtime-cache) * [Observability](/docs/observability) * [Tracing](/docs/observability/tracing) # CI/CD Easel can deploy automatically from Git or explicitly from a CI system with the CLI. Choose a deployment model [#choose-a-deployment-model] Use [Git deployments](/docs/deployments/git) when repository events should be the source of truth. Use [CLI deploy](/docs/cli/deploy) when another system controls release orchestration, generated source, or prebuilt output. Avoid configuring both paths to deploy the same commit unintentionally. Authentication [#authentication] Authenticate non-interactive jobs with `EASEL_TOKEN` or `--token`. See [CLI authentication](/docs/cli/authentication). ```bash export EASEL_TOKEN=easel_… easel whoami --format json ``` Do not print the token or include it in build artifacts. Preview deployment [#preview-deployment] Link the project, then create a Preview deployment: ```bash easel link --project my-app easel deploy --wait --format json ``` Production deployment [#production-deployment] Create a Production deployment explicitly and promote it according to the project's release policy: ```bash easel deploy --prod --yes --wait --format json ``` Do not assume a Production build is already receiving production traffic when auto-assignment is disabled. See [Production releases](/docs/deployments/production-releases). Deployment checks [#deployment-checks] Before promotion, run checks against the immutable deployment URL: * Health and smoke tests * Authentication flows * Critical API routes * Cache and header validation * Database connectivity * Browser tests Concurrency and cancellation [#concurrency-and-cancellation] Cancel superseded builds where supported. Serialize production promotion to avoid an older workflow assigning traffic after a newer workflow. Monorepos [#monorepos] Use changed-path detection carefully and include shared packages, lockfiles, and root configuration in each application's dependency set. See [Monorepos](/docs/deployments/monorepos). # Common application patterns Static marketing site [#static-marketing-site] Build pages and assets ahead of time, serve them through the CDN, and use immutable caching for fingerprinted files. Add a single-page fallback only when the application uses client-side routing. Full-stack web application [#full-stack-web-application] Use framework-native server rendering and route handlers. Keep sessions and durable state in external services. Cache public responses and shared data independently. JSON API [#json-api] Validate inputs, return explicit cache headers, bound request bodies, and set timeouts for downstream services. Avoid caching authenticated responses unless the cache key safely varies by identity. Webhook endpoint [#webhook-endpoint] Verify the provider signature using the raw request body, persist the event or job before returning success, and make processing idempotent. Use a durable queue for work that must retry. Authentication callback [#authentication-callback] Keep callback routes dynamic, validate state and nonce values, use secure cookies, and avoid caching authentication responses. Incremental regeneration [#incremental-regeneration] Use the framework's supported revalidation APIs. Verify the resulting cache state and invalidation scope in Preview before relying on it in Production. Database-backed page [#database-backed-page] Reuse a concurrency-safe database client. Keep the database region compatible with the Function region and use Runtime Cache for reusable, non-sensitive query results. File upload [#file-upload] Prefer direct-to-object-storage uploads with signed URLs for large files. Validate metadata and authorization before issuing upload credentials. Long-running work [#long-running-work] Persist a job and return `202 Accepted`. Process it with a durable worker that supports retries, visibility, and idempotency rather than holding an HTTP Function open. Gradual migration [#gradual-migration] Use rewrites or DNS cutover to move bounded traffic to Easel. Keep the previous platform available until validation and rollback windows have passed. # Debugging guide Use a request-first workflow. Avoid changing multiple platform settings before identifying which layer failed. 1. Identify the deployment [#1-identify-the-deployment] Confirm the exact URL, deployment ID, source revision, environment, and status. Reproduce against the immutable deployment URL when possible. If the deployment URL works but the production domain does not, investigate domain assignment, DNS, TLS, or host routing rather than rebuilding the application. 2. Capture the request ID [#2-capture-the-request-id] Inspect the response for the Easel request ID and use it to open request details, logs, traces, cache diagnostics, and security events. ```bash curl -i https://example.com/problem-path ``` 3. Determine which layer answered [#3-determine-which-layer-answered] Check whether the request was: * Blocked or challenged by security controls * Redirected or rewritten * Served from CDN cache * Served as a static asset * Routed to a Function * Returned as not found 4. Inspect Function execution [#4-inspect-function-execution] For dynamic requests, review application logs, error details, duration, memory, initialization state, region, and trace spans. Correlate downstream database or API failures with the same trace where possible. 5. Compare environments [#5-compare-environments] Differences between Preview and Production often come from environment variables, production-only domains, credentials, cache state, or build settings. 6. Reproduce minimally [#6-reproduce-minimally] Reduce the failing route to its smallest input. Test without browser extensions, service workers, cached redirects, or authenticated state when relevant. 7. Check platform status and limits [#7-check-platform-status-and-limits] Review the status page and applicable request, Function, deployment, and observability limits. Escalation details [#escalation-details] When contacting support, include: * Workspace and project * Deployment ID and URL * Exact UTC timestamp * Request ID * Route and HTTP method * Expected and actual behavior * Reproduction steps * Relevant logs with secrets removed # Performance guide Application performance on Easel depends on delivery, caching, Function execution, external dependencies, and browser behavior. Start with request details [#start-with-request-details] Measure before changing configuration. For a slow request, identify: * Whether it was served from cache, static output, or a Function * Delivery and Function region * Function duration and initialization state * Time spent in external databases or APIs * Response size and compression * Trace spans and application logs Prefer static output [#prefer-static-output] Generate static pages and assets when content does not require request-time execution. Static output avoids Function startup and external service latency. Cache complete responses [#cache-complete-responses] Use shared-cache directives for responses that can be reused. Verify the actual `X-Easel-Cache` result rather than assuming a route is cached. Do not cache personalized responses unless the cache key safely separates users. Cache application data [#cache-application-data] Use Runtime Cache for database results or expensive computations that are reused across requests. Keep CDN response caching and Runtime Cache conceptually separate. Reduce dependency latency [#reduce-dependency-latency] Dynamic request latency often depends more on databases and external APIs than on application code. Choose compatible regions, reuse connection pools, parallelize independent calls, and set explicit timeouts. Keep Functions concurrency-safe [#keep-functions-concurrency-safe] Easel Functions can process overlapping requests. Reuse concurrency-safe clients, but do not store request-specific state in mutable module-level variables. Stream intentionally [#stream-intentionally] Streaming can improve time to first byte and perceived responsiveness, but the Function remains active until execution and response delivery finish. Streaming is not a substitute for caching or bounded execution. Optimize assets [#optimize-assets] * Cache fingerprinted assets for a long duration. * Compress eligible text responses. * Use responsive images. * Avoid shipping unused JavaScript. * Preload only critical resources. Improve builds [#improve-builds] Commit lockfiles, avoid unnecessary monorepo rebuilds, cache dependency work where supported, and keep build scripts deterministic. Validate changes [#validate-changes] Compare Preview deployments using the same route, data conditions, and geographic test location. Confirm that an improvement does not increase error rate, stale content, or resource usage. # Application security guide Easel provides platform protections, firewall rules, Attack Mode, deployment protection, managed TLS, and security telemetry. These controls complement application security; they do not replace it. Protect non-production deployments [#protect-non-production-deployments] Require authentication or an access policy for Preview deployments that contain unreleased features, customer data, or privileged APIs. Do not assume an unlinked deployment URL is private. Store secrets outside source control [#store-secrets-outside-source-control] Use environment variables or the supported secret mechanism. Separate Preview and Production credentials, rotate exposed values, and avoid logging tokens or session data. Restrict application access [#restrict-application-access] Implement authentication and authorization in the application. A firewall can block traffic patterns, but it cannot determine whether a signed-in user may access a specific business object. Use narrow firewall rules [#use-narrow-firewall-rules] Start with logging when possible, inspect matched traffic, and then enforce. Avoid broad IP or user-agent rules that can block legitimate users or be trivially bypassed. Protect sensitive endpoints [#protect-sensitive-endpoints] Apply stricter controls to login, password reset, account recovery, administrative APIs, uploads, and expensive mutations. Use rate limiting when available and application-level quotas where identity-aware enforcement is required. Validate all input [#validate-all-input] Validate request bodies, query parameters, headers, file uploads, and webhook signatures. Treat rewritten or proxied requests as untrusted input. Secure cookies and headers [#secure-cookies-and-headers] Use `Secure`, `HttpOnly`, and appropriate `SameSite` cookie settings. Configure CSP, HSTS, frame restrictions, and other response headers according to the application's requirements. Review dependencies [#review-dependencies] Keep frameworks, adapters, runtime packages, and lockfiles current. Test upgrades in Preview before promotion. Monitor and respond [#monitor-and-respond] Review security events and unusual error or traffic patterns. Keep a rollback plan, credential-rotation procedure, and contact path for reporting vulnerabilities. See [Shared responsibility](/docs/security/shared-responsibility) for the division between Easel and application responsibilities. # Migrate from another platform Use this guide when the source platform does not have a dedicated migration page. Separate application behavior from platform behavior [#separate-application-behavior-from-platform-behavior] Identify what belongs to: * Application source and framework * Build system and runtime * CDN, domain, and TLS * Security controls * Managed services * Observability * Deployment automation Application code may transfer unchanged while platform-specific services require replacement. Record the current build [#record-the-current-build] Document: ```text Source repository Root directory Install command Build command Output directory Runtime version Environment variables Generated artifacts ``` Record runtime dependencies [#record-runtime-dependencies] Inventory: * Functions and containers * Persistent processes * WebSockets and streaming * Background jobs, queues, and scheduled work * Local filesystem use and persistent disks * Native binaries * Region and private-network requirements WebSockets are not supported on Easel today for typical framework deployments. Plan replacements before cutover when the application depends on them. Record delivery behavior [#record-delivery-behavior] Inventory CDN caching, invalidation, redirects, rewrites, headers, compression, image optimization, static assets, custom error pages, domain routing, and TLS. Record managed services [#record-managed-services] Identify dependencies such as database, object storage, key-value store, message queue, authentication, email, analytics, feature flags, and secret managers. Decide whether each service remains external or needs replacement. Easel does not replace those managed data services as part of a deploy. Import and validate [#import-and-validate] 1. Import the GitHub repository into Easel (or deploy with the [CLI](/docs/cli)). 2. Recreate build settings and environment variables. 3. Use a non-production Easel URL until the [Production validation checklist](/docs/migration/production-checklist) passes. Cut over [#cut-over] Follow [Domain cutover](/docs/migration/domain-cutover) and prepare [Migration rollback](/docs/migration/rollback) before changing traffic. Related guides [#related-guides] * [Migration overview](/docs/migration) * [Getting started](/docs/getting-started) * [Framework guides](/docs/framework-guides) * [Functions](/docs/functions) * [CDN](/docs/cdn) # Domain cutover A domain cutover changes where public DNS sends requests. Prepare the Easel deployment and rollback path before changing traffic. Before the cutover [#before-the-cutover] Confirm: * [Production validation](/docs/migration/production-checklist) is complete * The expected Easel Production deployment is Ready and Current * The previous platform remains available * Existing DNS values are recorded * Relevant TTLs were lowered in advance * The domain is added in Easel * Mail and verification records are preserved * Monitoring is active * Rollback criteria are agreed See [Migrate a domain to Easel](/docs/domains/migrate) for detailed DNS guidance. Define rollback criteria [#define-rollback-criteria] Examples include: * Sustained 5xx rate above an agreed threshold * Authentication unavailable * Database writes failing * Critical webhook failures * TLS errors * Severe latency regression Use application-specific criteria rather than relying only on total request volume. Change DNS [#change-dns] Update the record to the exact value shown by Easel: | Host | Record | | ----------------------------- | ---------------------------------------------- | | Apex (`example.com`) | **A** records to the edge IPs in the dashboard | | Subdomain (`www.example.com`) | **CNAME** to `cname.easelusercontent.com` | Do not use ALIAS, ANAME, or CNAME flattening for apex verification. Do not add AAAA unless Easel shows IPv6 values for your project. Do not remove unrelated records such as MX, SPF, DKIM, DMARC, or service verification TXT records. If you use Cloudflare or another proxy, switch to DNS-only until the domain is live on Easel. See [DNS configuration](/docs/domains/dns). TLS [#tls] Easel provisions certificates with Let’s Encrypt after DNS points at Easel. First HTTPS requests can fail until the certificate is Ready. See [TLS](/docs/domains/tls). Verify from multiple networks [#verify-from-multiple-networks] Check: ```bash dig example.com A dig www.example.com CNAME curl -I https://example.com ``` Confirm: * DNS reaches Easel * HTTPS certificate is valid * Correct project responds * Expected deployment is Current * Canonical redirect works Monitor the application [#monitor-the-application] Watch in [Observability](/docs/observability) and [Logs](/docs/observability/logs): * Request volume and status codes * Error rate and latency * Function failures * Authentication and API writes * Cache behavior * Traffic still reaching the old platform Keep the previous platform active [#keep-the-previous-platform-active] Do not immediately remove the old domain configuration, delete the project, or revoke credentials. Clients may retain cached DNS answers during the transition. Raise TTL after stabilization [#raise-ttl-after-stabilization] Once traffic is stable and rollback is no longer likely, increase the DNS TTL to the desired operational value. Related guides [#related-guides] * [Migrate a domain](/docs/domains/migrate) * [Migration rollback](/docs/migration/rollback) * [Instant Rollback](/docs/deployments/rollback) # Migrate to Easel Move an existing application to Easel while preserving the familiar Git, Preview, and Production workflow. Some source-platform features and configuration may need to be translated. Choose your migration guide [#choose-your-migration-guide] * [Migrate from Vercel](/docs/migration/vercel) * [Migrate from Netlify](/docs/migration/netlify) * [Migrate from another platform](/docs/migration/custom-platform) Migration phases [#migration-phases] A safe migration has six phases: ```text Inventory ↓ Import ↓ Translate configuration ↓ Validate ↓ Cut over traffic ↓ Retire the old platform ``` Keep the previous platform available until the Easel deployment is stable and the rollback window has passed. 1. Inventory the application [#1-inventory-the-application] Record: * Repository and framework * Root directory, install command, build command, output directory * Runtime version * Environment variables (Preview and Production) * Functions and middleware * Redirects, rewrites, and response headers * Image behavior * Cache and revalidation behavior * Scheduled or background work * Storage and databases * Webhooks * Domains * Deployment protection and firewall rules * Log drains and observability integrations * Monorepo path filters * Git deployment settings A successful build does not prove that every platform dependency has been migrated. 2. Confirm framework support [#2-confirm-framework-support] Review the matching [framework guide](/docs/framework-guides). Pay particular attention to: * Server rendering and static generation * Streaming and middleware * Image optimization * Caching and revalidation * Function limits and regions 3. Import the repository [#3-import-the-repository] Import the same GitHub repository into Easel. Do not change production DNS yet. Review: * Framework detection * Root directory * Build commands * Production branch * Preview behavior * Environment variables See [Getting started](/docs/getting-started). 4. Translate platform configuration [#4-translate-platform-configuration] Platform-specific files and features may not transfer directly. Examples include: ```text vercel.json netlify.toml _redirects _headers Platform-specific function directories Platform-specific runtime APIs ``` Use the source-platform migration guide to map each feature to Easel. Easel does not automatically import Netlify or full Vercel project configuration. 5. Validate outside Production [#5-validate-outside-production] Use the unique Easel deployment URL or a Preview URL. Test homepage, deep links, authentication, APIs, databases, static assets, images, redirects, cache behavior, webhooks, and error pages. See [Production validation checklist](/docs/migration/production-checklist). 6. Prepare the domain cutover [#6-prepare-the-domain-cutover] Before changing DNS: * Add the domain in Easel * Record existing DNS values * Preserve MX, SPF, DKIM, DMARC, and verification records * Lower the relevant TTL in advance * Keep the previous platform active * Define a rollback threshold TLS certificates are issued after DNS points at Easel. See [Domain cutover](/docs/migration/domain-cutover) and [Migrate a domain](/docs/domains/migrate). 7. Cut over traffic [#7-cut-over-traffic] Update the DNS record shown by Easel: * Apex: **A** records to the edge IPs in the dashboard * Subdomain: **CNAME** to `cname.easelusercontent.com` Monitor error rate, latency, TLS, authentication, webhooks, and cache behavior in [Observability](/docs/observability) and [Logs](/docs/observability/logs). 8. Roll back when necessary [#8-roll-back-when-necessary] Restore the previous DNS record when production validation fails or error levels exceed the agreed threshold. DNS rollback is not instantaneous for clients that cached the new answer. See [Migration rollback](/docs/migration/rollback). 9. Retire the previous platform [#9-retire-the-previous-platform] After the migration is stable: * Confirm traffic no longer reaches the previous platform * Remove obsolete domains and redirects * Revoke old deployment tokens * Rotate credentials that no longer need access * Disable old build hooks * Archive required logs * Cancel the old plan when appropriate Do not delete the old project until the rollback window has passed. Multi-application migrations [#multi-application-migrations] For several applications, start with a representative, lower-risk service. Run it long enough to observe normal deployments, production traffic, operational incidents, billing, cache behavior, and at least one release or rollback cycle. Then migrate more critical applications using the validated process. Related guides [#related-guides] * [Getting started](/docs/getting-started) * [Framework guides](/docs/framework-guides) * [Deployments](/docs/deployments) * [Domains](/docs/domains) # Migrate from Netlify Netlify and Easel both support Git-based deployments and previews, but their deployment terminology and configuration files differ. Easel does not automatically import `netlify.toml`, `_redirects`, or `_headers`. Translate those settings into Easel project settings and framework-native configuration. Concept mapping [#concept-mapping] | Netlify | Easel | | -------------------------- | ----------------------------------------------------------- | | Site | Project | | Deploy Preview | Preview deployment | | Branch deploy | Branch Preview | | Production deploy | Production deployment | | Published deploy | Current Production deployment | | Deploy permalink | Unique deployment URL | | Build contexts | Preview and Production environments | | Netlify Functions | Easel Functions (after code adaptation) | | Edge Functions | Framework middleware or function equivalent, when supported | | Traffic Rules | Easel Firewall | | Password or visitor access | Deployment protection (workspace sign-in) | | Domain management | Easel Domains | Inventory Netlify configuration [#inventory-netlify-configuration] Review: * `netlify.toml`, `_redirects`, `_headers` * Build command, publish directory, base directory * Build contexts and environment variables * Functions and Edge Functions * Scheduled and background functions * Forms, Identity, Blobs, Large Media, Image CDN * Plugins and split testing * Traffic Rules, rate limiting, deploy protection * Log drains and integrations Mark every Netlify-specific capability as supported differently, requires external replacement, not required, or unsupported. Import the repository [#import-the-repository] Import the GitHub repository into Easel. Map: ```text Netlify base directory → Easel root directory Netlify build command → Easel build command Netlify publish directory → framework or Easel output setting Production branch → Easel Production branch ``` Review the matching [framework guide](/docs/framework-guides). Translate build and routing [#translate-build-and-routing] Move build settings to the Easel project. Translate redirects and headers from `_redirects` / `netlify.toml` into: * Framework routing (preferred when the framework owns the routes) * Application redirects * [Domain redirects](/docs/domains/redirects) when the rule is hostname-level * [Security headers](/docs/security/security-headers) or framework header config Validate status codes, wildcards, SPA fallbacks, and proxy-style rewrites carefully. Behavior will not match Netlify one-for-one without testing. Functions [#functions] Compare Netlify Functions with [Easel Functions](/docs/functions): * Request and response API * Runtime and bundling * Duration and region * Environment variables * Background and scheduled execution Netlify-specific APIs require code changes or an external replacement. Scheduled and background Netlify functions are not first-class Easel products; use an external scheduler or queue when needed. Edge Functions [#edge-functions] Review every Edge Function for runtime APIs, geographic assumptions, cookies, headers, rewrites, and streaming. Use framework middleware or Node.js functions on Easel only after compatibility is confirmed for your stack. Netlify Forms and Identity [#netlify-forms-and-identity] These are application services, not ordinary deployment settings. Replace them with application-owned forms and API routes, an external form provider, an authentication provider, or a database-backed implementation. Do not treat a successful static build as proof that these services were migrated. Observability and drains [#observability-and-drains] Use [Observability](/docs/observability) and [Logs](/docs/observability/logs). Customer log drains are not available yet. See [Export](/docs/observability/export). Validate and cut over [#validate-and-cut-over] Complete the [Production validation checklist](/docs/migration/production-checklist), then follow [Domain cutover](/docs/migration/domain-cutover). Keep the Netlify site and published deploy available until the rollback window has passed. # Production validation checklist Complete this checklist before changing production DNS. Deployment [#deployment] * [ ] Expected source commit deployed * [ ] Deployment environment is Production * [ ] Deployment reached Ready * [ ] Deployment is Current (or promotion plan is clear) * [ ] Framework detected correctly * [ ] Build settings match the application * [ ] Required resources were generated * [ ] No unexplained build warnings Routes and rendering [#routes-and-rendering] * [ ] Homepage * [ ] Deep links * [ ] Dynamic routes * [ ] Server-rendered routes * [ ] Static routes * [ ] API routes * [ ] Redirects * [ ] Rewrites * [ ] Custom error pages * [ ] Not-found behavior Authentication and sessions [#authentication-and-sessions] * [ ] Sign in * [ ] Sign out * [ ] Session refresh * [ ] Password reset * [ ] OAuth callbacks (updated for the new hostname) * [ ] Cookie domain and Secure cookie behavior * [ ] CSRF protections * [ ] Authorized and unauthorized routes Data and integrations [#data-and-integrations] * [ ] Database reads and writes * [ ] Object storage and file uploads * [ ] Email * [ ] Webhooks * [ ] Third-party APIs * [ ] Queues and background work * [ ] Scheduled work (external scheduler if used) Delivery [#delivery] * [ ] Static assets, images, and fonts * [ ] Compression * [ ] Cache headers * [ ] Cache hits and misses (`X-Easel-Cache`) * [ ] Revalidation * [ ] Streaming * [ ] Large responses Security [#security] * [ ] Platform protections active * [ ] Firewall rules translated, if needed * [ ] Attack Mode disabled unless intentionally needed * [ ] Deployment protection correct for previews * [ ] Security headers * [ ] CORS * [ ] Secrets scoped to Preview vs Production correctly Domains [#domains] * [ ] Apex and `www` / canonical redirect planned * [ ] DNS records recorded * [ ] Mail and verification records preserved * [ ] TLS path understood (cert after DNS points to Easel) * [ ] OAuth and webhook URLs updated * [ ] Canonical URLs, sitemap, and robots behavior correct Observability [#observability] * [ ] Requests appear in **Logs** * [ ] Function activity and Traces appear as expected * [ ] Cache outcomes visible * [ ] Error rate baseline recorded * [ ] Latency / compute baseline recorded Failure testing [#failure-testing] * [ ] Missing route * [ ] Application error * [ ] Upstream timeout * [ ] Invalid authentication * [ ] Failed webhook signature * [ ] Function timeout behavior * [ ] DNS rollback procedure rehearsed * [ ] Instant Rollback path understood for post-cutover releases Acceptance [#acceptance] Record: ```text Validated deployment ID: Validated commit: Validator: Date: Known limitations: Rollback threshold: ``` Related guides [#related-guides] * [Domain cutover](/docs/migration/domain-cutover) * [Migration rollback](/docs/migration/rollback) * [Observability](/docs/observability) # Migration rollback A migration rollback restores the previous traffic destination. It is different from an Easel [Instant Rollback](/docs/deployments/rollback), which switches between deployments inside Easel without changing DNS. Before migration [#before-migration] Record: * Previous DNS record type * Previous DNS value * Previous TTL * Previous platform project * Required credentials * Validation commands * Responsible operator Keep the previous application and services running. When to roll back [#when-to-roll-back] Use the criteria established before cutover. Typical triggers include: * Critical application paths unavailable * Authentication failure * Data corruption risk * Webhook or payment failure * TLS failure * Sustained error increase * Severe performance regression Restore DNS [#restore-dns] Replace the Easel routing record with the recorded previous value. Do not guess or recreate the value from memory. Understand propagation [#understand-propagation] Some users may continue reaching Easel until their cached DNS answer expires. During rollback: * Keep both platforms operational * Monitor traffic on both * Avoid destructive configuration changes * Communicate partial recovery accurately Verify recovery [#verify-recovery] Confirm: * DNS resolves to the previous platform * HTTPS works * Authentication works * Critical writes work * Webhooks succeed * Error rate returns to baseline * Traffic decreases on Easel Preserve evidence [#preserve-evidence] Keep: * Easel deployment ID * Request IDs and Trace links * Logs and error screenshots * DNS timestamps * Monitoring data Do not delete the Easel project while investigating. Prepare the next attempt [#prepare-the-next-attempt] Before another cutover: 1. Identify the failure. 2. Reproduce it outside Production. 3. Correct the application or configuration. 4. Repeat the [Production validation checklist](/docs/migration/production-checklist). 5. Confirm rollback readiness again. Related guides [#related-guides] * [Domain cutover](/docs/migration/domain-cutover) * [Instant Rollback](/docs/deployments/rollback) * [Observability troubleshooting](/docs/observability/troubleshooting) # Migrate from Vercel Vercel and Easel share a Git-based Preview and Production workflow, but not every platform feature has identical semantics. Concept mapping [#concept-mapping] | Vercel | Easel | | ----------------------------- | -------------------------------------------- | | Project | Project | | Preview deployment | Preview deployment | | Production deployment | Production deployment | | Current production deployment | Current Production deployment | | Generated deployment URL | Unique deployment URL | | Branch URL | Branch Preview URL | | Environment variables | Environment variables (Preview / Production) | | Functions | Easel Functions | | Firewall | Easel Firewall | | Attack Challenge Mode | Attack Mode | | Observability | Observability, Logs, Invocations, Traces | | Domains | Easel Domains | | Deployment Protection | Deployment protection (workspace sign-in) | Inventory Vercel configuration [#inventory-vercel-configuration] Review: * Framework preset, root directory, install and build commands * Node.js version and Production branch * Environment variables * `vercel.json` and framework-native redirects, rewrites, and headers * Functions and middleware * ISR and revalidation * Data Cache / Runtime Cache usage * Image optimization * Cron jobs * Blob, KV, Postgres, or other managed storage * Firewall rules and deployment protection * Log drains, Web Analytics, or Speed Insights * Build and function limits Import the repository [#import-the-repository] Import the GitHub repository into Easel and compare detected settings with the Vercel project. Do not assume matching framework detection means matching runtime behavior. Review the matching [framework guide](/docs/framework-guides). Translate configuration [#translate-configuration] Build settings [#build-settings] Map install command, build command, root directory, and Production branch into Easel project settings. Redirects, rewrites, and headers [#redirects-rewrites-and-headers] Prefer framework-native configuration where possible (for example `next.config` for Next.js). For Vite-style static apps, `vercel.json` redirects, rewrites, and headers may apply—see the [Vite guide](/docs/framework-guides/vite). Do not copy unsupported `vercel.json` fields without verifying their effect. Cron schedules in `vercel.json` are not a first-class Easel product today. Environment variables [#environment-variables] Create Preview and Production values in **Project settings → Environment Variables**. Check: * Build-time versus runtime values * Public variable prefixes * Preview branch overrides * Whether redeployments need new snapshots after secret changes Deployments snapshot variables at create time. See [Deployment environments](/docs/deployments/environments) and [Secrets](/docs/security/secrets). Functions and middleware [#functions-and-middleware] Compare runtime, duration, streaming, `after` / `waitUntil` behavior, and filesystem assumptions with [Functions](/docs/functions) and your framework guide. Easel Functions run in US East (`iad`) today. See [Function regions](/docs/functions/regions) and [Function limits](/docs/functions/limits). Caching and revalidation [#caching-and-revalidation] Inventory static assets, CDN cache headers, ISR, revalidation APIs, and Runtime Cache usage. Validate with [Caching](/docs/cdn/caching), [Revalidation](/docs/cdn/revalidation), [Runtime Cache](/docs/runtime-cache), and [Cache observability](/docs/observability/cache). Images [#images] For Next.js, `next/image` can use Easel image optimization. Test remote allowlists, formats, and cache behavior after deploy. Cron and background work [#cron-and-background-work] Easel does not provide a first-class cron product today. Move scheduled work to: * An external scheduler calling a protected HTTP route * A queue or workflow system * A separate service Background work that continues after the response should use framework `after` / `waitUntil` patterns within function duration limits—not unbounded platform lifetime. Firewall and Attack Mode [#firewall-and-attack-mode] Recreate only the controls the application needs: * [Custom firewall rules](/docs/security/custom-rules) * [Attack Mode](/docs/security/attack-mode) * [Deployment protection](/docs/security/deployment-protection) Attack Mode is an emergency traffic control, not application authentication. Observability [#observability] After importing: 1. Generate representative requests. 2. Confirm records in **Logs**. 3. Inspect Traces, cold starts, and Invocations. 4. Validate cache outcomes. Customer log drains and OTLP export sinks are not available yet. See [Export observability data](/docs/observability/export). Validate and cut over [#validate-and-cut-over] Complete the [Production validation checklist](/docs/migration/production-checklist), then follow [Domain cutover](/docs/migration/domain-cutover). Keep the Vercel project active until the rollback window has passed. # Cache observability Cache observability explains whether Easel served a response from cache or performed additional work. Cache outcomes [#cache-outcomes] Easel exposes cache processing through the `X-Easel-Cache` response header and related Logs filters. | Status | Meaning | | ---------- | ---------------------------------------------------------------------------------------------------------------------- | | **HIT** | A fresh cached response was served | | **STALE** | A stale cached response was served within an explicit stale-while-revalidate window while refreshing in the background | | **MISS** | No usable fresh or SWR-eligible stale entry was found; origin or regeneration ran | | **BYPASS** | The request or response was not eligible for shared caching | | **ERROR** | Cache processing encountered an error path | Treat `X-Easel-Cache` as authoritative for whether Easel reused a cached response. See [Caching](/docs/cdn/caching) and [CDN headers](/docs/cdn/headers). Definitions [#definitions] HIT [#hit] The response was served from cache without invoking the application origin or function for that response body. MISS [#miss] No usable cached response existed, so Easel fetched or generated one. BYPASS [#bypass] The request did not use shared cache because of request properties, response headers, or policy (for example `Authorization`, `Set-Cookie`, `private`, or missing shared TTL). STALE [#stale] A stale entry was eligible under `stale-while-revalidate` and was served while Easel refreshed in the background. Security and cache [#security-and-cache] Security controls run before cache lookup. A blocked or challenged request does not proceed to ordinary application cache serving. See [Security evaluation order](/docs/security/request-chain). Runtime Cache panel [#runtime-cache-panel] On Observability, the Runtime Cache panel summarizes project-scoped runtime cache operations such as reads, hits, misses, writes, revalidations, and evictions for frameworks that use Easel’s runtime cache APIs. See [Runtime Cache](/docs/runtime-cache). Debugging workflow [#debugging-workflow] 1. Reproduce the request and read `X-Easel-Cache`. 2. Confirm cache headers on the origin response (`Cache-Control`, cookies). 3. Filter **Logs** by cache status when available. 4. For ISR or runtime-cache issues, check revalidation docs and the Runtime Cache panel. Related guides [#related-guides] * [Caching](/docs/cdn/caching) * [Revalidation](/docs/cdn/revalidation) * [CDN troubleshooting](/docs/cdn/troubleshooting) * [Request analytics](/docs/observability/requests) # Deployment events Deployment activity explains how application versions move from source to production traffic. Most deployment lifecycle detail lives in **Deployments**, not inside Observability charts. Use Observability to measure what happened **after** a deployment became Ready. What to inspect in Deployments [#what-to-inspect-in-deployments] From the project **Deployments** view (and CLI inspect), you can typically see: * Deployment created and status transitions * Build logs and failure reasons * Environment (`preview` or `production`) * Source commit and branch * Promotion and Instant Rollback actions * Which deployment is Current for production domains See [Deployments](/docs/deployments), [Promote](/docs/deployments/promote), and [Rollback](/docs/deployments/rollback). Correlate with Observability [#correlate-with-observability] After a release: 1. Note the deployment ID and Ready timestamp. 2. Open **Observability** for a window that starts at that time. 3. Watch error rate, compute, cold starts, and bandwidth. 4. Filter **Logs** by deployment ID when investigating a regression. 5. Open request Traces for slow or failing examples. Observability does not currently draw automatic deployment markers on charts. Use the deployment timestamp and filters instead. Configuration changes [#configuration-changes] Firewall rules, Attack Mode, and deployment protection can change traffic shape without a new deployment. When investigating a sudden block or challenge spike: * Check **Project settings** security controls * Review [Security events](/docs/observability/security) * Confirm the change time against the Observability window Environment variable changes apply to **new** deployments via snapshots. See [Secrets](/docs/security/secrets) and [Environments](/docs/deployments/environments). Related guides [#related-guides] * [Request analytics](/docs/observability/requests) * [Deployments overview](/docs/deployments) * [CLI deployments](/docs/cli/deployments) # Export observability data Use the dashboard and CLI to inspect and retrieve observability data for operations and debugging. Dashboard access [#dashboard-access] * **Observability**: charts and custom queries * **Logs**: request list, detail, log lines, and Traces * **Invocations**: wake-level metrics and request waterfalls Copy request IDs, invocation IDs, and deployment IDs from these views when filing support tickets or correlating with application logs. CLI access [#cli-access] Query the same request stream from the terminal: ```bash easel logs --since 1h --status-code 5xx easel logs --request-id --expand easel logs --follow --timeout 10m ``` See [Query request logs](/docs/cli/logs). What is not available yet [#what-is-not-available-yet] The following export destinations are not customer-facing product features today: * Log drains to external HTTPS endpoints * Customer-managed OTLP export sinks * Metrics export to third-party monitoring * Observability webhooks * Object-storage archives or SIEM connectors OpenTelemetry spans are collected **into** Easel Traces. See [OpenTelemetry](/docs/observability/opentelemetry). If you need longer retention or external analysis, export selected request IDs and summaries manually, or retain application-level logs in your own systems. Related guides [#related-guides] * [Logs](/docs/observability/logs) * [Retention and limits](/docs/observability/limits) * [Tracing](/docs/observability/tracing) # Function observability Function observability explains how Easel executes server-side code for a deployment. Use **Observability** charts for trends, **Logs** for per-request compute fields, and **Invocations** when one function wake served multiple HTTP requests. Invocations [#invocations] One function invocation (wake) can serve more than one HTTP request when the runtime keeps the isolate warm and multiplexes traffic. **Invocations** lists those wakes with columns such as: * Route * Request count (**Reqs**) * Cold start * Duration * Compute * AWS duration and AWS billed * Init * Memory * Instance Select an invocation to open: * **Overview**: summary metrics and a waterfall of requests served on that wake * **Trace**: an invocation-level Trace focused on lifecycle and per-request summary bars Use **Open in Logs** to jump to the request list for the same invocation ID. Duration fields [#duration-fields] Different duration values measure different things: | Field | Meaning | | -------------------------- | ---------------------------------------------------------- | | **Duration** (Invocations) | Wall time for the wake | | **Compute duration** | Customer function work through response body completion | | **Post-response** | Work after the response finishes (`waitUntil`, flush) | | **AWS duration** | Duration reported by the Lambda Telemetry API for the wake | | **AWS billed** | Billed duration reported for the wake | | **AWS init** / **Init** | Initialization time when the wake was cold | Do not use “duration” without checking which field you mean. Request Trace spans show where time went inside one HTTP request; AWS billed and cold start remain on request detail and Invocations. Cold and warm starts [#cold-and-warm-starts] * **Cold**: the wake initialized a new runtime process. * **Warm**: the wake reused an existing process. Observability **Function Starts** charts break volume into cold and warm. Easel does not use a separate “hot” product term. Memory and instance [#memory-and-instance] Invocations can show configured memory and maximum memory used for the wake, plus an **Instance** identifier for the underlying compute process. Function region for compute is typically `iad` (US East). See [Function regions](/docs/functions/regions). Errors and timeouts [#errors-and-timeouts] Failed requests appear with 5xx statuses in Logs and error series on Observability charts. Investigate with request detail, function log lines, and Trace spans. Application timeouts follow your route `maxDuration` and platform limits. See [Function limits](/docs/functions/limits). Related guides [#related-guides] * [Request details](/docs/observability/request-details) * [Tracing](/docs/observability/tracing) * [Functions overview](/docs/functions) * [Function runtime](/docs/functions/runtime) # Observability Easel observability connects each incoming request to the deployment, route, cache decision, function execution, and security action that handled it. Use observability to answer questions such as: * Which deployment served this request? * Did the response come from cache? * Which route matched? * Did a function run? * Was the function cold or warm? * Did a firewall rule block or challenge it? * Where did the request spend time? * Did the failure happen during build, routing, or runtime? Surfaces [#surfaces] Open a project and use these dashboard views: | Surface | Use it for | | ----------------- | -------------------------------------------------------------------------- | | **Observability** | Traffic, bandwidth, function starts, compute, and custom queries over time | | **Logs** | Individual requests, function log lines, and per-request Traces | | **Invocations** | Multiplexed function wakes, cold starts, and AWS billing fields | From the terminal, use [`easel logs`](/docs/cli/logs) to query and follow the same request stream. Observability model [#observability-model] Easel organizes observability around related data types: * **Requests** describe an incoming HTTP request and its final response. * **Logs** contain application and platform-generated messages for a request. * **Metrics** aggregate behavior over time on Observability charts. * **Traces** connect edge routing, function work, and application OpenTelemetry spans for one request. * **Invocations** describe a function wake that may serve multiple requests. * **Security signals** explain firewall and access-control decisions when available. * **Deployment activity** lives primarily in Deployments and build logs, with filters that let you scope Observability to a deployment. These signals share stable identifiers such as request ID and invocation ID so you can move from a chart to an individual request and then to its Trace. Request lifecycle [#request-lifecycle] A request may pass through several stages: ```text Client request ↓ Security evaluation ↓ Domain and deployment routing ↓ Cache lookup ↓ Static response, middleware, or function ↓ Response ``` Security controls run before cache and application execution. See [Security evaluation order](/docs/security/request-chain). Start here [#start-here] * [Request analytics](/docs/observability/requests) * [Request details](/docs/observability/request-details) * [Logs](/docs/observability/logs) * [Function observability](/docs/observability/functions) * [Cache observability](/docs/observability/cache) * [Metrics](/docs/observability/metrics) * [Tracing](/docs/observability/tracing) * [OpenTelemetry](/docs/observability/opentelemetry) * [Security events](/docs/observability/security) * [Deployment events](/docs/observability/deployments) * [Export](/docs/observability/export) * [Retention and limits](/docs/observability/limits) * [Troubleshooting](/docs/observability/troubleshooting) Related documentation [#related-documentation] * [Query request logs (CLI)](/docs/cli/logs) * [CDN headers](/docs/cdn/headers) * [Firewall observability](/docs/security/firewall-observability) * [Functions](/docs/functions) # Observability retention These limits describe current dashboard, CLI, and runtime capture behavior for observability signals. For platform-wide ceilings, see [Limits reference](/docs/reference/limits). Dashboard time windows [#dashboard-time-windows] Typical presets: | Surface | Windows | | -------------------- | ---------------------------------- | | Observability charts | 6h, 12h, 24h, 7d | | Logs | From about 30 minutes up to 7 days | Metric buckets are commonly 5 minutes (≤24h windows) or 60 minutes (longer windows). Longer archival retention is not published as a separate product SLA today. Request list [#request-list] Logs queries return a bounded number of rows per page or request (commonly up to a few hundred). Narrow the time range and filters when searching busy projects. Function log capture [#function-log-capture] Per-invoke console capture is bounded so telemetry cannot keep the runtime open indefinitely. Approximate platform limits: | Limit | Value | | ------------------------ | ------- | | Max lines per invoke | 256 | | Max bytes per line | 256 KiB | | Max total captured bytes | 1 MiB | Excess output may be dropped. Prefer concise structured logs on hot paths. OpenTelemetry span capture [#opentelemetry-span-capture] Application span buffers are bounded by count and size. Excess spans may be truncated on the Trace. See [OpenTelemetry](/docs/observability/opentelemetry). CLI follow [#cli-follow] `easel logs --follow` polls periodically (about every 10 seconds) and respects `--timeout` (default 5 minutes unless you set another value). Delivery [#delivery] Observability shipping is best-effort and must not block request handling. Do not treat Logs as a guaranteed, complete audit log for compliance without an external retention system. Related guides [#related-guides] * [Logs](/docs/observability/logs) * [Metrics](/docs/observability/metrics) * [Export](/docs/observability/export) * [Troubleshooting](/docs/observability/troubleshooting) # Logs Logs provide detailed messages from functions, application code, and selected platform components, correlated to individual requests. Where logs appear [#where-logs-appear] * **Logs** in the project dashboard: request list, detail panel, and dock * [`easel logs`](/docs/cli/logs) in the terminal Build output lives with the deployment (inspect build logs from Deployments or the CLI). Request Logs focus on runtime traffic after a deployment is serving requests. Request list and filters [#request-list-and-filters] **Logs** lists recent requests for the project. Filter by: * Time range * Environment * Method * Status * Path and route * Host * Cache status * Console level (`warning`, `error`, `fatal`) * Deployment ID Select a row to open detail. The dock **Logs** tab shows function and platform log lines for that request. Application logs [#application-logs] Write to standard console APIs in your function runtime. Easel captures console output and associates it with the request ID when available. Prefer structured JSON when you need searchable fields: ```json { "level": "info", "message": "Checkout completed", "order_id": "ord_123" } ``` Do not log secrets, tokens, or full connection strings. Automatic redaction cannot catch every transformed value. See [Secrets](/docs/security/secrets). Correlation fields [#correlation-fields] Useful identifiers: | Field | Use | | ------------------------- | -------------------------------------------------- | | Request ID (`X-Easel-Id`) | One HTTP request across Logs, Trace, and support | | Invocation ID | The function wake that may serve multiple requests | | Deployment ID | Scope to a specific deployment | | Environment | `production` or `preview` | From an Invocation, use **Open in Logs** or filter with `invocationId:…`. CLI [#cli] ```bash easel logs easel logs --follow --expand --timeout 10m easel logs --environment production --status-code 5xx --since 1h easel logs --request-id ``` `--follow` polls for new requests. `--expand` prints function log lines for each request. See [Query request logs](/docs/cli/logs) for the full option list. Capture limits [#capture-limits] Function log capture is bounded per invoke so telemetry cannot keep the runtime open indefinitely. Very chatty processes may drop excess lines. Prefer concise, structured messages for high-volume paths. Delivery is best-effort and must not block the request path. Do not treat Logs as a guaranteed audit archive. Related guides [#related-guides] * [Request details](/docs/observability/request-details) * [Tracing](/docs/observability/tracing) * [Function observability](/docs/observability/functions) * [Query request logs (CLI)](/docs/cli/logs) # Metrics Metrics summarize application and platform behavior over time on the project **Observability** page. Default charts [#default-charts] Typical charts include: Traffic [#traffic] * **Edge Requests**: request volume and error rates * **Edge Bandwidth**: incoming and outgoing bytes Functions [#functions] * **Function Starts**: cold versus warm starts * **Functions**: invocation and error series * **Compute**: total compute duration Runtime Cache [#runtime-cache] * Runtime Cache operations when the project uses runtime cache APIs Time windows and buckets [#time-windows-and-buckets] Select a preset window such as 6 hours, 12 hours, 24 hours, or 7 days. Aggregation buckets are typically: * 5 minutes for windows up to 24 hours * 60 minutes for longer windows Custom queries [#custom-queries] Open **Observability → Query** (or the project query view) to build charts from supported metrics such as: * Edge requests * Duration * Bandwidth / FDT fields * Route cache and route bandwidth * Function metrics Supported aggregations include count, sum, min, max, average, and percentiles such as p75, p90, p95, and p99 where the metric allows them. Custom query metrics are platform-defined fields and aggregations, not arbitrary customer-defined metric names emitted from application code. How to use metrics after a deploy [#how-to-use-metrics-after-a-deploy] 1. Open Observability for the hours after the release. 2. Compare error rate, latency/compute, cold starts, and bandwidth to the prior window. 3. When a series moves, open **Logs** for the same time range. 4. Inspect request detail and Trace for examples. Deployment overlay markers on charts are not a separate product feature today. Use time range and deployment ID filters instead. Related guides [#related-guides] * [Request analytics](/docs/observability/requests) * [Function observability](/docs/observability/functions) * [Cache observability](/docs/observability/cache) * [Retention and limits](/docs/observability/limits) # OpenTelemetry Easel collects OpenTelemetry spans emitted from your function runtime and nests them under the platform **Invoke function** span in the request Trace. Use OpenTelemetry to see database queries, outbound fetches, and application operations next to edge routing and invoke timing. How collection works [#how-collection-works] On supported Node.js runtimes, Easel provides a request context compatible with `@vercel/otel` and `telemetry.reportSpans`. Spans buffered during the invoke are flushed into the observability pipeline and attached to the request Trace. You do not configure a separate OTLP endpoint for Easel collection. Instrument your app; Easel ingests the spans with the request. Span capture is bounded (count and byte limits). Excess spans may be truncated. Next.js [#nextjs] Create `instrumentation.ts` in the project root or `src` directory: ```ts export async function register() { if (process.env.NEXT_RUNTIME === "nodejs") { await import("./instrumentation.node"); } } ``` In `instrumentation.node.ts`, initialize `@vercel/otel` or a standard OpenTelemetry Node SDK and instrumentation packages for the libraries you use. See the [Next.js framework guide](/docs/framework-guides/nextjs). Other Node.js frameworks [#other-nodejs-frameworks] For React Router, TanStack Start, and other Node.js server runtimes on Easel Functions: 1. Install OpenTelemetry SDK and instrumentation packages. 2. Register instrumentation at process startup for the server entry. 3. Instrument server routes, loaders, database clients, and outbound HTTP. 4. Deploy and open **Logs → Trace** for a request that hit the instrumented path. Framework-specific notes live in each [framework guide](/docs/framework-guides). What you will see [#what-you-will-see] After instrumentation: 1. Open **Logs** and select a request. 2. Open the **Trace** tab. 3. Expand **Invoke function**. 4. Inspect Framework, Fetch, and Custom spans from your application. If instrumentation is missing, platform spans still appear (Resolve Deployment, Resolve Route, Invoke middleware, Invoke function). Limits [#limits] * Collection is for Easel Traces, not a general-purpose customer OTLP sink. * Spans are associated with the active request and invocation when available. * Late async work should finish within `waitUntil` bounds so spans can flush before the isolate parks. See [Function runtime](/docs/functions/runtime). Related guides [#related-guides] * [Tracing](/docs/observability/tracing) * [Request details](/docs/observability/request-details) * [Next.js on Easel](/docs/framework-guides/nextjs) # Request details The request detail view connects one client request to the deployment, route, cache decision, function work, and logs that handled it. Request identity [#request-identity] Each request has a stable **request ID**. Easel also returns it to clients as `X-Easel-Id`. Use the request ID to: * Search Logs and the CLI (`easel logs --request-id …`) * Correlate function log lines and Trace spans * Contact support * Jump between related views The request ID is a UUID string generated at the edge. Open a request [#open-a-request] 1. Open the project and go to **Logs**. 2. Filter to the time range and path you care about. 3. Select a request row. 4. Review the detail panel. 5. Use the dock tabs **Logs** and **Trace**. From detail you can also open the matching invocation when one exists. Summary fields [#summary-fields] A request summary commonly includes: * Request ID * Timestamp (request started) * Host * Method and path * Status code * User agent and referer when available * Firewall badge when a security action is present * Function and middleware indicators when compute ran Timing and function fields [#timing-and-function-fields] When a function handled the request, the detail panel can show: | Field | Meaning | | -------------------- | ---------------------------------------------------------------------- | | **Compute duration** | Time executing customer function code through response body completion | | **Post-response** | Work after the response finishes (for example `waitUntil` / flush) | | **AWS duration** | Lambda-reported duration for the wake | | **AWS billed** | Lambda-reported billed duration for the wake | | **AWS init** | Initialization time when the wake was a cold start | | **Cold start** | Whether this invoke initialized a new runtime process | | **Invocation ID** | AWS request ID for the function wake | | **Instance** | Lambda instance identity for the wake | AWS duration, billed duration, and init apply to the **invocation** (the wake), not always as independent per-HTTP-request meters. Use Invocations when one wake served multiple requests. These fields sit on the detail panel. They are not separate Trace span names. Dock: Logs and Trace [#dock-logs-and-trace] The sticky dock under the list has two tabs for the selected request: * **Logs**: function and platform log lines for that request * **Trace**: the span tree for edge and function work on that request See [Logs](/docs/observability/logs) and [Tracing](/docs/observability/tracing). What detail does not replace [#what-detail-does-not-replace] * Cold start and AWS billed context also appear on **Invocations**. * Build failures appear in deployment build logs, not request detail. * Cache policy diagnosis often needs the `X-Easel-Cache` response header in addition to Logs filters. See [Cache observability](/docs/observability/cache). Related guides [#related-guides] * [Logs](/docs/observability/logs) * [Function observability](/docs/observability/functions) * [Tracing](/docs/observability/tracing) * [Query request logs (CLI)](/docs/cli/logs) # Request analytics Request analytics summarizes traffic handled by an Easel project. Use it to understand: * Request volume * Response status and error rate * Latency and compute * Cache effectiveness * Function starts (cold vs warm) * Bandwidth * Deployment-scoped activity Where to look [#where-to-look] 1. Open the project. 2. Open **Observability** for charts over a time window. 3. Open **Logs** for the request list behind a spike. 4. Select a request to open detail, log lines, and Trace. Typical Observability charts include: * **Edge Requests**: request volume and error rates at the edge * **Edge Bandwidth**: bytes served (incoming and outgoing) * **Function Starts**: cold versus warm starts * **Functions** and **Compute**: invocations, errors, and duration Pick a deploy window and compare it to the hours before the release. When a chart moves, use Logs or Invocations to inspect the requests behind the spike. Filters [#filters] In **Logs**, useful filters include: * Time range (for example 30 minutes to 7 days) * Environment (`production` or `preview`) * Method * Status * Path and route * Host * Cache status * Console level * Deployment ID Use `invocationId:…` style filters when jumping from an Invocation to its requests. From charts to requests [#from-charts-to-requests] A practical workflow: 1. Confirm the time window and environment on Observability. 2. Note which chart moved (errors, bandwidth, cold starts, compute). 3. Open **Logs** for the same window. 4. Filter to the failing status, path, or deployment. 5. Open a request for detail and Trace. Related guides [#related-guides] * [Request details](/docs/observability/request-details) * [Metrics](/docs/observability/metrics) * [Logs](/docs/observability/logs) * [Function observability](/docs/observability/functions) # Security events Security events explain why Easel allowed, blocked, logged, or challenged a request. For the full security product model, see [Firewall observability](/docs/security/firewall-observability). Where security shows up [#where-security-shows-up] * Request detail **Firewall** badge on Logs * Project overview firewall action snapshots * Observability charts that group by firewall action when the field is present * Response headers such as `X-Easel-Firewall-Action` and `X-Easel-Firewall-Rule-Id` on some denials Actions [#actions] Customer-facing action values you may see include: ```text deny challenge challenge_pass challenge_fail redirect log bypass ``` Platform protections may also surface actions such as `ban` or `throttle`. Decision sources [#decision-sources] Prefer product concepts when describing a decision: * Platform protection * Custom rule * Attack Mode * Deployment protection See [Attack Mode](/docs/security/attack-mode) and [Custom firewall rules](/docs/security/custom-rules). Debugging workflow [#debugging-workflow] 1. Reproduce the request and capture the request ID. 2. Open **Logs** for that ID or time window. 3. Check the Firewall badge and status code. 4. Confirm rule configuration in **Project settings → Custom WAF rules**. 5. Confirm whether Attack Mode or deployment protection is enabled. When a challenge interstitial appears, verification outcomes may show as challenge pass or fail around `/.well-known/easel-challenge/verify`. Related guides [#related-guides] * [Firewall observability](/docs/security/firewall-observability) * [Security troubleshooting](/docs/security/troubleshooting) * [Request details](/docs/observability/request-details) # Tracing A request Trace is a span tree for one HTTP request. Easel builds it from platform timing plus any OpenTelemetry (OTEL) spans your function emits. How to open a Trace [#how-to-open-a-trace] 1. Open the project and go to **Logs**. 2. Filter to the time range and path you care about. 3. Select a request row. 4. Open the **Trace** tab in the dock. 5. Choose **Timeline**, **Tree**, or **Waterfall**. From Logs detail you can also open the matching invocation when you need the function-level view. Platform spans [#platform-spans] Platform spans include: * Root span for the request method and path * **Resolve Deployment** and **Resolve Route** for edge routing * **Invoke middleware** and **Invoke function** for compute windows * Nested application OTEL spans under **Invoke function** when you instrument the runtime If the function has not emitted OTEL yet, the Trace still shows platform spans. A placeholder under the invoke notes that application spans appear there once instrumentation is present. Timeline, Tree, and Waterfall [#timeline-tree-and-waterfall] The Trace toolbar shares one span tree across three views: * **Timeline**: absolute-time lanes with zoom and a minimap * **Tree**: hierarchy with expand and collapse * **Waterfall**: indented labels with duration bars Search spans from the Trace toolbar when the tree is large. Span kinds in the UI include Platform, Framework, Fetch, and Custom. What a Trace does not replace [#what-a-trace-does-not-replace] Cold start flags and **AWS billed** duration remain on the request detail panel and on **Invocations**. Use them together with the Trace: * The Trace shows where time went inside the request * Detail and Invocations show billing and cold-start context for the wake OpenTelemetry [#opentelemetry] Instrument your Node.js function with OpenTelemetry so application spans nest under **Invoke function**. See [OpenTelemetry](/docs/observability/opentelemetry). Easel collects spans into the request Trace. Customer-managed OTLP export to an external backend is not a separate product feature today. Invocation Traces [#invocation-traces] **Invocations** can show an invocation-level Trace focused on wake lifecycle and per-request summary bars when one wake served multiple requests. Related guides [#related-guides] * [Request details](/docs/observability/request-details) * [OpenTelemetry](/docs/observability/opentelemetry) * [Function observability](/docs/observability/functions) * [Logs](/docs/observability/logs) # Observability troubleshooting Start with a request ID, deployment ID, invocation ID, or exact timestamp whenever possible. A request does not appear [#a-request-does-not-appear] Check: * Selected time range * Project and environment filters * Deployment ID * Domain or host filter * Whether the request hit a different project * Brief ingest delay after the request completed Very early platform-only traffic without project attribution may not appear in project Logs. Logs are missing [#logs-are-missing] Check: * The function actually ran (cache HIT and static responses produce little or no function output) * The selected deployment and environment * Console level filters * Whether capture limits dropped excess lines * Whether the process exited before flush on a crash path Traces look empty under Invoke function [#traces-look-empty-under-invoke-function] Platform spans still appear without application instrumentation. To see nested app spans: 1. Confirm OpenTelemetry registration in the runtime. 2. Deploy the instrumented build. 3. Hit a path that exercises the instrumented code. 4. Re-open **Logs → Trace**. See [OpenTelemetry](/docs/observability/opentelemetry). AWS billed or init fields are empty [#aws-billed-or-init-fields-are-empty] AWS duration, billed duration, and init come from the function wake telemetry report. They attach to the **invocation** and can arrive slightly after the HTTP response completes. Open **Invocations** for the wake, or refresh request detail after a short delay. Cache status disagrees with expectations [#cache-status-disagrees-with-expectations] 1. Read `X-Easel-Cache` on the live response. 2. Confirm `Cache-Control` and cookies on the origin response. 3. See [Cache observability](/docs/observability/cache) and [CDN troubleshooting](/docs/cdn/troubleshooting). Firewall badge is Allow but the client saw a challenge [#firewall-badge-is-allow-but-the-client-saw-a-challenge] Challenge and platform decisions can short-circuit before ordinary application handling. Confirm Attack Mode and custom rules in project settings, and see [Security events](/docs/observability/security). Metrics and Logs disagree slightly [#metrics-and-logs-disagree-slightly] Charts aggregate over buckets; Logs lists individual requests. Align the time window, environment, and deployment filters. Expect brief ingest delay and best-effort delivery. Information to include with support [#information-to-include-with-support] Include: * Workspace and project * Deployment ID * Request ID * Invocation ID when relevant * Approximate timestamp and timezone * Environment and domain * Expected versus observed behavior * Screenshot or CLI output of `easel logs --request-id … --expand` Related guides [#related-guides] * [Request details](/docs/observability/request-details) * [Logs](/docs/observability/logs) * [Tracing](/docs/observability/tracing) * [Retention and limits](/docs/observability/limits) # Core concepts Project [#project] A project connects source code and platform configuration to a deployable application. It contains build settings, environment variables, domains, security controls, and deployment history. Deployment [#deployment] A deployment is an immutable version of an application built from a specific source revision and configuration. Every successful deployment receives a unique URL. Environment [#environment] An environment selects the settings used to build and run a deployment. Easel currently distinguishes Preview and Production configuration. An environment is not a long-running server and is not the same thing as a deployment. Preview deployment [#preview-deployment] A Preview deployment is built with Preview configuration. Branch and pull-request URLs can move forward to the latest ready deployment associated with that source context. Production deployment [#production-deployment] A Production deployment is built with Production configuration. It may be assigned to production domains automatically or remain staged until promoted. Current deployment [#current-deployment] The Current deployment is the Production deployment receiving traffic for production domains. Promoting or rolling back changes this assignment without modifying the deployment itself. Deployment URL [#deployment-url] A deployment URL identifies one immutable deployment. Branch, pull-request, and production URLs are aliases that may point to different deployments over time. Static asset [#static-asset] A static asset is a file produced by the build and served without application compute, such as HTML, JavaScript, CSS, fonts, and images. Function [#function] A Function runs server-side application code, including server-rendered routes, API handlers, loaders, actions, webhooks, and supported framework background work. Request and invocation [#request-and-invocation] A request is one HTTP exchange received by Easel. An invocation is an active Function execution session. One invocation may serve more than one request. CDN cache [#cdn-cache] The CDN cache stores complete HTTP responses and is evaluated before a Function is invoked. Runtime Cache [#runtime-cache] Runtime Cache stores application values accessed from Function code. It is separate from HTTP response caching. Redirect and rewrite [#redirect-and-rewrite] A redirect returns a 3xx response that tells the client to request another URL. A rewrite changes the internal destination while preserving the browser-visible URL. Region and edge location [#region-and-edge-location] An edge location receives and delivers requests. A compute region runs dynamic application code. These locations are not necessarily the same. Request ID [#request-id] A request ID uniquely identifies an Easel request and can be used to correlate response headers, request details, logs, traces, cache diagnostics, and security events. # Platform limits Easel applies limits to builds, deployments, delivery, Functions, security controls, observability, and workspace resources. The canonical limits reference is [Reference: Limits](/docs/reference/limits). Use that page for exact current values. Why limits exist [#why-limits-exist] Limits protect platform reliability, bound resource consumption, and make application behavior predictable. Some limits are fixed platform constraints; others vary by plan or can be raised by agreement. Before production launch [#before-production-launch] Review limits when your application has: * Large request or response bodies * Long-running dynamic requests * Streaming responses * Large dependency bundles or deployment artifacts * High concurrency or burst traffic * Extensive custom firewall rules * High-volume logs or traces * Large cache entries or frequent invalidation Do not infer a limit from a framework default or another hosting provider. Easel's enforced runtime and delivery limits are the source of truth. # Platform architecture Easel separates application builds from request delivery. A deployment is assembled once, published as immutable output, and then served through shared delivery and runtime systems. Build plane [#build-plane] The build plane turns source code into deployable artifacts. ```text Git commit or CLI upload ↓ Install dependencies ↓ Run framework build ↓ Analyze build output ├─ static assets ├─ server functions ├─ routing metadata ├─ cache metadata └─ image configuration ↓ Publish immutable deployment ``` The exact output depends on the framework. A Vite application may contain only static assets. A Next.js application may also contain server handlers, middleware, cached route output, and image optimization configuration. Delivery plane [#delivery-plane] The delivery plane receives every request to an Easel URL or connected domain. ```text Request ↓ Platform protections ↓ Deployment protection ↓ Project firewall rules ↓ Routing, redirects, and rewrites ↓ CDN cache or static asset ↓ when application execution is required Function routing ↓ Function instance ``` The delivery plane can complete requests without application compute. This reduces latency and avoids unnecessary Function usage. Compute plane [#compute-plane] Dynamic routes run in Easel Functions. A Function instance can process overlapping requests when the runtime and application permit it. Applications must not depend on instance affinity, durable local files, or mutable process-wide state. Use external data stores or Runtime Cache for shared state. Data and cache layers [#data-and-cache-layers] Easel exposes two distinct cache systems: | System | Stores | Accessed from | Typical use | | ------------- | ----------------------- | ---------------- | --------------------------------- | | CDN cache | Complete HTTP responses | Delivery network | Pages, assets, API responses | | Runtime Cache | Application values | Function code | Database results, computed values | A single request can use both. A Function can read data from Runtime Cache and return a response that is then stored in the CDN cache. Control plane [#control-plane] The dashboard, Git integration, CLI, and future public API operate the control plane. They create projects, configure environments, start deployments, assign domains, and expose operational data. Control-plane actions do not change an immutable deployment. Changes to source code, build-time variables, or build settings require a new deployment. Traffic assignment can move between existing compatible Production deployments without rebuilding. Observability plane [#observability-plane] Easel records telemetry across request delivery and Function execution. Request IDs correlate client-visible responses with request details, logs, traces, cache decisions, security events, and Function invocations. Because one Function invocation may process multiple requests, request and invocation records are related but not interchangeable. Availability boundaries [#availability-boundaries] A globally reachable delivery network does not imply that every service executes in every edge location. Static and cached responses may be served close to users while dynamic Functions run in documented compute regions. See [Regions](/docs/reference/regions) and [Limits](/docs/reference/limits) for the current platform boundaries. # Pricing and usage Easel usage is driven by the work required to build, deliver, and execute an application. Included allowances, on-demand ranges, and regional rates live in [Pricing](/docs/pricing) and on the [pricing page](/pricing). This guide explains the usage model. It does not duplicate price numbers that can change. Main usage categories [#main-usage-categories] Builds [#builds] Build usage includes dependency installation, framework compilation, output analysis, and publication. Rebuilding the same source can still consume build resources when configuration or dependencies differ. Bandwidth and delivery [#bandwidth-and-delivery] Delivery usage includes bytes returned through Easel's network. Static assets, cached responses, and Function responses can all contribute to transfer. Function execution [#function-execution] Function usage is based on Function Requests and Function Duration. Function Requests are logical requests into your functions, not cloud-provider invoke counts. Post-response work continues to consume Function Duration until it completes or reaches the applicable limit. In-function concurrency can allow one active instance to serve overlapping requests. Do not assume that request count maps one-to-one to instance count or invocation count. Image optimization [#image-optimization] Image Optimization is metered as Image Transformations on cache MISS and STALE only. See [Image Optimization pricing](/docs/pricing/image-optimization). Observability [#observability] Logs, metrics, traces, retention, and export can have plan-specific allowances or limits. Security [#security] Some security capabilities or higher limits may depend on the workspace plan. What reduces usage [#what-reduces-usage] * Serve immutable assets with long-lived cache headers. * Cache eligible dynamic responses at the CDN. * Use Runtime Cache for repeated application data or computation. * Avoid unnecessary rebuilds in monorepos. * Keep Function work bounded and move durable jobs to a queue or background system. * Control verbose application logging in production. Preview and Production usage [#preview-and-production-usage] Preview deployments use platform resources in the same way as Production deployments. A high-volume Preview or automated branch workflow can therefore contribute meaningful build, bandwidth, Function, and observability usage. Usage attribution [#usage-attribution] Inspect usage in the dashboard by workspace and project. Breakouts by deployment, environment, and resource category appear where the product exposes them. Avoiding unexpected usage [#avoiding-unexpected-usage] 1. Review build triggers and monorepo path filters. 2. Configure cache policies intentionally. 3. Inspect Function duration and request volume. 4. Set retention and export policies appropriate for the application. 5. Review plan limits before a launch or migration. See [Limits](/docs/reference/limits) for technical ceilings and the [pricing page](/pricing) for current commercial terms. # What is Easel? Easel is a cloud platform for building, deploying, delivering, and operating modern web applications. Connect a Git repository or deploy from the CLI. Easel builds the application, publishes static assets to its delivery network, runs server-side code in Functions, protects incoming traffic, and records request-level telemetry. What you can deploy [#what-you-can-deploy] Easel supports static sites, client-rendered applications, and full-stack JavaScript frameworks. Supported framework integrations include Next.js, TanStack Start, Nuxt, React Router, SvelteKit, and Vite. A framework build can produce several kinds of output: * Static HTML, JavaScript, CSS, fonts, and images * Server-rendered routes and API handlers * Middleware and routing metadata * Cacheable pages and application data * Image optimization requests Easel maps each output to the appropriate platform service. The platform in one request [#the-platform-in-one-request] A request to an Easel deployment passes through the delivery network before it reaches application code. ```text Visitor ↓ Easel delivery network ├─ security controls ├─ redirects and rewrites ├─ CDN cache └─ static asset lookup ↓ when dynamic execution is required Easel Function ↓ Database, API, or Runtime Cache ``` A request may complete at the edge without invoking a Function when it is blocked, redirected, served from cache, or matched to a static asset. Deployment workflow [#deployment-workflow] Every successful build creates an immutable deployment with its own URL. Git branches and pull requests can receive stable Preview URLs, while production domains point to the deployment currently assigned to Production. This separation lets teams validate a build before release and roll production traffic back without rebuilding. What Easel manages [#what-easel-manages] Easel manages: * Build execution and framework detection * Deployment artifacts and immutable deployment URLs * Static asset delivery and HTTP caching * Dynamic Functions and runtime resources * Domain routing and managed TLS * Firewall controls and deployment protection * Logs, metrics, traces, and request diagnostics Your application remains responsible for its own business logic, authentication and authorization, data model, external services, and secure handling of credentials. Where to go next [#where-to-go-next] * Follow [Getting started](/docs/getting-started) to deploy an application. * Read [Platform architecture](/docs/overview/platform-architecture) to understand the request and deployment model. * Review [Frameworks](/docs/framework-guides) for integration-specific behavior. * See [Core concepts](/docs/overview/core-concepts) for the terminology used throughout the documentation. # Configuration reference Use this page as an index of configuration surfaces. Detailed field behavior lives on the linked product pages. Project and build [#project-and-build] * [Build configuration](/docs/deployments/build-configuration) * [Monorepos](/docs/deployments/monorepos) * [Git deployments](/docs/deployments/git) * [Creating deployments](/docs/deployments/create) Environments and variables [#environments-and-variables] * [Environments](/docs/deployments/environments) * [Environment variables](/docs/runtime/environment-variables) * [Secrets](/docs/security/secrets) Functions and runtime [#functions-and-runtime] * [Function configuration](/docs/functions/configuration) * [Function runtime](/docs/functions/runtime) * [Function regions](/docs/functions/regions) * [Background work](/docs/runtime/background-work) * [Runtime Cache](/docs/runtime-cache) Delivery [#delivery] * [Routing](/docs/delivery/routing) * [Redirects and rewrites](/docs/delivery/redirects-and-rewrites) * [Caching](/docs/cdn/caching) * [Headers](/docs/cdn/headers) * [Compression](/docs/cdn/compression) Domains and TLS [#domains-and-tls] * [Domains](/docs/domains) * [Domains and TLS](/docs/delivery/domains-and-tls) * [Domains reference](/docs/domains/reference) Security [#security] * [WAF rules](/docs/security/waf-rules) * [Access protection](/docs/security/deployment-protection) * [Security evaluation order](/docs/security/request-chain) CLI [#cli] * [CLI overview](/docs/cli) * [Deploy from the CLI](/docs/cli/deploy) # Error codes Easel failures surface through dashboard status, CLI output, HTTP responses, and logs. Prefer the request ID, deployment ID, and product-specific troubleshooting guides over inventing undocumented error symbols. What to capture [#what-to-capture] When a build, deployment, or request fails, record: | Field | Why it matters | | ------------- | -------------------------------------------- | | Surface | Dashboard, CLI, build, request, or webhook | | Message | Human-readable failure text | | Request ID | Correlates delivery and Function telemetry | | Deployment ID | Identifies the immutable artifact | | Timestamp | Narrows log and metric windows | | Retryable | Whether retrying without changes may succeed | Categories and where to look [#categories-and-where-to-look] Build errors [#build-errors] Dependency installation, framework detection, command failure, output validation, and timeouts. See [Deployment troubleshooting](/docs/deployments/troubleshooting) and [Build configuration](/docs/deployments/build-configuration). Deployment errors [#deployment-errors] Publication, artifact upload, domain assignment, cancellation, and invalid state transitions. See [Managing deployments](/docs/deployments/manage) and [Promoting deployments](/docs/deployments/promote). Request and routing errors [#request-and-routing-errors] Invalid host, redirect loops, route not found, malformed headers, and body limits. See [Routing](/docs/delivery/routing), [CDN troubleshooting](/docs/cdn/troubleshooting), and [Debugging](/docs/guides/debugging). Function errors [#function-errors] Initialization, runtime crash, timeout, out-of-memory, and invalid responses. See [Function observability](/docs/observability/functions) and [Function limits](/docs/functions/limits). Domain and TLS errors [#domain-and-tls-errors] Invalid DNS, ownership conflicts, certificate issuance, and renewal. See [Domain troubleshooting](/docs/domains/troubleshooting) and [HTTPS and TLS](/docs/cdn/https-and-tls). Security errors [#security-errors] Blocked, challenged, or protected-deployment responses. See [Security troubleshooting](/docs/security/troubleshooting) and [Firewall observability](/docs/security/firewall-observability). API and CLI errors [#api-and-cli-errors] Authentication, authorization, validation, and service availability. See [CLI authentication](/docs/cli/authentication) and [API status](/docs/api). Request IDs are not error codes [#request-ids-are-not-error-codes] A request ID identifies one request and helps correlate telemetry. It does not describe the failure category. Include both the message and the request ID when contacting support. # Limits reference This page is the entry point for enforced Easel limits. Product pages keep the detailed tables; use the links below for exact current values. Builds and deployments [#builds-and-deployments] See [Build configuration](/docs/deployments/build-configuration) and [Deployment troubleshooting](/docs/deployments/troubleshooting) for build and artifact constraints that appear during deploy. Delivery [#delivery] See [CDN](/docs/cdn), [Caching](/docs/cdn/caching), and [Headers](/docs/cdn/headers) for request, response, cache, and header limits that apply at the delivery layer. Functions [#functions] See [Function limits](/docs/functions/limits) for duration, memory, CPU, request body size, deployment bundle size, environment variable size, and related execution ceilings. Runtime Cache [#runtime-cache] See [Runtime Cache](/docs/runtime-cache) for key, value, TTL, and invalidation constraints. Domains and TLS [#domains-and-tls] See [Domains reference](/docs/domains/reference) and [HTTPS and TLS](/docs/cdn/https-and-tls) for hostname, DNS, and certificate constraints. Security [#security] See [Security limits](/docs/security/limits) for firewall rule counts, conditions, IP rules, and related ceilings. Observability [#observability] See [Observability retention](/docs/observability/limits) for dashboard time windows, log capture bounds, and query limits. Raising a limit [#raising-a-limit] Some limits are fixed platform constraints. Others may be plan-dependent or adjustable by agreement. Contact [hello@easel.sh](mailto:hello@easel.sh) when a production workload needs a higher ceiling, and include the limit name and observed failure mode. # Regions Easel distinguishes edge delivery locations from Function compute regions. Edge delivery [#edge-delivery] Requests enter Easel through its delivery network. Static assets and cacheable responses may be served from an edge location without invoking application compute. Do not interpret the number of edge locations as the number of places where Functions run. Function compute [#function-compute] Dynamic routes run in Function compute regions supported by the platform. | Region ID | Location | Available for Functions | Selection | | --------- | -------------------------------- | ----------------------- | ---------------------------------------------------- | | `iad` | US East (Northern Virginia, USA) | Yes | Platform default; currently the only Function region | Additional Function regions are not available yet. Region placement is fixed by the platform today. Do not rely on route-level or project-level region configuration examples until the product exposes them. The delivery layer may attach the Function region on the request your application receives: ```http X-Easel-Function-Region: iad ``` See [Function regions](/docs/functions/regions) and [Request and response headers](/docs/cdn/headers). Data locality [#data-locality] Place latency-sensitive databases and APIs near the Function compute region. Edge delivery cannot remove the network trip between a Function and a distant data service. Observability [#observability] Request and invocation records can expose edge and compute context separately where available. See [Function observability](/docs/observability/functions). # Build pricing **Build Execution** is vCPU-minutes while the builder sandbox is ready, until it is deleted, multiplied by the configured vCPU count. Queue time is not billed. Baker and probe sandboxes are not billed. Hobby and Pro include **100 Standard build minutes** per month. Standard is the 2 vCPU build machine. Enhanced (4 vCPU) consumes the grant twice as fast. After the grant, Hobby builds stop until you upgrade to Pro. Pro continues from usage credits, then on-demand rates. Included allowances and on-demand ranges are in [What each meter counts](/docs/pricing/usage). Easel checks remaining build allowance before starting a build. Exhausted Hobby allowance requires an upgrade. Exhausted Pro credit without a top-up can block new builds. See [Build configuration](/docs/deployments/build-configuration). # Cache pricing ISR and Runtime Cache share **Cache Read Units** and **Cache Write Units**. A unit is 8 KB: `ceil(bytes / 8192)`. A 0-byte miss is 0 units. Regional multipliers apply after the 8 KB conversion. Remaining cache units in the dashboard are base-region-equivalent. Included allowances and on-demand ranges are in [What each meter counts](/docs/pricing/usage). Image Optimization cache hits are not cache-write units and are not Image Transformations. See [Image Optimization pricing](/docs/pricing/image-optimization) and [Runtime Cache](/docs/runtime-cache). # Usage credit and top-up Pro includes $20 of usage credit each month (2,000 credits, where 1 credit is $0.01). Direct included allowances on a meter are consumed first. Further usage draws this credit at the meter’s base (iad) on-demand rate, because tracked quantities are already region-normalized. Hobby has no usage credit. Work beyond Hobby included limits requires Pro. Top-up [#top-up] You can purchase a **$10** top-up for **1,000 credits**. Purchased credits persist independently of the monthly Pro grant. On the billing page you can enable **auto top-up** for usage credit. You must set a monthly purchase limit. A failed charge does not invent balance. Unused monthly credit does not roll over. See [How Easel pricing works](/docs/pricing) and the [pricing page](/pricing). # Data transfer pricing Delivery has two transfer meters. **Edge Data Transfer** counts bytes from the edge to the client. Cached, static, and function responses all contribute when bytes leave the edge. **Origin Transfer** counts bytes on the origin-to-edge fetch (`originBytesIn` only). It does not add request `originBytesOut`. The region is the function’s AWS region when the origin is a function, otherwise the edge region. Included allowances and on-demand ranges are in [What each meter counts](/docs/pricing/usage). Both meters use regional multipliers. See [regional rates](/docs/pricing/regions). # Function pricing Functions have two meters: **Function Requests** and **Function Duration**. Function Requests count logical application requests into your functions (pages, route handlers, server actions, and similar). They are not AWS Lambda invocation counts. Overlapping in-function concurrency can serve more than one request on one billed execution. Function Duration is GB-hours from billed duration and configured memory on the function report, recorded once per underlying execution id. Wall-clock time on each HTTP response is not a separate duration charge. Hobby includes **100 GB-hours** of Function Duration and **100,000** Function Requests per month. Pro includes **1,000 GB-hours** and **1,000,000** Function Requests. On-demand Function Duration is **$0.18 / GB-hour** (flat across regions at launch). Full included tables and on-demand ranges are in [What each meter counts](/docs/pricing/usage). What is not billed here [#what-is-not-billed-here] * Queue time before a build or a cold start is not Function Duration. * Static and cached responses that never reach a function do not create Function Requests. See [Functions](/docs/functions) for runtime behavior and [regional rates](/docs/pricing/regions) for duration multipliers. # Image Optimization pricing Image Optimization is billed per **Image Transformation**, in thousands, in the **edge delivery region**. A transformation is billed on cache **MISS** and **STALE** only. A cache **HIT** is not an Image Transformation. Easel does not bill separate Image Cache Read or Image Cache Write meters. Hobby includes 5,000 transformations per month. Pro has no included grant: transformations draw from usage credit at the on-demand rate. Base rate is $0.05 per 1,000 transformations in `iad`. Other regions use the multipliers below (regional USD ÷ 0.05). Published marketing range: $0.05–$0.0812 per 1,000. See [Image Optimization](/docs/runtime/image-optimization) for how transforms are requested. # How Easel pricing works Hobby is free within published included allowances. Pro is $20 per month and includes $20 of metered usage credit. On-demand rates apply after included allowances and Pro credit are consumed. Numbers on this page come from the same catalog as the [pricing page](/pricing). Remaining usage is measured in **base-region-equivalent** units: regional multipliers are applied before usage is counted against an allowance. Plans [#plans] * **Hobby**: included allowances only. No usage credit pool. Stays free inside those limits. * **Pro**: $20 platform fee, $20 included usage credit each month, higher included allowances on several meters. * **Enterprise**: custom contracts for SSO, multi-region failover, and platform SLAs. Contact us. Unused included usage and unused Pro credit do not roll over. What you are billed for [#what-you-are-billed-for] Easel meters Edge Requests, Edge Data Transfer, Origin Transfer, Function Requests, Function Duration, cache units, Image Transformations, Build Execution, Rate Limiting, and WAF inspection. Function Requests are application-level requests into your functions, not cloud-provider invoke counts. Observability export meters are declared for a later release and are not billed today. Next steps [#next-steps] * [What each meter counts](/docs/pricing/usage) * [Regional rates](/docs/pricing/regions) * [Usage credit and top-up](/docs/pricing/credits) # Regional rates On-demand prices differ by region. Easel converts usage into **base-region-equivalent** units with a regional multiplier, then counts that quantity against your allowance and credit. A 1 GB transfer in São Paulo (`gru`) with a 1.7× egress multiplier counts as 1.7 GB against Edge Data Transfer remaining. The billing UI remaining figure is that normalized quantity, not “GB remaining in every region.” Unknown regions are not billed and are not mapped to `iad`. Published retail by region [#published-retail-by-region] Rates below are USD at the meter’s billing increment (per GB, per 1M, per 1K, or per GB-hour). Function Requests and Build Execution are not region-multiplied. Image transformation multipliers follow the Image Optimization regional table on [Image Optimization pricing](/docs/pricing/image-optimization). # Security pricing Security meters apply only when a request enters the matching path. **Rate Limiting** counts requests that were subject to a rate-limit rule and allowed. Requests that never entered a rate-limit path are not counted. Requests that were limited (HTTP 429) are not counted as allowed. **WAF Inspected Requests** counts requests on the WAF inspect path. **WAF Inspected Payload** counts the inspected request-body prefix only, not response bodies. Hobby and Pro have no included WAF grant at launch. Rate Limiting has an included monthly allowance. Included allowances and on-demand ranges are in [What each meter counts](/docs/pricing/usage). See [Firewall](/docs/security/firewall) and [WAF rules](/docs/security/waf-rules). # What each meter counts Each meter counts one kind of work. Dashboard remaining values are **base-region-equivalent**: a request or gigabyte in a higher-priced region consumes more of the same allowance than the same work in `iad`. Edge and delivery [#edge-and-delivery] * **Edge Requests**: one count per completed response at the edge. * **Edge Data Transfer**: bytes sent from the edge to the client, billed per GB. * **Origin Transfer**: bytes fetched from origin into the edge (`originBytesIn` only). Response bytes from origin to the visitor are Edge Data Transfer, not Origin Transfer. Functions [#functions] * **Function Requests**: one count per logical function request. This is not an AWS Lambda invoke count. Concurrent or fluid execution can serve overlapping work without a 1:1 map to cloud invokes. * **Function Duration**: GB-hours from the billed duration and configured memory on the function report, once per underlying execution id. See [Function pricing](/docs/pricing/functions). Cache and images [#cache-and-images] * **Cache Read Units** and **Cache Write Units**: ISR and Runtime Cache share these meters. Units are `ceil(bytes / 8192)`. Zero-byte misses are 0 units. * **Image Transformations**: one transformation on cache **MISS** or **STALE**. Cache **HIT** is not billed. See [Image Optimization pricing](/docs/pricing/image-optimization). Builds and security [#builds-and-security] * **Build Execution**: builder sandbox ready-to-delete time × configured vCPUs. Queue time and probe/baker sandboxes are excluded. Hobby and Pro include 100 Standard (2 vCPU) build minutes per month. Hobby stops at that grant. Pro continues from usage credits. * **Rate Limiting**: requests that entered a rate-limit path and were allowed. Requests that never hit a rate-limit rule are not counted. Blocked (429) requests are not counted as allowed. * **WAF Inspected Requests** and **WAF Inspected Payload**: requests and inspected request-body prefix on the WAF inspect path. Response bodies are not inspected for this meter. # Background work Some application work does not need to block the HTTP response. Easel supports framework APIs that register post-response work within the active Function lifecycle. Post-response work is not a durable job queue. Post-response work [#post-response-work] A supported framework API can return the response and continue work before the invocation ends. For example, Next.js provides `after()`: ```ts import { after } from "next/server"; export async function POST(request: Request) { const event = await request.json(); after(async () => { await recordAnalyticsEvent(event); }); return Response.json({ accepted: true }); } ``` The client can receive the response before the callback completes, but the Function remains active and continues consuming duration. Appropriate uses [#appropriate-uses] Post-response work is appropriate for short, best-effort tasks such as: * Recording non-critical analytics * Updating a secondary cache * Sending a lightweight notification * Completing a bounded cleanup step Use a durable job system when work [#use-a-durable-job-system-when-work] * Must survive Function termination * Requires automatic retries * May exceed the Function duration limit * Must run independently of the originating request * Requires scheduling, fan-out, or concurrency control * Must guarantee eventual completion A durable pattern is: ```text HTTP request ↓ Validate and persist job ↓ Return 202 Accepted ↓ Worker processes job with retries ``` Failure behavior [#failure-behavior] A successful HTTP response does not prove that post-response work completed. Log failures and make non-idempotent operations safe to retry when possible. Limits [#limits] Post-response work shares the Function's duration, memory, CPU, networking, and shutdown boundaries. See [Function limits](/docs/functions/limits). # Environment variables Environment variables provide configuration to builds and Functions without committing values to source control. Environment scopes [#environment-scopes] Easel separates Preview and Production values. A deployment receives values from the environment used to build it. | Scope | Used by | | ---------- | --------------------------------------------------- | | Preview | Branch, pull-request, and other Preview deployments | | Production | Production deployments | Changing a variable does not mutate an existing immutable deployment. Create a new deployment for the change to take effect. Build-time variables [#build-time-variables] Build tools and frameworks can read variables while compiling the application. ```js const apiOrigin = process.env.API_ORIGIN; ``` Public prefixes such as `NEXT_PUBLIC_`, `VITE_`, and framework equivalents can embed values into browser JavaScript. Never place secrets in public variables. Runtime variables [#runtime-variables] Server-side routes and Functions can read environment variables at runtime through the framework's normal API. ```js const databaseUrl = process.env.DATABASE_URL; ``` Treat all secret values as sensitive even when they are only available server-side. Variable names and values [#variable-names-and-values] Use descriptive uppercase names. Avoid storing large files or structured credentials when a dedicated secret mechanism is required. Combined environment variable names and values are subject to the Function environment size limit. See [Function limits](/docs/functions/limits). Local development [#local-development] Keep local values in the framework's supported local environment files and exclude secret-bearing files from Git. Easel does not automatically synchronize local files with project settings unless a documented CLI command provides that behavior. Rotation [#rotation] To rotate a credential: 1. Create the replacement credential at the provider. 2. Update Preview and validate a new Preview deployment. 3. Update Production and create a Production deployment. 4. Promote the new deployment. 5. Revoke the previous credential after rollback risk has passed. Troubleshooting [#troubleshooting] When a value is missing or stale, verify: * The correct Easel project * Preview versus Production scope * Exact variable name and capitalization * Whether the variable is required during build or runtime * Whether a new deployment was created after the change * Whether the framework embeds public variables at build time # Image optimization Easel image optimization transforms source images into appropriately sized and encoded responses for supported framework integrations. How it works [#how-it-works] A framework image component generates an optimization request containing the source image and requested dimensions. Easel validates the request, retrieves or reads the source, performs the supported transformation, and caches the result. ```text Browser image request ↓ Validate source and dimensions ↓ Optimization cache lookup ↓ miss Fetch or read source image ↓ Resize and encode ↓ Cache and return response ``` Framework support [#framework-support] Use the framework-native image component when the matching Easel framework guide marks image optimization as supported. Static applications can serve pre-generated responsive images directly from deployment assets. Remote images [#remote-images] Remote image sources must match the framework and platform allowlist configuration. Restrict patterns to trusted hosts and paths. Broad remote-image patterns can allow unintended fetches and increase resource usage. Cache behavior [#cache-behavior] Transformed variants are cached independently based on source, dimensions, format, and other supported transformation parameters. Updating a remote image at the same URL may not immediately invalidate previously cached variants. Performance guidance [#performance-guidance] * Provide explicit width and height to reduce layout shift. * Use responsive `sizes` values so browsers do not request unnecessarily large images. * Prefer modern source formats where practical. * Avoid generating excessive near-duplicate dimensions. * Keep remote origins reliable and cacheable. Security [#security] Image optimization fetches source images server-side. Restrict remote patterns to trusted hosts and paths. Prefer framework-native configuration documented in the matching [framework guide](/docs/framework-guides). Troubleshooting [#troubleshooting] Check the framework guide, remote-source allowlist, requested dimensions, source response headers, and related limits when an image returns an error or bypasses optimization. # Attack Mode Attack Mode protects a project during active abuse by requiring browser clients to complete a verification step before accessing application content. In the dashboard this control appears as **Attack challenge mode**. Use Attack Mode as a temporary defensive control during: * Sudden request floods * Automated scraping * Credential-stuffing attempts * Broad malicious scanning * Attacks that are difficult to isolate with a narrow rule Attack Mode is not a replacement for application authentication, authorization, or rate limiting. How Attack Mode works [#how-attack-mode-works] When Attack Mode applies to a request: 1. Easel evaluates whether the request is eligible for browser verification. 2. Unverified clients receive a challenge page with HTTP status **429**. 3. The browser completes a proof-of-work verification. 4. The browser posts the result to `/.well-known/easel-challenge/verify`. 5. On success, Easel sets a short-lived session cookie (`easel_ch_pass`, about one hour, bound to hostname and deployment). 6. The client continues to the application without repeating the challenge until the session expires. Challenge responses use `Cache-Control: no-store` so intermediaries do not cache the interstitial. Enable Attack Mode [#enable-attack-mode] 1. Open the project. 2. Open **Project settings**. 3. Find **Attack challenge mode**. 4. Review the impact on APIs, bots, and non-browser clients. 5. Optionally enable **Allow verified bots**. 6. Turn Attack Mode on and save. 7. Monitor firewall events and application health. Scope [#scope] Attack Mode is project-wide. When enabled, it applies to hostnames that serve the project, including production and preview URLs, unless a request is exempt. Exemptions include: * A valid Attack Mode session cookie * A matching **Bypass attack challenge** custom rule * Verified bots when **Allow verified bots** is enabled * Trusted internal edge fetches * BotID opaque verification paths used by the BotID SDK Custom **Challenge** rules still apply before Attack Mode. See [Security evaluation order](/docs/security/request-chain). Browser verification [#browser-verification] Verification requires a browser that can run the challenge page and store the session cookie. It does not prove that a visitor is human; it only shows that the client completed the configured verification flow. Verification is bound to the hostname and deployment identity. Changing host, deployment, or clearing cookies can require a new solve. APIs and non-browser clients [#apis-and-non-browser-clients] Attack Mode can disrupt: * API clients * Webhooks * Mobile applications * Command-line clients * Monitoring systems * Automated integrations Before enabling it, create narrow **Bypass attack challenge** rules for trusted machine traffic when needed. Recommended signals include: * Trusted IP ranges * Dedicated webhook paths * Known client headers that your edge can trust Continue validating application-level signatures in your code. Firewall rules cannot replace webhook signature checks. Bots [#bots] Verified bots may bypass Attack Mode when **Allow verified bots** is enabled. See [Bot traffic](/docs/security/bots). A bot should not be trusted solely because its user agent resembles a known crawler. Disable Attack Mode [#disable-attack-mode] Disable Attack Mode after the attack subsides and narrower protections are in place. Before disabling it: 1. Review matched traffic. 2. Identify stable malicious patterns. 3. Convert appropriate patterns into custom rules. 4. Confirm application capacity is healthy. 5. Continue monitoring after deactivation. Related guides [#related-guides] * [Custom rules](/docs/security/custom-rules) * [Bot traffic](/docs/security/bots) * [Firewall observability](/docs/security/firewall-observability) * [Security troubleshooting](/docs/security/troubleshooting) # Bot traffic Automated traffic includes legitimate crawlers, monitoring systems, integrations, scrapers, scanners, and abusive bots. A user-agent string alone cannot reliably prove a bot’s identity. Verified bots [#verified-bots] A verified bot is an automated client whose identity has been validated using stronger evidence than a user-agent claim. Easel may verify bots using: * Published IP ranges * Forward and reverse DNS * Cryptographic request signatures (Web Bot Auth) Examples include major search crawlers and selected service bots. The verified set can change as providers update their infrastructure. Attack Mode bypass [#attack-mode-bypass] When Attack Mode is on and **Allow verified bots** is enabled, verified bots may skip the browser challenge so search indexing and approved integrations can continue. Verified-bot bypass applies to Attack Mode only. It does not: * Skip custom **Deny**, **Redirect**, or **Challenge** rules * Skip [deployment protection](/docs/security/deployment-protection) * Skip [platform protections](/docs/security/platform-protections) You can still block a verified bot with an explicit custom rule. Arbitrary clients cannot claim verified status by forging a user agent. Customer controls [#customer-controls] Use custom firewall rules to: * Block a specific path for unwanted crawlers * Challenge unverified automation * Bypass Attack Mode for monitoring IPs * Deny known abusive sources Prefer IP, ASN, path, and verified-bot behavior over fragile user-agent matching when stronger signals are available. AI crawlers [#ai-crawlers] Some AI crawlers appear in the verified-bot registry when their identity can be validated. Others are unverified. Blocking by user agent alone is easy to evade. Combine path restrictions, authentication, and Attack Mode or custom challenges when you need stronger controls. Application visibility [#application-visibility] Bot classification is used during edge security evaluation and may appear in security observability. Do not rely on a client-supplied bot identity header as proof of verification. For application-level bot detection in frameworks that support it, use BotID in your application code. BotID opaque verification paths are excluded from the Attack Mode interstitial so the SDK can complete its flow. Related guides [#related-guides] * [Attack Mode](/docs/security/attack-mode) * [Custom rules](/docs/security/custom-rules) * [Firewall observability](/docs/security/firewall-observability) # Custom firewall rules Custom rules let you define project-specific security policies without modifying application code. Rules are project-scoped: they apply to every hostname that serves that project, including preview URLs, production URLs, and custom domains. Create a rule [#create-a-rule] 1. Open the project. 2. Open **Project settings**. 3. Scroll to **Custom WAF rules**. 4. Choose **Add rule**. 5. Enter a descriptive name. 6. Add one or more conditions. 7. Select an action. 8. Review the rule order. 9. Toggle **Active** and save. 10. Confirm matches in firewall observability. Clearing all rules and saving stores an empty ruleset. How matching works [#how-matching-works] * **Rule order matters.** Easel evaluates active rules from top to bottom. The first matching rule applies; lower rules are not considered for that request. * **Condition groups (OR).** A rule can have multiple groups. If any group matches, the rule matches. * **Conditions inside a group (AND).** Every condition in a group must be true for that group to match. * **Negate.** Inverts a single condition after the operator runs. Prefer explicit operators such as **Does not equal** or **Is not any of** for new rules. Conditions [#conditions] Each condition has a type, an operator, and either a single value or a values list (one per line) for **Is any of** / **Is not any of**. Some types use a key (header name, query parameter name, or cookie name). Types [#types] | Type | What is matched | | ----------------------------- | -------------------------------------------------------------------------------- | | **Request path (normalized)** | URL path with consistent trailing-slash handling. | | **Raw path** | Path as received, without that normalization step. | | **Route pattern** | The route pattern from your deployment that matched this request. | | **Method** | HTTP method, compared in uppercase (`GET`, `POST`, …). | | **User-Agent** | The `User-Agent` header. | | **Request header** | A header by key. Legacy `Name:expected` in value is supported when key is empty. | | **Query string (full)** | Everything after `?` in the URL. | | **Query parameter** | A single query parameter; set key to the parameter name. | | **Cookie** | A named cookie (key), or the raw `Cookie` header when key is empty. | | **Hostname** | Host without port. | | **Environment** | Deployment environment (`production` or `preview`). | | **IP / CIDR** | Client IP after trusted proxy handling. See [IP rules](/docs/security/ip-rules). | | **Edge region** | The edge region that handled the request. | | **Geo continent / country** | Continent or country derived from the client IP. | | **AS number** | Autonomous system number (ASN) derived from the client IP. | | **Server action (Next.js)** | Matches `Next-Action` (falls back to `Next-Router-State-Tree` if empty). | Operators [#operators] | Label | Operand | | ------------------------------------------------------ | --------------------- | | **Equals** / **Does not equal** | Single value | | **Is any of** / **Is not any of** | Values (one per line) | | **Starts with** / **Does not start with** | Single value | | **Ends with** / **Does not end with** | Single value | | **Contains** / **Does not contain** | Single value | | **Matches expression** / **Does not match expression** | Regular expression | Validate regex rules with **Log** first. Patterns that work in JavaScript may still fail to compile on the edge. Geo and ASN [#geo-and-asn] Geo country, geo continent, and ASN conditions use IP address data powered by [IPLocate.io](https://www.iplocate.io) (CC BY-SA 4.0). Comparisons for country and continent are case-normalized to uppercase. Trusted client IP [#trusted-client-ip] For IP, geo, and ASN conditions, the edge resolves the client IP from the connection that reaches Easel. Easel honors `X-Forwarded-For` only on its trusted edge path. Client-supplied forwarding headers from untrusted peers are ignored. Actions [#actions] | Action | Effect | | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | **Log** | Records a firewall observation and continues to your app. | | **Deny (403)** | Responds with 403 Forbidden. The request does not reach your deployment. | | **Challenge** | Serves a browser proof-of-work checkpoint (same family of flow as Attack Mode). After success, the edge sets a session cookie (about one hour). | | **Bypass attack challenge** | Skips project-wide Attack Mode only. Other platform protections still apply. | | **Redirect** | Responds with an HTTP redirect. Set a 3xx status and Location (absolute `https://…` URL recommended). | **Challenge** rules use the same verification endpoint as Attack Mode. The edge always allows `/.well-known/easel-challenge/*` so verification can complete without looping. Recommended rollout [#recommended-rollout] 1. Create a rule with **Log** and confirm traffic in observability. 2. Refine conditions until only the traffic you care about matches. 3. Change the action to **Deny**, **Challenge**, **Bypass**, or **Redirect**. Observe first, then enforce. Current limitations [#current-limitations] * No per-rule rate limiting in the dashboard yet. * No time-based IP ban from rule settings. * Rules are ordered; there is no separate priority number beyond list order. * TLS fingerprint matching is not available yet. Examples [#examples] Block a sensitive path by country [#block-a-sensitive-path-by-country] ```text Path starts with /admin Country is not US Action: Deny ``` Bypass Attack Mode for a webhook path [#bypass-attack-mode-for-a-webhook-path] ```text Path starts with /api/webhooks/ Action: Bypass attack challenge ``` Continue validating webhook signatures in application code. Challenge suspicious login traffic [#challenge-suspicious-login-traffic] ```text Path equals /login Method equals POST User agent contains HeadlessChrome Action: Challenge ``` Observe an API client before enforcing [#observe-an-api-client-before-enforcing] ```text Path starts with /api Header x-client-version does not exist Action: Log ``` Related guides [#related-guides] * [Firewall](/docs/security/firewall) * [Attack Mode](/docs/security/attack-mode) * [IP rules](/docs/security/ip-rules) * [Firewall observability](/docs/security/firewall-observability) * [Security troubleshooting](/docs/security/troubleshooting) # Deployment protection Access protection controls who can open a deployment before application code runs. Easel currently provides this through **Deployment Protection** policies. Deployment protection controls who can access a deployment after the request passes traffic-level security checks. It is designed for: * Preview deployments * Branch deployments * Pull request previews * Unique deployment URLs * Staged Production deployments that are not Current Protection method [#protection-method] Easel provides **workspace authentication** (SSO through the Easel app host). When deployment protection is enabled, visitors must sign in with a workspace account before they can view protected deployment URLs. After a successful sign-in, the edge sets a session cookie (`easel_dp`, about one hour). Where to configure [#where-to-configure] 1. Open the project. 2. Open **Project settings**. 3. Find **Deployment protection**. 4. Enable **Require workspace sign-in**. 5. Save. Allow a short CDN cache delay after toggling. Security layers [#security-layers] Deployment protection is separate from the firewall. A request may: 1. Pass platform protections. 2. Pass custom firewall rules and Attack Mode. 3. Still be denied because the requester is not authorized to view the deployment. Custom firewall actions do not bypass deployment authentication. What is protected [#what-is-protected] When enabled, visitors need workspace sign-in for: * Preview hostnames on `preview.easelusercontent.com` (commit and branch URLs) * Any deployment that is not the current production release The following stay public: * The canonical production subdomain for the current production deployment * LIVE custom domains on the current production deployment Preview deployments [#preview-deployments] By default, preview hostnames are publicly reachable unless deployment protection is enabled. Preview responses also send `X-Robots-Tag: noindex, nofollow, noarchive` so crawlers skip indexing. `noindex` is not an access control. See [Preview deployments](/docs/deployments/previews). Forks and untrusted code [#forks-and-untrusted-code] Treat Preview configuration as untrusted-input space: * Keep Production secrets out of Preview variables * Enable deployment protection when previews must not be public See [Secrets](/docs/security/secrets) and [Deployment environments](/docs/deployments/environments). Related guides [#related-guides] * [Preview deployments](/docs/deployments/previews) * [Security evaluation order](/docs/security/request-chain) * [Workspace security](/docs/security/workspace-security) # Firewall observability Firewall observability connects security decisions to request activity. Use it to: * Debug a custom rule * Investigate an attack * Confirm Attack Mode behavior * Identify false positives * Determine whether application code ran * Measure the impact of a policy change Firewall events [#firewall-events] Firewall and edge request views can include: * Timestamp * Request ID * Project and deployment * Domain * Client IP, subject to permissions and privacy controls * Method and path * Action * Rule ID * Country and ASN, when available * Response status Actions [#actions] Customer-facing action values you may see include: ```text deny challenge challenge_pass challenge_fail redirect log bypass ``` Platform protections may also surface actions such as `ban` or `throttle` on blocked or rate-limited traffic. Use stable machine-readable values when filtering, even when dashboard labels change. Decision sources [#decision-sources] When describing where a decision came from, prefer product concepts: * Platform protection * Custom rule * Attack Mode * Deployment protection Do not rely on internal vendor names as part of the public event schema. Response metadata [#response-metadata] For custom-rule denials and some redirect failures, responses may include: ```text X-Easel-Firewall-Action X-Easel-Firewall-Rule-Id ``` Challenge responses may also include: ```text X-Easel-Mitigated: challenge X-Easel-Challenge-Token ``` Treat these as operational debugging signals. Incoming client copies of security headers are not a trust boundary. Rule testing [#rule-testing] A safe workflow is: 1. Create a rule with the **Log** action. 2. Filter events by the rule ID or path. 3. Review expected and unexpected matches. 4. Refine the rule. 5. Change the action to **Deny**, **Challenge**, **Bypass**, or **Redirect**. 6. Continue monitoring after enforcement. Challenge pass and fail counts reflect verification outcomes at `/.well-known/easel-challenge/verify`, not every page view. Privacy [#privacy] Client IP and geographic information may be sensitive. Limit access to workspace members who need it for operations, and avoid exporting raw client identifiers into systems that do not require them. Related guides [#related-guides] * [Custom rules](/docs/security/custom-rules) * [Attack Mode](/docs/security/attack-mode) * [Security troubleshooting](/docs/security/troubleshooting) * [Observability](/docs/observability) * [Security events](/docs/observability/security) # Firewall The Easel Firewall evaluates incoming requests before they reach your application. Use it to block known abusive traffic, protect sensitive routes, challenge suspicious browsers, or observe traffic patterns without changing application code. Firewall components [#firewall-components] The project firewall includes: * [Custom rules](/docs/security/custom-rules) * [IP conditions](/docs/security/ip-rules) inside custom rules * [Attack Mode](/docs/security/attack-mode) * [Bot classification](/docs/security/bots) used by Attack Mode exemptions Platform protections remain active independently of the project firewall. Where to configure [#where-to-configure] 1. Open the project. 2. Open **Project settings**. 3. Configure **Custom WAF rules** and **Attack challenge mode**. Rule changes save on the project. The edge picks them up after a short delay. You do not need to redeploy to update rules. Rule anatomy [#rule-anatomy] A firewall rule has: * A name * One or more condition groups * An action * An enabled (active) state * A position in the ordered list Example: ```text Name: Block admin access outside the office Conditions: Path starts with /admin AND IP is not any of the office addresses Action: Deny ``` Conditions [#conditions] Supported request fields include: | Field | Example | | ------------------------ | ------------------------- | | Hostname | `api.example.com` | | Path (normalized or raw) | `/admin` | | Method | `POST` | | IP address | `203.0.113.10` | | Country | `US` | | ASN | `13335` | | Header | `x-api-client` | | User agent | `curl/8.0` | | Query parameter | `preview=true` | | Environment | `production` or `preview` | See [Custom rules](/docs/security/custom-rules) for the full condition and operator reference. Actions [#actions] | Action | Effect | | --------------------------- | ------------------------------------------------------------------------ | | **Log** | Records a firewall observation and continues. | | **Deny** | Responds with 403 Forbidden. The request does not reach your deployment. | | **Challenge** | Serves a browser proof-of-work checkpoint. | | **Bypass attack challenge** | Skips project-wide Attack Mode for matching requests. | | **Redirect** | Responds with an HTTP redirect to a Location you configure. | Default behavior [#default-behavior] When no project rule matches, the request continues unless another security layer blocks or challenges it. Firewall and application authorization [#firewall-and-application-authorization] Firewall rules do not replace application authorization. For example, an IP condition can reduce exposure of `/admin`, but the application must still require authenticated and authorized users. Related guides [#related-guides] * [Custom rules](/docs/security/custom-rules) * [IP rules](/docs/security/ip-rules) * [Attack Mode](/docs/security/attack-mode) * [Security evaluation order](/docs/security/request-chain) * [Firewall observability](/docs/security/firewall-observability) # Security Easel protects every deployment with always-on platform protections and gives you additional controls for managing application traffic and access. Security on Easel has four layers: * **Platform protections** identify and mitigate malicious or abusive traffic automatically. * **Firewall controls** let you log, block, challenge, redirect, or bypass Attack Mode for requests using project-specific rules. * **Access protection** (Deployment Protection) controls who may access previews and other non-public deployments. * **Workspace security** controls access to projects, settings, secrets, and production operations. These layers complement application-level security. Your application remains responsible for authentication, authorization, input validation, session management, and secure use of external services. How requests are protected [#how-requests-are-protected] A request may pass through several security layers before reaching your application: ```text Internet ↓ Platform protections ↓ Deployment protection ↓ Custom firewall rules ↓ Attack Mode ↓ CDN, routing, and cache ↓ Static assets or functions ``` The exact evaluation order matters because an earlier decision may prevent later layers or application code from running. See [Security evaluation order](/docs/security/request-chain). Platform protections [#platform-protections] Platform protections are operated by Easel and apply automatically. They are designed to detect and mitigate: * Network and application-layer attacks * Request floods * Known malicious or abusive sources * Automated scanning and exploit attempts * Suspicious traffic patterns * Traffic intended to exhaust application resources Platform protections do not require project-specific rules. See [Platform protections](/docs/security/platform-protections). Firewall controls [#firewall-controls] The Easel Firewall lets you define project-specific traffic rules. Start with [WAF rules](/docs/security/waf-rules) for an overview, then configure detailed rules under Firewall, Custom rules, and IP rules. Rules can evaluate request properties such as: * Path * Method * IP address * Country and ASN * Header * User agent * Hostname * Environment A matching rule can log, deny, challenge, redirect, or bypass Attack Mode, depending on the action you choose. See [Firewall](/docs/security/firewall) and [Custom rules](/docs/security/custom-rules). Attack Mode [#attack-mode] Attack Mode places an additional browser verification step in front of a project during an active attack or sudden surge in abusive traffic. It is intended as an emergency control, not as a replacement for application authentication or carefully scoped firewall rules. See [Attack Mode](/docs/security/attack-mode). Access protection [#access-protection] Access protection controls who can open a deployment before application code runs. Easel provides this through Deployment Protection for Preview deployments, branch URLs, unique deployment URLs, and other non-current deployments. These controls are separate from the firewall: * The firewall determines whether traffic is permitted at the request layer. * Deployment protection determines whether the requester is authorized to access the deployment. See [Access protection](/docs/security/deployment-protection). Security observability [#security-observability] Security decisions appear alongside ordinary request activity. You can use firewall events and request details to determine: * Which action Easel took * Which rule matched * Whether the request reached application code * Whether the request was challenged * Which project and deployment received the request See [Firewall observability](/docs/security/firewall-observability). Shared responsibility [#shared-responsibility] Easel secures the deployment platform and provides traffic and access controls. You remain responsible for the security of your application and data. See [Shared responsibility](/docs/security/shared-responsibility). Related documentation [#related-documentation] * [Platform protections](/docs/security/platform-protections) * [Security evaluation order](/docs/security/request-chain) * [WAF rules](/docs/security/waf-rules) * [Firewall](/docs/security/firewall) * [Custom rules](/docs/security/custom-rules) * [IP rules](/docs/security/ip-rules) * [Attack Mode](/docs/security/attack-mode) * [Bot traffic](/docs/security/bots) * [Firewall observability](/docs/security/firewall-observability) * [Access protection](/docs/security/deployment-protection) * [Security headers](/docs/security/security-headers) * [Secrets](/docs/security/secrets) * [Workspace security](/docs/security/workspace-security) * [Security limits](/docs/security/limits) * [Security troubleshooting](/docs/security/troubleshooting) * [Report a vulnerability](/docs/security/report-a-vulnerability) # IP rules IP matching in the Easel Firewall uses the client IP address determined by Easel. Use IP conditions to: * Restrict internal tools * Allow trusted services past Attack Mode * Block abusive networks * Limit administrative routes IP matching is configured as a condition on a [custom firewall rule](/docs/security/custom-rules). There is no separate IP-list product today. Single addresses [#single-addresses] Examples: ```text 203.0.113.10 2001:db8::10 ``` IPv4 and IPv6 addresses are both supported as string values in IP conditions. Matching operators [#matching-operators] For the **IP / CIDR** condition type: * **Equals** / **Does not equal** compare against the client IP string * **Is any of** / **Is not any of** accept a list of values (one per line) * String operators such as **Starts with** or **Contains** match the textual IP form Prefer exact equality or list membership over broad substring matches. Client IP trust [#client-ip-trust] Easel determines the client IP from its trusted edge connection. Headers such as these are not trusted directly from arbitrary clients: ```text X-Forwarded-For X-Real-IP CF-Connecting-IP ``` Easel honors forwarding headers only on its trusted edge path. That same client IP feeds geo and ASN conditions. Bypass Attack Mode for trusted IPs [#bypass-attack-mode-for-trusted-ips] To exempt a monitoring service or office network from Attack Mode without disabling platform protections: ```text IP is any of 203.0.113.10 198.51.100.20 Action: Bypass attack challenge ``` **Bypass attack challenge** skips Attack Mode only. It does not skip platform protections, deployment protection, or other custom rules above this rule in the list. Deny by IP [#deny-by-ip] ```text IP equals 203.0.113.66 Action: Deny ``` Combine IP conditions with path or method conditions when you only need to protect a subset of routes. Dynamic clients [#dynamic-clients] IP rules are a poor fit for users with frequently changing residential or mobile addresses. For human access to non-public deployments, prefer [Deployment protection](/docs/security/deployment-protection) with workspace sign-in. Webhooks [#webhooks] Webhook providers may publish IP ranges, but IP validation alone is often insufficient. Also verify: * Request signatures * Timestamps * Replay protection * Expected event types * Request body integrity Related guides [#related-guides] * [Custom rules](/docs/security/custom-rules) * [Firewall](/docs/security/firewall) * [Attack Mode](/docs/security/attack-mode) * [Security limits](/docs/security/limits) # Security limits These limits describe current platform behavior that affects security configuration and challenge flows. Attack Mode [#attack-mode] | Limit | Value | | ----------------------------- | ------------------------------------------ | | Challenge response status | `429` | | Verification endpoint | `POST /.well-known/easel-challenge/verify` | | Session cookie | `easel_ch_pass` | | Verification session lifetime | About 1 hour | | Work-token lifetime | About 15 minutes | Challenge difficulty is platform-managed and may change. Deployment protection [#deployment-protection] | Limit | Value | | ---------------- | ------------ | | Session cookie | `easel_dp` | | Session lifetime | About 1 hour | Request size [#request-size] Large request bodies are rejected before they can exhaust edge or application resources. The default maximum request body size is 10 MiB unless otherwise configured for the platform. Firewall rules [#firewall-rules] Custom rules are ordered per project. There is no separate priority number beyond list order. Practical guidance: * Prefer fewer, well-scoped rules over many overlapping ones * Validate regex rules with **Log** before enforcing * Combine path and method conditions before broad IP or country denies Plan-specific quotas for rule count, IP lists, and log retention may be published separately as product packaging evolves. Geographic data [#geographic-data] | Field | Support | | ------------- | ------------------------------------ | | Country | Supported in custom rules | | Continent | Supported in custom rules | | ASN | Supported in custom rules | | Region / city | Not available as firewall conditions | Geo and ASN data is powered by [IPLocate.io](https://www.iplocate.io) (CC BY-SA 4.0). Accuracy can vary for VPNs, proxies, and mobile networks. Unknown locations do not match equality conditions unless you account for that explicitly. Propagation [#propagation] Firewall and Attack Mode configuration changes save on the project and propagate to the edge after a short delay. You do not need to redeploy. Allow a brief window before expecting every region to serve the latest rules. Related guides [#related-guides] * [Custom rules](/docs/security/custom-rules) * [Attack Mode](/docs/security/attack-mode) * [Firewall observability](/docs/security/firewall-observability) # Platform protections Platform protections identify and mitigate malicious or abusive traffic before it can consume application resources. They are managed by Easel and apply without project-specific configuration. What platform protections cover [#what-platform-protections-cover] Platform protections are designed to mitigate traffic such as: * Network floods * Application-layer request floods * Known malicious sources * Automated vulnerability scanning * Repeated abusive behavior * Suspicious request patterns * Attempts to exhaust functions or origin resources The specific detection systems and data sources used by Easel may change over time. The customer-facing contract is the protection behavior, not a particular internal implementation or third-party provider. Automatic operation [#automatic-operation] Platform protections are always active for supported Easel deployments. They do not require: * Custom firewall rules * Attack Mode * Application middleware * Framework configuration * Changes to application code Project-specific firewall settings provide additional control but do not replace the underlying platform protections. What happens to blocked traffic [#what-happens-to-blocked-traffic] Traffic rejected by a platform protection is stopped before it reaches static output or invokes a function. Blocked requests do not run your application code. They may still appear in security and request analytics so you can investigate activity. Do not assume blocked traffic is excluded from every usage meter unless a billing document states that explicitly. Visibility [#visibility] When a platform protection stops a request, Easel exposes enough information for you to understand the outcome without revealing sensitive detection details. Useful signals include: * Action taken (for example block, challenge, or throttle) * Request ID and timestamp * Path, method, and hostname * Whether the decision came from platform protection rather than a custom rule Easel does not expose internal provider names, private detection signatures, raw reputation scores, or details that make evasion easier. Customer overrides [#customer-overrides] Custom firewall rules cannot disable platform protections. The **Bypass attack challenge** action skips project-wide [Attack Mode](/docs/security/attack-mode) only. It does not override platform blocks, deployment protection, or other edge safety controls. DDoS protection [#ddos-protection] Platform protections help absorb network and HTTP request floods at the edge. They are not interchangeable with: * Custom firewall rules * Attack Mode * Application authentication * Application-level rate limiting or quotas No traffic-protection system can guarantee that every application remains available during every attack. Protect expensive endpoints with authentication, caching, validation, and carefully scoped firewall rules. Limitations [#limitations] Application design still matters. Protect expensive endpoints with: * Authentication * Authorization * Caching * Request validation * Idempotency * Application-level quotas * Efficient database queries * Carefully scoped firewall rules Related guides [#related-guides] * [Security evaluation order](/docs/security/request-chain) * [Firewall](/docs/security/firewall) * [Attack Mode](/docs/security/attack-mode) * [Shared responsibility](/docs/security/shared-responsibility) # Report a vulnerability Report suspected vulnerabilities privately so the Easel security team can investigate before public disclosure. What to include [#what-to-include] Include: * A clear description * Affected product or endpoint * Reproduction steps * Proof-of-concept requests or code * Security impact * Preconditions * Relevant account, workspace, project, deployment, or request IDs * Whether customer data may be affected * Your preferred contact information Do not include customer secrets or unrelated personal data. Contact [#contact] Email: ```text security@easel.sh ``` Do not post vulnerabilities in community channels. Easel acknowledges reports promptly and coordinates disclosure with you. Research guidelines [#research-guidelines] When testing: * Stay within systems you are authorized to assess * Avoid denial-of-service testing against production * Do not access or exfiltrate customer data * Do not use social engineering against Easel staff or customers * Give Easel reasonable time to investigate before public disclosure Account compromise [#account-compromise] For suspected account compromise: 1. Revoke active sessions. 2. Rotate API tokens. 3. Rotate exposed application secrets. 4. Review workspace members. 5. Contact support or `security@easel.sh`. Related guides [#related-guides] * [Workspace security](/docs/security/workspace-security) * [Secrets](/docs/security/secrets) * [Shared responsibility](/docs/security/shared-responsibility) # Security evaluation order Security controls run in a defined order. An earlier decision may prevent later controls or application code from running. Request flow [#request-flow] ```text Request received ↓ Platform protections ↓ Deployment protection ↓ Verified bot classification ↓ Custom firewall rules ↓ Attack Mode ↓ Cache and routing ↓ Static output or function ``` Platform protections run first and are not overridable by custom rules. Deployment protection runs before custom firewall rules. Custom rules run before Attack Mode, so a matching rule can deny, challenge, redirect, or bypass Attack Mode before the project-wide challenge applies. Why order matters [#why-order-matters] Suppose a request matches both a custom deny rule and Attack Mode. Because custom rules run first: * A **Deny** rule stops the request with 403 before Attack Mode runs. * A **Challenge** rule issues browser verification for that match. * A **Bypass attack challenge** rule skips Attack Mode for that match. * A **Log** rule records the match and continues evaluation. Platform protections remain active regardless of custom-rule outcomes. Terminal actions [#terminal-actions] A terminal action ends evaluation for that layer and prevents the request from reaching the application. Terminal outcomes include: * Platform block or throttle * Custom rule **Deny** * Custom rule **Redirect** * Challenge response (from a custom rule or Attack Mode) * Deployment protection sign-in response A **Log** action records a match and continues. When no custom rule matches, the request continues unless another security layer blocks or challenges it. There is no separate **Allow** action that ends evaluation and skips later layers. Use **Bypass attack challenge** when you need trusted traffic to skip Attack Mode only. Rule order [#rule-order] Within a custom firewall configuration: 1. Active rules are evaluated from top to bottom. 2. The first matching rule wins. 3. Disabled rules are skipped. 4. Reordering rules changes which match applies first. See [Custom rules](/docs/security/custom-rules). Attack Mode [#attack-mode] Attack Mode is a project-wide challenge that runs after custom firewall rules. Custom rules can: * Block traffic before the challenge * Challenge only selected routes * Bypass Attack Mode for matching traffic * Redirect selected clients elsewhere Verified bots may skip Attack Mode when **Allow verified bots** is enabled. See [Bot traffic](/docs/security/bots). Cache interaction [#cache-interaction] Security controls run before cache lookup and origin routing. A request must pass platform protections, deployment protection (when applicable), custom rules, and Attack Mode before Easel serves cached content or invokes your application. Short-circuit security responses are not stored as ordinary cacheable application content. Rewrites and internal requests [#rewrites-and-internal-requests] Internal rewrites and routing decisions happen after security evaluation. Easel does not re-run the full security chain for each internal rewrite hop. External rewrite destinations and proxied origins receive traffic only after the original client request has passed security controls. Internal edge fetches used for platform operations can use a trusted bypass that external clients cannot reproduce. Challenge verification for `/.well-known/easel-challenge/*` is handled so verification can complete without looping. Request metadata [#request-metadata] When a custom rule denies or fails a redirect, Easel may set response headers such as: * `X-Easel-Firewall-Action` * `X-Easel-Firewall-Rule-Id` Treat these as operational signals for debugging. Do not rely on client-supplied copies of security headers as proof of identity. See [Firewall observability](/docs/security/firewall-observability). Related guides [#related-guides] * [Platform protections](/docs/security/platform-protections) * [Firewall](/docs/security/firewall) * [Attack Mode](/docs/security/attack-mode) * [Deployment protection](/docs/security/deployment-protection) # Secrets Secrets include API keys, database credentials, signing keys, and other values that must not be committed to source control. On Easel, secrets are stored as environment variables in project settings. Environment scopes [#environment-scopes] Use different secret values for: * Preview * Production Preview deployments do not automatically receive Production secrets. You can also set Preview branch overrides for branch-specific values. See [Deployment environments](/docs/deployments/environments). Encryption and access [#encryption-and-access] Easel encrypts environment variable values at rest and restricts access through workspace membership and project settings controls. Sensitive variables are write-only in the dashboard after creation: you can replace the value, but you cannot read it back or rename the key in a way that exposes the prior secret. Snapshots [#snapshots] When a deployment is created, Easel snapshots the resolved variable values for that environment. The build and runtime use that snapshot. Changing variables later does not alter an existing deployment. Create a new deployment to pick up updates. Build access [#build-access] Build commands may read environment variables required to compile the application. Dependency installation scripts and build-time code may therefore access build-scoped secrets. Reduce risk by: * Avoiding unnecessary secrets during builds * Pinning dependencies * Reviewing install scripts * Separating build and runtime credentials * Using least-privilege tokens * Rotating exposed credentials Runtime access [#runtime-access] Functions receive the secrets available in the deployment’s snapshot for that environment. Logs [#logs] Automatic redaction cannot catch every transformed or encoded value. Applications must not log: * Authorization headers * Session cookies * Full connection strings * Private keys * Access tokens * Passwords * Signed webhook secrets Forks and Preview secrets [#forks-and-preview-secrets] Treat Preview configuration as untrusted-input space: * Keep Production secrets out of Preview variables * Prefer Preview branch overrides only where needed * Enable [deployment protection](/docs/security/deployment-protection) when previews must not be public Rotation [#rotation] Rotate a secret when: * A team member with access leaves * It appears in source control or logs * A third-party integration is compromised * A credential reaches its scheduled rotation date * Access scope changes After updating a secret, create a new deployment so application code receives the new value. Deletion [#deletion] Deleting a secret prevents future deployments from receiving it. Existing deployments retain their previously resolved snapshot until they are replaced. Related guides [#related-guides] * [Deployment environments](/docs/deployments/environments) * [Workspace security](/docs/security/workspace-security) * [Shared responsibility](/docs/security/shared-responsibility) # Security headers HTTP response headers can reduce the impact of cross-site scripting, framing, MIME confusion, and information leakage. Easel may set infrastructure-level headers, but application-specific browser policies remain your responsibility. Content Security Policy [#content-security-policy] Content Security Policy (CSP) restricts which resources a browser may load and execute. Example starting point: ```http Content-Security-Policy: default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none' ``` A real CSP must account for the application’s scripts, styles, images, APIs, fonts, frames, and development tooling. Roll out CSP carefully: 1. Begin with `Content-Security-Policy-Report-Only`. 2. Collect violation reports. 3. Remove unsafe dependencies. 4. Narrow allowed sources. 5. Enforce the policy. Avoid adding broad values such as `*`, `'unsafe-inline'`, or `'unsafe-eval'` without understanding the tradeoff. HSTS [#hsts] HTTP Strict Transport Security instructs browsers to use HTTPS. Example: ```http Strict-Transport-Security: max-age=31536000 ``` Only add `includeSubDomains` when every subdomain supports HTTPS. Only request preload after verifying the domain meets browser preload requirements and the policy can be maintained. See [HTTPS and TLS](/docs/cdn/https-and-tls). Framing [#framing] Prevent unauthorized framing with CSP: ```http Content-Security-Policy: frame-ancestors 'none' ``` `X-Frame-Options` remains useful for older clients: ```http X-Frame-Options: DENY ``` Use `SAMEORIGIN` or an explicit CSP allowlist when legitimate embedding is required. MIME sniffing [#mime-sniffing] ```http X-Content-Type-Options: nosniff ``` This helps prevent browsers from interpreting a response as a different content type. Referrer policy [#referrer-policy] Example: ```http Referrer-Policy: strict-origin-when-cross-origin ``` Choose a policy based on analytics and privacy requirements. Permissions policy [#permissions-policy] Permissions Policy controls browser features such as camera, microphone, and geolocation. Example: ```http Permissions-Policy: camera=(), microphone=(), geolocation=() ``` Framework configuration [#framework-configuration] Configure headers using your framework’s supported mechanism (for example Next.js `headers` in `next.config`). See also [Request and response headers](/docs/cdn/headers) for platform headers Easel may set on CDN responses. Related guides [#related-guides] * [Request and response headers](/docs/cdn/headers) * [HTTPS and TLS](/docs/cdn/https-and-tls) * [Shared responsibility](/docs/security/shared-responsibility) # Shared responsibility Security on Easel is shared between Easel and the customer. Easel secures the platform and provides deployment, traffic, access, and observability controls. Customers secure their application logic, identities, data, dependencies, and external integrations. Easel responsibilities [#easel-responsibilities] Easel is responsible for: * Operating the deployment platform * Protecting the edge and control plane * Isolating customer workloads * Encrypting supported platform traffic * Applying platform protections * Enforcing configured firewall rules * Protecting stored configuration according to platform controls * Maintaining platform software * Recording supported security events * Responding to platform security incidents Customer responsibilities [#customer-responsibilities] Customers are responsible for: * Application authentication * Authorization and data isolation * Input validation * Secure session handling * Dependency selection and updates * Secret scope and rotation * Database security * External service permissions * Application security headers * Firewall-rule correctness * Reviewing security events * Legal and privacy obligations for application data Shared areas [#shared-areas] | Area | Easel | Customer | | ----------------- | ------------------------------------ | -------------------------------------------- | | Secrets | Secure storage and access controls | Choose, scope, and rotate values | | Firewall | Execute rules reliably | Design and test rules | | Deployments | Isolate and publish artifacts | Review source and dependencies | | Logs | Collect and protect supported events | Avoid logging secrets and monitor events | | Domains | Provide routing and TLS controls | Control DNS and domain ownership | | Incident response | Respond to platform incidents | Respond to application and account incidents | Application vulnerabilities [#application-vulnerabilities] The firewall can reduce exposure but cannot correct insecure application logic. Examples include: * Broken authorization * Injection flaws in application code * Insecure direct object references * Weak password-reset flows * Cross-tenant data access * Unsafe file uploads * Vulnerable dependencies Use secure development practices and application testing in addition to platform controls. Compliance [#compliance] Using a hosting platform does not automatically make an application compliant. Customers remain responsible for: * Data classification * Configuration * Access policies * Retention * User consent * Vendor management * Required audits * Application-specific controls Related guides [#related-guides] * [Platform protections](/docs/security/platform-protections) * [Secrets](/docs/security/secrets) * [Workspace security](/docs/security/workspace-security) * [Report a vulnerability](/docs/security/report-a-vulnerability) # Security troubleshooting Start with the request ID and firewall event whenever available. A legitimate request is blocked [#a-legitimate-request-is-blocked] Check: * Firewall action * Rule ID and rule name * Whether the decision came from a platform protection or custom rule * Client IP * Path and method * Country and ASN * Rule order * Recent firewall changes If a custom rule caused the block: 1. Disable or narrow the rule. 2. Confirm traffic recovers. 3. Recreate the issue with the **Log** action. 4. Refine the conditions. 5. Re-enable enforcement. Do not create a broad **Bypass attack challenge** rule until you understand which layers it does and does not skip. A request is challenged unexpectedly [#a-request-is-challenged-unexpectedly] Check whether: * Attack Mode is enabled * A custom rule uses **Challenge** * The client lacks a valid verification cookie * The request uses a different hostname or deployment * Cookies or JavaScript are blocked * A privacy tool removes verification state An API or webhook stopped working [#an-api-or-webhook-stopped-working] Attack Mode and browser challenges are usually incompatible with machine clients. Create a narrow **Bypass attack challenge** exemption based on strong signals such as: * Trusted IP range * Dedicated webhook path * Dedicated API hostname Continue validating webhook signatures in application code. A browser is stuck in a challenge loop [#a-browser-is-stuck-in-a-challenge-loop] Check: * JavaScript is enabled * Cookies are allowed for the hostname * The system clock is approximately correct * The hostname remains consistent across redirects * Multiple domains are not switching between unprotected and protected hosts * CSP is not blocking challenge assets Use the request ID from the challenge page when contacting support. A verified bot is blocked [#a-verified-bot-is-blocked] Check: * The bot is genuinely verified * Customer rules do not explicitly deny it * The request comes from expected infrastructure * **Allow verified bots** is enabled for Attack Mode * Custom **Challenge** rules do not match the bot first Do not allow a bot solely by user agent when stronger verification is available. A rule does not match [#a-rule-does-not-match] Check: * The rule is active * Rule order * Exact path representation (normalized vs raw) * Method * Header spelling and key * Case sensitivity * Client IP source * IPv4 versus IPv6 * Unknown geography * Environment (`production` vs `preview`) Use **Log** with one condition at a time to isolate the mismatch. A rule matches too much traffic [#a-rule-matches-too-much-traffic] Common causes include: * `contains` used instead of `equals` * Missing hostname condition * Broad IP list * Negated condition * Incorrect AND/OR grouping * Empty or missing header behavior * Path normalization assumptions Rule changes appear inconsistent by region [#rule-changes-appear-inconsistent-by-region] Check: * Change timestamp * Edge region in request logs * Whether older requests were already in flight Firewall changes propagate after a short delay. Escalate behavior outside that window with request IDs and regions. Information to include with support [#information-to-include-with-support] Include: * Workspace and project * Deployment ID * Domain * Request ID * Approximate timestamp * Client region * Firewall action * Rule ID * Expected behavior * Reproduction steps Related guides [#related-guides] * [Custom rules](/docs/security/custom-rules) * [Attack Mode](/docs/security/attack-mode) * [Firewall observability](/docs/security/firewall-observability) * [Report a vulnerability](/docs/security/report-a-vulnerability) # WAF rules WAF rules let you match request attributes and choose an action before the request reaches cache, static assets, or Functions. What you can match [#what-you-can-match] Depending on the rule type, matchers can include path, method, IP address, country, ASN, header, user agent, hostname, and environment. See [Firewall](/docs/security/firewall), [Custom rules](/docs/security/custom-rules), and [IP rules](/docs/security/ip-rules). Actions [#actions] A matching rule can log, deny, challenge, redirect, or bypass Attack Mode, depending on the action you configure. Order and bypasses [#order-and-bypasses] Earlier security decisions can prevent later layers from running. Test with log-only rules when you need to observe matches without changing traffic. See [Security evaluation order](/docs/security/request-chain). Observability and limits [#observability-and-limits] * [Firewall observability](/docs/security/firewall-observability) * [Security limits](/docs/security/limits) * [Security troubleshooting](/docs/security/troubleshooting) * [Attack Mode](/docs/security/attack-mode) * [Bot traffic](/docs/security/bots) # Workspace security Workspace security determines who can view projects and perform sensitive actions. Accounts [#accounts] Protect Easel accounts with: * Strong unique passwords, when password sign-in is enabled * Secure OAuth account hygiene for connected identity providers * Reviewed active sessions and revoked unused access Sign-in options depend on your workspace configuration (for example GitHub OAuth). Roles [#roles] Workspace roles follow least privilege. Easel uses these roles: | Role | Typical access | | ---------- | ------------------------------------------------------------- | | **Owner** | Full workspace control, including billing and membership | | **Admin** | Workspace and project administration | | **Member** | Day-to-day project work such as deployments and configuration | Owners and admins are managers for membership and workspace administration tasks. Prefer inviting members with the least access they need. Sensitive actions [#sensitive-actions] Treat these as high-impact changes: * Viewing or editing secrets * Changing domains * Enabling Attack Mode * Editing firewall rules * Promoting deployments * Rolling back production * Deleting projects * Managing members and billing * Creating or revoking API tokens API tokens [#api-tokens] Use API tokens with the narrowest practical scope, rotate them regularly, and revoke tokens that are no longer needed. Avoid long-lived, unscoped personal tokens for production automation. Sessions [#sessions] Revoke sessions you no longer recognize and rotate credentials if you suspect account compromise. See [Report a vulnerability](/docs/security/report-a-vulnerability) for security contact details. Related guides [#related-guides] * [Secrets](/docs/security/secrets) * [Deployment protection](/docs/security/deployment-protection) * [Shared responsibility](/docs/security/shared-responsibility)