Skip to content

Browser Support

The floor OnRamp's UI is built against, why it sits where it does, and how a lint rule keeps the code honest about it.

The supported floor

browserslist in package.json is the single declaration. Everything else — the lint rules, the messages devs read — derives from it.

EngineFloorReleased
Chrome116Aug 2023
Edge116Aug 2023
Firefox115Jul 2023 (current ESR lineage)
Safari (macOS)16.4Mar 2023
Safari (iOS)16.4Mar 2023

Why these, and not "last 2 versions":

  • Safari is the binding constraint, and it is pinned to the OS. A customer on macOS Monterey or Ventura cannot install Safari 17 or 18. Firefox and the Chromium engines update themselves; Safari's floor is really a macOS floor.
  • Enterprise browsers lag. OnRamp's org-admin users are on managed machines, frequently on Chrome/Edge extended-stable or Firefox ESR. Firefox 115 is the ESR lineage those fleets sit on.
  • Explicit versions, not relative queries. last 2 versions re-resolves every time caniuse-lite updates, which would churn the generated lint rules and fail the staleness gate on unrelated PRs. A fixed floor changes only when someone decides it should.

This floor is a proposal that has not yet been ratified against usage data. It was chosen to be defensible rather than measured — no published support statement existed when it was written. or_user_sessions.user_agent holds real production user agents and is the way to replace judgement with evidence.

Raising or lowering the floor

  1. Edit browserslist in package.json.
  2. Run bun run compat.rules.
  3. Commit the regenerated .oxlintrc.compat.json alongside it.
  4. Fix, or deliberately allow, whatever the lint now reports.

Lowering the floor widens the restricted set and will surface new violations; raising it retires rules. Both are the point — the floor is the input, and the rules are downstream of it.

How the floor is enforced

A build target cannot do this job. build.target downlevels syntax; Promise.withResolvers is a runtime built-in method, so every target setting leaves the call intact and it ships. The check has to know which API arrived in which browser version.

devtools/browser-compat/generate-oxlint-compat.ts reads the browserslist floor and MDN's @mdn/browser-compat-data, and writes .oxlintrc.compat.json — a set of no-restricted-globals and no-restricted-properties rules covering everything a bare identifier can name that some targeted browser lacks. That is four surfaces in MDN's data: interfaces on the global object (api.*), the window object's own functions and properties (api.Window.*, where requestIdleCallback and the file pickers live), the language's globals (javascript.builtins.*), and the static members of both (_static-suffixed keys, and non-prototype members of a built-in). .oxlintrc.json extends it, so the existing bun run lint job enforces it with no second linter.

The rules apply to app/api/embed_widget/static/**, app/onramp/**, app/ui-customer/**, app/ui-organization/**, app/*.js and design-system/generated/**. Two of those are in the list despite their paths: customers load the embed-widget SDK on their own sites, so its floor is wider than the app's, and both bundle entries import the generated PrimeVue preset, which carries hand-written helpers rather than only data. A violation in the preset is fixed in design-system/scripts/transform-tokens.ts — the generated file is overwritten by bun run tokens.

This list is the one part of the mechanism that is hand-maintained rather than derived, so a new browser-served tree has to be added here. The rest of the repo — devtools, scripts, playwright, .github — runs on Bun or Node, where the floor is meaningless.

A browserslist entry that MDN's data cannot cover fails generation rather than dropping silently out of the floor, so a target cannot be declared and then go unenforced.

An entry only counts as support the code can rely on. MDN qualifies the rest on the same record — preference-gated (flags), shipped under another name (prefix, alternative_name), withdrawn (version_removed), or shipped incomplete (partial_implementation, which is how iOS Safari carries a Notification that exists only for a page saved to the home screen). The generator's list of those qualifiers is asserted exhaustive against the pinned data, so a key MDN starts using fails the suite instead of quietly reading as full support.

Frontend CI regenerates the file and git diff --exit-codes it, the same staleness pattern the design tokens and route maps use. A bump to @mdn/browser-compat-data that changes a support table fails that gate until the regenerated rules are committed.

One caveat: the field has a second reader

postcss.config.js runs autoprefixer, which reads browserslist too, so in principle this field also retargets CSS prefixing. Measured on this repo it does not: vite.config.js configures css.lightningcss, which owns the transform, and the production bundle is byte-identical with and without the field — 324 stylesheets, same hashes, and the same counts of every prefix autoprefixer would have changed. Worth re-checking if the CSS pipeline ever moves back to postcss.

What it does not cover

Instance methods, on both language built-ins and Web API interfaces. value.toSorted(), str.replaceAll(), arr.at(), signal.throwIfAborted() — a lint rule cannot tell what type value is, so restricting the bare property name would fire on every unrelated object that happens to share it. The generator skips them deliberately; MDN's spec anchor (sec-array.prototype.…) is what separates them from statics.

Syntax. New syntax is a parse concern, not an API lookup. That belongs to the build target, which is the one thing a target setting does handle.

When a violation is wrong

Use a targeted oxlint-disable-next-line no-restricted-properties with a comment saying why the code is unreachable on an unsupported engine. Never widen the floor or disable the rule wholesale to get CI green — that returns the codebase to the state in which a Safari-breaking call reached production and was found by a Sentry alert rather than by review.

Internal documentation — gated behind Cloudflare Access.