Bundled features vs. custom
Kumiko ships a set of bundled features — pre-built feature definitions
that solve the problems most apps need solved before they get to the
business-specific work. Identity, sessions, audit trails, secrets, file
storage, notifications, jobs, rate limiting, feature toggles. They live
in packages/bundled-features/src/<name>/ and you bring them into your
app like any other feature.
import { createAuditFeature } from "@cosmicdrift/kumiko-bundled-features/audit";import { createSecretsFeature } from "@cosmicdrift/kumiko-bundled-features/secrets";
await runDevApp({ features: [ createAuditFeature(), createSecretsFeature(), myAppFeature, ], auth: { /* enables config + user + tenant + auth-email-password */ },});A bundled feature is not a magic library. It is a feature definition,
the same shape your own features have. It declares entities, registers
handlers, hooks into other entities, exports translations. Other features
can r.requires("audit") to depend on it; if you remove it from your
features array, the dependency check at boot tells you which feature
needs it.
What “bundled” buys you
Section titled “What “bundled” buys you”Three things, in order of how often they matter:
- You skip the schema work. A bundled feature comes with the right table layout, the right indexes, the right migration history. Replicating that yourself is a week of work that ends with a worse audit trail than the one you could have used.
- You skip the integration work.
auditalready hooks into every write through the system-hook layer at priority 1002.secretsalready handles the brand-walk in the error serializer to prevent leaks.jobsalready does leader election, heartbeat, and retry. None of those are obvious if you build them fresh. - You inherit upgrades. Bundled features get patched when the framework finds a problem. A custom audit table you wrote eighteen months ago does not.
The cost is that the schema is what it is. If your audit needs to capture an extra dimension that the bundled feature does not — a tenant-specific classification, an external compliance ID — you cannot just add a column.
What “custom” buys you
Section titled “What “custom” buys you”The freedom to design exactly the schema you need, without the bundled
feature’s migration history or its conventions. For business-specific
things — your incidents, your invoices, your inventory — this is
the right answer almost without thinking. Bundled features do not have a
business-domain version of themselves; the framework does not ship an
orders feature.
For cross-cutting concerns — auth, audit, secrets, files, jobs — going
custom is the rare answer. The bundled features are designed for the 90 %
case. If your case is the 10 % that genuinely differs (a regulated
industry’s specific audit format, a pre-existing identity provider you
must integrate with, a file storage scheme that must match a legacy
contract), you take the framework’s primitives — r.entity, r.hook,
r.scheduledJob — and build your own.
Extending a bundled feature
Section titled “Extending a bundled feature”A common middle ground: keep the bundled feature, layer extensions on top of it. Two extension surfaces are usually all you need.
Registrar extensions. Many bundled features expose r.extendsRegistrar
slots. The audit feature, for example, exposes r.audited("entity") —
your own feature opts in for an entity, and audit’s hooks attach to that
entity’s writes automatically. You did not modify the audit feature; you
declared a relationship.
Hooks on the bundled entities. A bundled feature’s entities are
ordinary entities, and your feature can hook into them. r.hook("postSave", "audit-entry", …) runs on every audit write, lets you push it to an
external SIEM, and does not require any change to the audit feature
itself.
When neither of those is enough, you replace the bundled feature with a
custom one. Replacement is uneventful: the dependent features
r.requires("audit") against the role, not the implementation. As long
as your replacement registers under the same name and provides the same
handlers, the rest of the app does not notice.
The split is not “framework vs. user code”
Section titled “The split is not “framework vs. user code””A bundled feature is in the framework repo, but it has no privileged access. It cannot do anything your code cannot do, and it does not bypass any of the rules — boot validation, tenant isolation, access checks all apply to it the same way. The author of a bundled feature reads the same documentation you do.
This matters when you read bundled-feature source code as a reference.
The audit feature’s source is a worked example of how to attach to
many entities through registrar extensions. The secrets feature’s
source is a worked example of how to use the brand-walk to prevent
sensitive data from leaking through error responses. Both are normal
features; nothing they do is reserved.
The decision rule
Section titled “The decision rule”When you need a cross-cutting capability — identity, audit, secrets, files, jobs, notifications — start with the bundled feature. The defaults are battle-tested, the integrations are correct, and the upgrade path is real. Replace it only when you have measured a specific mismatch with your domain.
When you need business behaviour — orders, incidents, customers, inventory — always go custom. There is no bundled feature for your business, and there is no point pretending otherwise.
When you are in between — say, an existing identity system you must integrate with — layer on top before forking off. Extend the bundled feature first, replace it only if the extension surface does not stretch that far.
The bundled features are scaffolding. Use them where they fit; rebuild them where they don’t; ignore them where they are not the question.
See also
Section titled “See also”- Features and composition — bundled features are normal feature definitions and follow the same rules.
- Inline authoring — why bundled features get an exemption from the inline-only contract.