Back to all posts

My Pull Request Is Still Open. My Code Already Shipped in Convex.

I've been reading Convex docs for three years — once, memorably, during a lecture. Then they open-sourced the backend. Nine lines later my fix for the missing URL.canParse() is live in main, and my PR is still open. Neither is a mistake.

Syed Suhail Ahmed

Syed Suhail Ahmed

Aug 5, 20266 min read

My Pull Request Is Still Open. My Code Already Shipped in Convex.

Some pull requests get merged. This one got adopted.

I found Convex about Three years ago, early in 2nd year college, back when I was still stitching backends together out of whatever tutorial I'd watched that week. It was the first time I saw a backend where you write a query as an ordinary TypeScript function and every client just… stays current. No polling, no cache invalidation, no websocket plumbing. Someone had deleted the boring half of backend work.

I got a little obsessed. Obsessed enough that a professor once caught me reading the Convex docs in the middle of his lecture — laptop open, very much not on the slides. I don't remember what he said. I do remember not closing the tab.

That reading added up. The blog you're on right now runs on Convex — these posts, the tags, the view counts I check in the studio, the newsletter, the editor I'm typing into. And somewhere along the way I picked up a quiet ambition: I'd like to be a Convex Champion one day. I'm not one.

For a long time there was nothing to contribute to. Convex was a product I used, not a codebase I could open. Then they open-sourced the backend, and the door I'd been standing outside of was just… open. I still took my time. When a issue finally turned up in convex-backend, I had 3 years of docs in my head and no more excuses.

Then something happened I wasn't expecting. The fix is live in main. My pull request is still open. Both are true at once, and neither is a mistake.

What Convex actually is

Convex is an open-source reactive backend for TypeScript apps — database, functions, scheduling and file storage in one box. Your query is a plain function; Convex works out when its result changed and pushes it.

Under the hood, though, it's a Rust backend running your TypeScript inside V8 isolates. That has a consequence most people never think about: Convex has to build the web platform itself. No browser, no Node in there — so fetch, Headers, crypto, TextEncoder and URL are all hand-implemented, in npm-packages/udf-runtime (UDF = user-defined function). It's why a Convex function feels like it's in a browser when it's really inside a Rust process.

That runtime is where I ended up.

A library that broke on a method that didn't exist

Issue #510, from RJ Dellecese, was two lines: Convex's URL had no static canParse(), and that broke Effect 4's urlFromString schema transform.

canParse is the boring half of the URL API. new URL(x) throws on bad input, so validating means try/catch; URL.canParse(x) just answers true or false. Node, Deno, Bun and every browser have shipped it for years — which is exactly why library authors reach for it without thinking, and why its absence doesn't fail loudly. It fails as URL.canParse is not a function, deep inside somebody else's validator, in a stack trace with your app's name on it.

A class with no statics at all

One file: npm-packages/udf-runtime/src/00_url.ts. (That 00_ is a Deno-ism Convex kept — runtime files numbered by bootstrap order, so URL exists before anything needs it.)

The class was thorough: searchParams, every href setter, the whole instance surface. And zero static methods. Not canParse, not parse, not createObjectURL. That's not sloppiness — it's what incremental platform work looks like. You implement what users hit, and the gaps stay invisible until someone's library falls into one.

Then the interesting part. The JavaScript class doesn't parse URLs at all. It asks Rust to:

const urlInfo: UrlInfo = performOp("url/getUrlInfo", url, baseHref);

performOp is a syscall out of the isolate. Follow it into crates/isolate/src/ops/http.rs and you find op_url_get_url_info, parsing with Rust's url crate and, on failure:

// The URL spec (https://url.spec.whatwg.org/) dictates that JS
// throw a TypeError when the URL is invalid.

Err(_) => anyhow::bail!(TypeError::new(format!("Invalid URL: '{url}'"))),

That comment is the fix, handed to me by someone I've never met. Somebody had already read the spec on the Rust side, so new URL() inside a Convex function already threw a real TypeError for exactly the right reasons.

So canParse doesn't need a parser. It needs a catch.

Nine lines

static canParse(url: string | URL, base?: string | URL): boolean {
  try {
    new URL(url, base);
    return true;
  } catch {
    return false;
  }
}

One file, nine additions, zero deletions. It's the canonical polyfill, and it's right here for a non-obvious reason: it inherits its correctness from the constructor. Convex's parsing lives in Rust and will keep evolving — any edge case they fix there, canParse agrees with automatically, forever. A hand-written validator would have started drifting immediately.

Then the PR stopped being the point

Six days later the fix shipped, in a commit I didn't write.

commit 3d22026817508938455f3fabc084981ea8a7778d
Author: Ian Macartney
    Add URL.canParse() and URL.parse() static methods
    to the UDF runtime (#55225)
    ...
    Co-authored-by: SUHAIL <...>
    GitOrigin-RevId: cdc651574242479b21785bc55b7146da01640839

That (#55225) is not a number this repo has ever seen. The public repo is in the 500s. #55225 is a PR in Convex's private monorepo, and GitOrigin-RevId is the internal revision this commit was exported from. Every commit in the log carries both.

get-convex/convex-backend isn't where Convex develops Convex. It's a mirror, pushed outward from the inside, synced within a few days — something the README says in one easy-to-skim sentence that I had read without understanding.

Which means external PRs can't be merged the normal way; there's nothing on this side to merge into. A maintainer lands your change internally, and it flows back out with your name on a Co-authored-by trailer. Not a consolation prize — the only mechanism the architecture allows.

As I write this: PR #511 open, issue #510 open, code in main. All three at once. The export just doesn't reach back through GitHub to tidy up after itself.

What the maintainer added

My nine lines landed byte for byte. Then Ian Macartney added URL.parse()canParse's sibling static, returning the URL or null — and wrote the tests, internally, which is why the public commit still touches only one file. I'd fixed the reported bug; they fixed the gap. Nine additions became seventeen.

Worth It

That changes includes this blog, eventually — the runtime I patched is the runtime serving you this paragraph.

I still want the Champion badge. But this was the part that actually mattered: 3 years after a professor caught me reading these docs instead of listening to him, nine of my lines are inside them.

On to the next convex issue. #512

Subscribe for new contributions

Get an email when I publish a new open-source write-up — how I approached the issue, the code, and lessons learned. No spam.