update #6: how namespaces work
Mar 5, 2026 by Kasper Timm Hansen and David Rodríguez
We’re live in beta! Here are our next steps
Last week, we at gem.coop announced support for namespaces that you can publish to and use namespaced gems in Bundler today. Our original post outlines how namespaces improve package security, but in this post we’ll go through how to use them and how Bundler handles namespaced gems.
We’ve already had 166 people sign up to try the beta, and we have 175 namespaces live today. Thank you for trying us out! Please keep the feedback coming. You can sign up at https://beta.gem.coop/sign_up
In the immediate term, we’re working on better support for gems that have been precompiled (we’ve had an overzealous gem version check when you push gems), but we’re pretty close to having it.
We’ve also been working on incorporating your feedback and we’ve already shipped a more detailed namespace profile and gem profile page. You can see an example of each here:
While these are early stages we’re intentionally aiming for these to be pages that you as a gem author would feel proud to post. Gem authors are also some of the most conscientious and discerning Rubyists around, and they’re typically plugged in to which gems they’d recommend others check out. We’re working on a lightweight recommendation system to help with that. This work is inspired by seeing what the team at rubyevents.org has been putting out, and helping Rubyists show off their work. It’s time gems, the backbone of so much of our day-to-day code, get the same loving treatment.
For now, all gem.coop namespaces are public and don’t require any authentication. We plan to allow namespaces to have private gems in the future, and those will require authentication. We’ll announce our roadmap soon, along with with what we’re thinking for free and paid plans and pricing.
How Bundler handles namespaces already
First, when you claim your personal namespace at https://beta.gem.coop/sign_up, you get your own index that works just like the global gem.coop index, but it only includes the gems that you’ve published there. As an example, here’s the official namespace for gem.coop: https://beta.gem.coop/@gem-coop
Next, to tell Bundler (and soon, rv) that you want to use gems from your namespace, wrap them in a source block in your Gemfile, like this:
source "https://gem.coop"
gem "global"
source "https://beta.gem.coop/@myspace" do
gem "personal"
end
This way Bundler knows to resolve and install personal through the @myspace namespace, while other gems should come from the global source.
If personal has any internal dependencies, Bundler will also pull those from the same @myspace source to protect from dependency confusion attacks.
Now what if personal and global share a common dependency like i18n? How do we resolve that ambiguity and prevent dependency confusion? A case like this:
source "https://gem.coop"
gem "global" # depends on i18n
source "https://beta.gem.coop/@myspace" do
gem "personal" # depends on i18n, too
end
What source should Bundler pick from to resolve i18n? The global source, or the namespaced one in @myspace?
Bundler will consider a namespaced source to rank higher in specificity over a global source, so i18n will still come from @myspace if it’s there.
But what if you’re using two namespaces, one for the organization you work for, and one that you published to your personal namespace, like this:
source "https://gem.coop"
source "https://beta.gem.coop/@organization" do
gem "business" # depends on i18n
end
source "https://beta.gem.coop/@myspace" do
gem "personal" # depends on i18n, too
end
If both @organization, and @myspace have versions of i18n, this case is ambiguous and Bundler doesn’t want to choose for you, since that could be insecure. Instead, Bundler errors out and tells you to declare gem i18n inside which source you want to pull it from.
This process of “resolving sources” requires querying the index for each source, to know which gem names are available, and then Bundler can move on to resolving gem versions. It’s recorded in Gemfile.lock, so it can be skipped the next time Bundler runs (if the lockfile is in sync with the Gemfile).
Next, let’s look at the HTTP requests that Bundler uses do all of this work.
How Bundler queries sources, resolves versions and installs gems
To give you more context for how Bundler works, these are the important routes and how Bundler uses them:
/versions— used to resolve which gems are in a source/info/:name— to resolve which versions are available for each gem/gems/:name-:version.gem— downloading a gem resolved to one version
/versions
- https://gem.coop/versions (watch out, it’s ~20mb!)
- https://beta.gem.coop/@kaspth/versions
A text file of gem names + versions + hash of the current /info file.
The versions file is mostly append only, so Bundler can use HTTP Range requests to update the 20mb version of the global index incrementally.
When a new gem version is pushed, it goes on a single line at the end of the file like:
gem 2.0.0 abc123
Once a month or so, /versions files are compacted to combine versions, so the lines look like:
gem 1.0.0, 1.0.1, 2.0.0 abc123
If you’ve ever heard talks from the original Bundler maintainers mention the “Compact Index”, this is it!
/info/:name
An info file contains versions plus dependencies plus metadata. gem.coop outputs more metadata than RubyGems, like licenses, executables, and published_at time. This is to help us do further install-time optimizations that we’re experimenting with.
/gems/:name-:version.gem
Finally, when it comes time to install a gem, Bundler downloads a specific gem + version package from this route. If it’s already downloaded and installed, Bundler will skip querying for it.
Wrap-up
We’re thrilled about the encouraging response to our beta, and the trust the Ruby community is showing us. We’d like to keep earning it.
If you’d like to sign up for the beta to publish your own gems to your own namespace, you can do so at https://beta.gem.coop/sign_up
Thank you for trying us out! Please keep the feedback coming. We’ll be here.