From .vmodule to "Folders Are Modules": the UEFN 23.20 Module Overhaul
Lesson 3 said "making a folder is making a module" — a rule that didn't fall out of the sky. It's the product of a bone-deep system overhaul in UEFN 23.20. Read this slice of history and you'll truly understand why modules default to internal today.
1. The Before Times: Staking Claims with .vmodule Files
Before 23.20, modules in a Verse project had to be defined by a dedicated kind of file, the .vmodule: want a module, drop in a matching .vmodule file — the module's name and boundary were whatever that file said. Folders were just folders, with no inherent tie to modules. The mechanism worked, but every extra file type adds mental load — the directory tree and the module tree were two separate maps, and newcomers constantly failed to line them up.
Epic announced the new scheme in the official post "Verse module hierarchy changes in UEFN 23.20": modules are now defined by directory names — every folder in your project's Verse code automatically becomes a module of the same name; .vmodule files are kept for compatibility but formally enter the deprecation lane. From then on the directory tree is the module tree — "make a folder = make a module" — one map for the whole project. That's where Lesson 3's line "every folder automatically becomes a module of the same name" comes from.
2. The Key Semantic Shift: Default internal and Suddenly-Failing References
The change most easily overlooked — and quickest to bite — is that the default visibility changed:The change most easily overlooked — and quickest to bite — is that the default "who gets to use it" changed — that's the same access setting as Public / Private on Blueprint variables and functions, which Verse calls visibility: the old .vmodule modules defaulted to public, so anyone could reference them; the new directory modules default to <internal> — visible only inside their containing module. So plenty of projects met paranormal activity after upgrading to 23.20: not a line of code changed, yet cross-module references that had run fine suddenly failed with access errors across the board. The cause: those modules had quietly gone from "public by default" to "internal by default".
The official remedy given in the announcement is to pin the visibility down with an explicit module declaration: in a Verse file next to the directory, place a public module expression that declares visibility on the directory module's behalf:
The official remedy is to hand-write one declaration that pins down "who gets to use it": place a .verse file next to the directory containing a small module declaration with a public sign hung on it, stating on the directory module's behalf, "I am open to the outside":
# In a .verse file sitting beside the same-named directory:
# use a module expression to explicitly declare the module public
MyModule<public> := module:
# the rest of the directory's definitions fold into this module automatically
# if the declaration syntax differs between versions, defer to the Verse API Reference
The announcement also listed migration steps for older projects: move Verse files out of the dedicated /Verse folder into Content; delete the .vmodule file named after the project; then rename directories to match module names. Which also explains that pitfall from Lesson 3 — "casually renaming a folder breaks references": the directory name carries the module path, so touching it means touching an API address.
3. Why the Default Tightened: the Principle of Least Visibility
Flipping the default from public to internal looks like short-term compatibility pain, but long-term it's deliberate design: doors closed by default; whatever you want open, you declare explicitly. This is software engineering's "principle of least visibility" — the less a member exposes, the freer you are to change it later; every public is a long-term promise, and the fewer promises you make, the lighter your evolution. Recall the official module definition Lesson 3 quoted — "able to evolve over time without breaking dependents" — default internal exists to serve exactly that sentence: only what nobody depends on can be refactored at will.
So when today's Verse makes you mark both the module and the member <public> for a cross-module reference, the language isn't hazing you — it's forcing you to think it through: is this thing really worth a promise to the outside world?
A project upgrades to UEFN 23.20. Not a line of code changes, yet cross-module references suddenly fail with access errors. What's the root cause?
Sources & Further Reading
Compiled from the official announcement on the Epic developer forums: