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.
| Engine | Floor | Released |
|---|---|---|
| Chrome | 116 | Aug 2023 |
| Edge | 116 | Aug 2023 |
| Firefox | 115 | Jul 2023 (current ESR lineage) |
| Safari (macOS) | 16.4 | Mar 2023 |
| Safari (iOS) | 16.4 | Mar 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 versionsre-resolves every timecaniuse-liteupdates, 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
- Edit
browserslistinpackage.json. - Run
bun run compat.rules. - Commit the regenerated
.oxlintrc.compat.jsonalongside it. - 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.