Skip to content

r.unmanagedTable(meta, { reason })

Use when a feature owns a table that does not fit the r.entity(...) aggregate-lifecycle: no audit-trail, no optimistic-version, no tenant-id base-columns. Typical case: a flat read-side projection of an event-stream (delivery-attempts, job-run logs).

import { defineUnmanagedTable } from "@cosmicdrift/kumiko-framework/db";
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
const deliveryAttemptsTableMeta = defineUnmanagedTable({
tableName: "read_delivery_attempts",
columns: [
{ name: "id", pgType: "uuid", notNull: true, primaryKey: true },
{ name: "tenant_id", pgType: "uuid", notNull: true },
// ...
],
});
createDeliveryFeature = defineFeature("delivery", (r) => {
r.unmanagedTable(deliveryAttemptsTableMeta, {
reason: "read-side log projection of DELIVERY_ATTEMPT events — flat shape, no aggregate lifecycle.",
});
// ...
});

Why declare via r.unmanagedTable instead of top-level export?

Section titled “Why declare via r.unmanagedTable instead of top-level export?”

Because the app-author then needs zero awareness of which features ship unmanaged-tables. The kumiko schema generate CLI iterates the composed feature-set, collects every feature.unmanagedTables entry, and emits the migration — without per-app hand-lists.

Before (manual push in kumiko/schema.ts):

metas.push(deliveryAttemptsTableMeta); // ← framework-internal detail leaks into app
metas.push(jobRunLogsTableMeta);

After:

for (const entry of Object.values(feature.unmanagedTables ?? {})) {
metas.push(entry.meta); // ← features self-declare
}

The reason string justifies the bypass at the declaration site and shows up in audit/ops UIs that list all unmanaged tables. Reviewers can judge legitimacy without spelunking through history. Empty / whitespace reason throws at registration time — if you can’t write a reason, declare data via r.entity() instead.

r.rawTable(name, pgTable, { reason }) is the legacy Drizzle-PgTable variant. Both APIs coexist until Drizzle is fully cut out of the framework; after that the two will likely merge. Pick r.unmanagedTable for new code (uses EntityTableMeta, which the migrate-runner consumes directly).

Two features cannot register the same physical tableNamecreateRegistry throws at boot with Unmanaged-table "..." registered by both feature "..." and "...". Pick a feature-prefixed tableName to disambiguate.