Skip to content

Bundled features vs. custom

Kumiko includes bundled features for common cross-cutting concerns such as identity, audit, secrets, files, notifications, and jobs. They are normal feature definitions that can be added to an app and used through their documented handlers and extension points.

Use a bundled feature when its schema and behaviour fit. Extend it when the available hooks are enough, and replace it when they are not. Business-specific entities such as orders, customers, and products belong in custom features.

ready-made piece --[fits your case]--> use it as-is
ready-made piece --[almost fits]-----> extend it (add a hook, don't rebuild it)
your own business logic -------------> always build 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.

Three things, in order of how often they matter:

  • You reuse a maintained data model. A bundled feature declares the table shape and indexes that app migration generation consumes. Fixes arrive with package upgrades; each app still generates and commits its own migrations.
  • You skip the integration work. audit already hooks into every write through the system-hook layer at priority 1002. secrets already handles the brand-walk in the error serializer to prevent leaks. jobs already does durable BullMQ scheduling, execution logs, 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.

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.job, and build your own.

A common middle ground: keep the bundled feature, layer extensions on top of it. Two extension surfaces are usually all you need.

Registrar extensions. Some bundled features expose named extension points. For example, the tags and custom-fields bundles let an app opt an entity into their behavior with r.useExtension(...). The provider registers the hooks and schema additions during boot. Your app declares the relationship without modifying the bundled feature.

Hooks on bundled entities. A bundled feature’s entities are ordinary entities. Your feature can attach a documented hook to them when it declares the dependency and uses the qualified target required by that bundle.

When neither is enough, a custom implementation can replace a bundle only if it preserves the required feature name, handlers, events, extensions, screens, and data migration contract. Treat that as a compatibility migration, not an automatic swap.

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 shows a system-scoped query over the event log. The secrets feature shows encrypted tenant-secret storage and audited reads. Both use public framework contracts.

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.

Use bundled features for shared infrastructure and custom features for your business model. Extend a bundle before replacing it.