The Monorepo Dilemma

Your team uses a monorepo. All Go services live under one Git repository:

monorepo/
├── go/
│   ├── auth/
│   │   └── go.mod          # module github.com/org/monorepo/go/auth
│   ├── config/
│   │   └── go.mod          # module github.com/org/monorepo/go/config
│   └── logger/
│       └── go.mod          # module github.com/org/monorepo/go/logger
├── proto/
├── deploy/
└── README.md

Three Go modules, one repo. This is a common pattern — shared CI, atomic cross-module changes, consistent versioning.

But the import paths are ugly and deeply nested:

import (
    "github.com/org/monorepo/go/auth"
    "github.com/org/monorepo/go/config"
    "github.com/org/monorepo/go/logger"
)

And they’re tied to GitHub. Migrate to GitLab? Rewrite every import.

You want vanity URLs:

import (
    "go.company.com/auth"
    "go.company.com/config"
    "go.company.com/logger"
)

But here’s the catch: traditional go-import meta tags map one import path to one repo. Your monorepo has three modules in one repo. How do you make this work?


Go 1.25 to the Rescue: Subdirectory Modules

Go 1.25 introduced subdirectory module support in the go-import meta tag protocol. The tag now accepts an optional 4th field — the subdirectory path within the repo where the go.mod file lives:

<meta name="go-import" content="go.company.com/auth git https://github.com/org/monorepo go/auth">

This tells the Go toolchain: “The module go.company.com/auth lives in the go/auth subdirectory of https://github.com/org/monorepo.”

When you run go get go.company.com/auth:

  1. Go fetches https://go.company.com/auth?go-get=1
  2. Finds the meta tag above
  3. Clones https://github.com/org/monorepo
  4. Looks for go.mod in go/auth/
  5. Fetches the module from that subdirectory

One repo, multiple modules, clean import paths. Exactly what monorepo teams need.


Setting It Up with gvu

The gvu CLI supports subdirectory modules via the --subdir flag. Here’s the complete workflow.

Step 1: Install gvu

curl -fsSL gomodvanityurls.com/install.sh | bash

Step 2: Add Routes with –subdir

For each module in your monorepo, add a route specifying the subdirectory:

# auth module lives in go/auth/ subdirectory
gvu add go.company.com/auth https://github.com/org/monorepo.git --subdir go/auth

# config module lives in go/config/
gvu add go.company.com/config https://github.com/org/monorepo.git --subdir go/config

# logger module lives in go/logger/
gvu add go.company.com/logger https://github.com/org/monorepo.git --subdir go/logger

Each command creates a vanity URL route with the subdirectory mapping. The server generates the correct 4-field go-import meta tag automatically.

Step 3: Update go.mod in Each Module

Each module’s go.mod must declare the vanity URL as its module path:

// go/auth/go.mod
module go.company.com/auth

go 1.25
// go/config/go.mod
module go.company.com/config

go 1.25
// go/logger/go.mod
module go.company.com/logger

go 1.25

Step 4: Configure GOPRIVATE

go env -w GOPRIVATE=go.company.com

Step 5: Verify

$ go get go.company.com/auth
go: downloading go.company.com/auth v0.1.0
go: added go.company.com/auth v0.1.0

All three modules are now importable with clean vanity paths.


Scaling Up: Subdir Wildcard (W2)

Three modules is manageable. What if your monorepo has 20 modules?

monorepo/
├── go/
│   ├── auth/go.mod
│   ├── config/go.mod
│   ├── logger/go.mod
│   ├── cache/go.mod
│   ├── queue/go.mod
│   ├── db/go.mod
│   ├── http/go.mod
│   ├── grpc/go.mod
│   ├── metrics/go.mod
│   ├── tracing/go.mod
│   ├── featureflag/go.mod
│   ├── ratelimit/go.mod
│   ├── retry/go.mod
│   ├── circuitbreaker/go.mod
│   ├── healthcheck/go.mod
│   ├── ...

Adding 20 routes individually is tedious. gvu’s subdir wildcard (W2) handles this with a single rule:

gvu add go.company.com/libs https://github.com/org/monorepo.git '--subdir=*'

This single rule auto-matches any subdirectory:

Import pathResolved to
go.company.com/libs/authgithub.com/org/monorepo subdir go/auth
go.company.com/libs/configgithub.com/org/monorepo subdir go/config
go.company.com/libs/loggergithub.com/org/monorepo subdir go/logger

Add a new module to the monorepo? It’s automatically importable — no route changes needed.

Note: Subdir wildcard requires a Pro account and a custom domain. See Wildcard Routing for details.


Monorepo Best Practices

1. Use a consistent subdirectory convention

Pick one structure and stick with it across all monorepos:

# Option A: go/<module>/
monorepo/go/auth/go.mod
monorepo/go/config/go.mod

# Option B: modules/<module>/
monorepo/modules/auth/go.mod
monorepo/modules/config/go.mod

# Option C: services/<module>/
monorepo/services/auth/go.mod

Consistency makes --subdir patterns predictable and wildcard rules simpler.

2. Tag versions per-module

Go modules in a monorepo need module-specific tags:

# Tag auth v1.0.0
git tag go/auth/v1.0.0

# Tag config v0.3.0
git tag go/config/v0.3.0

The tag prefix must match the subdirectory path. Go uses these tags to determine module versions.

3. Keep go.mod minimal

Each submodule’s go.mod should only declare dependencies it actually uses. Avoid importing sibling modules unless necessary — this keeps the dependency graph clean.

4. Use replace directives for local development

When developing across modules in the same monorepo, use replace directives:

// go/auth/go.mod
module go.company.com/auth

go 1.25

require go.company.com/config v0.3.0

replace go.company.com/config => ../config

This lets you test changes locally without pushing tags.

5. Cross-module changes in one commit

The biggest monorepo advantage: atomic cross-module changes.

# Change config API, update auth to use new API, all in one commit
git add go/config/ go/auth/
git commit -m "feat(config): add Validate(); update auth to use it"
git tag go/config/v0.4.0
git tag go/auth/v1.1.0
git push --tags

No version coordination nightmares. No “publish config first, then update auth” dance.


Before and After

AspectBefore (raw GitHub paths)After (vanity URLs + subdir)
Import pathgithub.com/org/monorepo/go/authgo.company.com/auth
Repo migrationRewrite all import pathsUpdate route target URL
New moduleAdd to repo + update all consumersAdd to repo + one gvu add
BrandingLooks like a subdirectoryLooks like a product
IDE supportDeep nested pathsShort, clear paths

Summary

Go 1.25’s subdirectory module support + gvu’s --subdir flag is the cleanest way to manage multi-module monorepos with vanity URLs:

  • One repo, multiple independently versioned Go modules
  • Clean import paths that don’t expose repo structure
  • Platform independence — switch Git hosts without rewriting imports
  • Subdir wildcard — one rule covers all current and future modules
# Single module
gvu add go.company.com/auth https://github.com/org/monorepo.git --subdir go/auth

# All modules at once (Pro)
gvu add go.company.com/libs https://github.com/org/monorepo.git '--subdir=*'

Try gvu — free for up to 5 routes. Your monorepo deserves clean import paths.