# Private modules and the \`-internals\` convention

**URL:** <https://meta.discourse.org/t/private-modules-and-the-internals-convention/414400>\
**Category:** Developer Guides\
**Created:** [10월 9, 2026, 7:23오후 UTC](https://meta.discourse.org/t/private-modules-and-the-internals-convention/414400 "2026-10-09T19:23:12Z")\
**Posts on this page:** 1\
**Page:** 1

## AI-generated summary

The `-internals` convention designates folders containing code that is **not public API**. Code within these folders may be renamed, reshaped, or deleted without notice or deprecation. Plugins and themes must never depend on this code. Only the folder's owner imports from it; other code, including core modules using a primitive, must import the owner's public entry point instead. Since no linter enforces this, the folder name serves as the sole signal.

Placement is determined by ownership:

1. **Single Owner**: If one component or module owns the concept, the `-internals` folder sits inside that owner. Only that owner imports from it.
2. **Shared Mechanism**: If multiple members share a mechanism, the folder goes in the `-internals` of the smallest namespace containing all consumers. The folder is named after the mechanism, and any member of that namespace may import it.
3. **App-Wide**: If consumers span several namespaces, the mechanism belongs in `lib/-internals/<mechanism>/`. This should only be used when no narrower namespace fits.

Users should not nest `-internals` under a namespace-scoped `lib/` (e.g., `form-kit/lib/`), as `lib/` holds flat, supported helpers, and `-internals` already indicates private status. When a shared mechanism emerges from an owner's internals, it should be moved up to the shared namespace rather than importing across. This move should be behavior-neutral, verified by existing tests. Shared mechanism folders keep types next to the code using them, adding a `types.ts` file only when multiple files in the folder read from it.

<div class="post-metadata">

**Author:** ![system](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/system/32/443519_2.png) [@system](https://meta.discourse.org/u/system)\
**Post date:** [10월 9, 2026, 7:23오후 UTC](https://meta.discourse.org/t/private-modules-and-the-internals-convention/414400/1 "2026-10-09T19:23:12Z")

</div>

# What `-internals` means

A folder named `-internals` holds code that is **not public API**. It may be renamed, reshaped or deleted without notice or a deprecation cycle. Plugins and themes must never depend on it.

Only the folder’s owner imports from it. Everything else, including core code that uses a primitive, imports the owner’s public entry instead. The [UI kit guide](https://meta.discourse.org/t/-/411319) states the same rule for the kit’s primitives. Nothing lints this, so the folder name is the only signal.

# Where it goes

Put `-internals` immediately inside the unit that **owns the concept**. Ownership decides placement, not who imports it.

**One named owner.** A component or module owns the concept, so its internals sit inside it, and only that owner imports them:

```plaintext
ui-kit/d-reorderable-list/-internals/ owned by DReorderableList
lib/blocks/-internals/ owned by the blocks module

```

The UI kit guide’s “Splitting a large primitive” section describes how a primitive lays out its own `-internals` folder.

**No single owner.** Several members share the mechanism, so it goes in the `-internals` of the smallest namespace that contains every consumer, under a folder named for the mechanism. The owner is then that namespace, and any member of it may import the folder:

```plaintext
ui-kit/-internals/cursor/ shared by dRovingFocus and DReorderableList,
                                  both ui-kit members
lib/-internals/drag-and-drop/ shared by ui-kit modifiers and a service, so no
                                  namespace narrower than the app contains them

```

`lib/` is the app-wide root, which is why a mechanism whose consumers span several namespaces lands there. Reach for it only when nothing narrower fits. A mechanism used only within `ui-kit` belongs to `ui-kit`.

# Choosing

1. Does one component or module own the concept? Then `<owner>/-internals/`.
2. Otherwise, list every consumer and take the smallest namespace containing all of them. Then `<namespace>/-internals/<mechanism>/`.
3. If that namespace is the app itself, it is `lib/-internals/<mechanism>/`.

Do not nest `-internals` under a namespace-scoped `lib/`. A namespace `lib/` (as in `form-kit/lib/`) holds flat, supported helpers. `-internals` already says the code is private, so the extra level adds nothing.

# Splitting a shared mechanism out

When a second consumer appears for something inside one owner’s `-internals`, move the shared part up rather than importing across. Keep the move behaviour-neutral and let the existing suite prove it. `ItemScope` and the stepping helpers were lifted out of `ui-kit/modifiers/d-roving-focus/` into `ui-kit/-internals/cursor/` this way, with the modifier’s own tests as the gate.

A shared mechanism folder keeps its types next to the code that uses them. Add a `types.ts` only when several files in the folder read from it.

* * *

This document is version controlled - suggest changes [on github](https://github.com/discourse/discourse/blob/main/docs/developer-guides/docs/03-code-internals/32-private-modules.md).
