@minion-stack/db 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/pg/schema/auth.d.ts +1591 -0
  2. package/dist/pg/schema/auth.d.ts.map +1 -0
  3. package/dist/pg/schema/auth.js +160 -0
  4. package/dist/pg/schema/auth.js.map +1 -0
  5. package/dist/pg/schema/builder.d.ts +1 -1
  6. package/dist/pg/schema/builder.d.ts.map +1 -1
  7. package/dist/pg/schema/builder.js +3 -1
  8. package/dist/pg/schema/builder.js.map +1 -1
  9. package/dist/pg/schema/channels.d.ts +336 -0
  10. package/dist/pg/schema/channels.d.ts.map +1 -1
  11. package/dist/pg/schema/channels.js +45 -1
  12. package/dist/pg/schema/channels.js.map +1 -1
  13. package/dist/pg/schema/gateway.d.ts +17 -0
  14. package/dist/pg/schema/gateway.d.ts.map +1 -1
  15. package/dist/pg/schema/gateway.js +4 -0
  16. package/dist/pg/schema/gateway.js.map +1 -1
  17. package/dist/pg/schema/index.d.ts +2 -1
  18. package/dist/pg/schema/index.d.ts.map +1 -1
  19. package/dist/pg/schema/index.js +5 -1
  20. package/dist/pg/schema/index.js.map +1 -1
  21. package/dist/pg/schema/missions.d.ts +2 -2
  22. package/dist/pg/schema/personal-agents.d.ts +1 -1
  23. package/dist/pg/schema/server-ops.d.ts +1 -1
  24. package/dist/pg/schema/sessions.d.ts +1 -1
  25. package/dist/schema/bugs.d.ts.map +1 -1
  26. package/dist/schema/bugs.js +7 -1
  27. package/dist/schema/bugs.js.map +1 -1
  28. package/dist/schema/connection-events.js +5 -4
  29. package/dist/schema/connection-events.js.map +1 -1
  30. package/dist/schema/index.d.ts +0 -1
  31. package/dist/schema/index.d.ts.map +1 -1
  32. package/dist/schema/index.js +0 -1
  33. package/dist/schema/index.js.map +1 -1
  34. package/dist/schema/personal-agents.d.ts +1 -1
  35. package/dist/schema/reliability-events.d.ts +2 -2
  36. package/dist/schema/reliability-events.d.ts.map +1 -1
  37. package/dist/schema/reliability-events.js +6 -6
  38. package/dist/schema/reliability-events.js.map +1 -1
  39. package/dist/schema/skill-execution-stats.d.ts +1 -1
  40. package/dist/schema/unified-events.d.ts.map +1 -1
  41. package/dist/schema/unified-events.js +3 -0
  42. package/dist/schema/unified-events.js.map +1 -1
  43. package/package.json +12 -11
  44. package/src/pg/schema/auth.ts +198 -0
  45. package/src/pg/schema/builder.ts +3 -1
  46. package/src/pg/schema/channels.ts +62 -1
  47. package/src/pg/schema/gateway.ts +4 -0
  48. package/src/pg/schema/index.ts +22 -1
  49. package/src/schema/bugs.ts +7 -1
  50. package/src/schema/connection-events.ts +4 -4
  51. package/src/schema/index.ts +0 -1
  52. package/src/schema/reliability-events.ts +5 -6
  53. package/src/schema/unified-events.ts +3 -0
  54. package/src/schema/flows.ts +0 -14
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Better Auth schema tables — Postgres variant.
3
+ *
4
+ * Faithful 1:1 port of `../../schema/auth/index.ts` (sqlite) for the Turso→Supabase
5
+ * Better Auth cutover (Stage 5 / Track B). Export NAMES must match Better Auth's
6
+ * model names (`user`, `session`, …) because the auth factory passes this module
7
+ * straight to the drizzle adapter. Ids stay `text` (Better Auth generates string
8
+ * ids — keeping them text preserves existing ids across the store migration, so
9
+ * `profiles.legacy_user_id` keeps mapping). sqlite `integer{mode:timestamp}` →
10
+ * `timestamptz`; `integer{mode:boolean}` → `boolean`.
11
+ *
12
+ * Provider: pg, plugins: emailAndPassword, google OAuth, jwt, organization, oidc.
13
+ */
14
+ import { pgTable, text, timestamp, boolean, index } from 'drizzle-orm/pg-core';
15
+
16
+ // ── Core: user ──────────────────────────────────────────────────────────────
17
+ export const user = pgTable('user', {
18
+ id: text('id').primaryKey(),
19
+ name: text('name').notNull(),
20
+ email: text('email').notNull().unique(),
21
+ emailVerified: boolean('email_verified').notNull(),
22
+ image: text('image'),
23
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull(),
24
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull(),
25
+ role: text('role', { enum: ['user', 'admin'] })
26
+ .notNull()
27
+ .default('user'),
28
+ personalAgentId: text('personal_agent_id'),
29
+ });
30
+
31
+ // ── Core: session ────────────────────────────────────────────────────────────
32
+ export const session = pgTable(
33
+ 'session',
34
+ {
35
+ id: text('id').primaryKey(),
36
+ expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
37
+ token: text('token').notNull().unique(),
38
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull(),
39
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull(),
40
+ ipAddress: text('ip_address'),
41
+ userAgent: text('user_agent'),
42
+ userId: text('user_id')
43
+ .notNull()
44
+ .references(() => user.id, { onDelete: 'cascade' }),
45
+ // Added by organization plugin
46
+ activeOrganizationId: text('active_organization_id'),
47
+ },
48
+ (t) => [index('idx_session_user').on(t.userId)],
49
+ );
50
+
51
+ // ── Core: account ────────────────────────────────────────────────────────────
52
+ export const account = pgTable(
53
+ 'account',
54
+ {
55
+ id: text('id').primaryKey(),
56
+ accountId: text('account_id').notNull(),
57
+ providerId: text('provider_id').notNull(),
58
+ userId: text('user_id')
59
+ .notNull()
60
+ .references(() => user.id, { onDelete: 'cascade' }),
61
+ accessToken: text('access_token'),
62
+ refreshToken: text('refresh_token'),
63
+ idToken: text('id_token'),
64
+ accessTokenExpiresAt: timestamp('access_token_expires_at', { withTimezone: true }),
65
+ refreshTokenExpiresAt: timestamp('refresh_token_expires_at', { withTimezone: true }),
66
+ scope: text('scope'),
67
+ password: text('password'),
68
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull(),
69
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull(),
70
+ },
71
+ (t) => [index('idx_account_user').on(t.userId)],
72
+ );
73
+
74
+ // ── Core: verification ────────────────────────────────────────────────────────
75
+ export const verification = pgTable(
76
+ 'verification',
77
+ {
78
+ id: text('id').primaryKey(),
79
+ identifier: text('identifier').notNull(),
80
+ value: text('value').notNull(),
81
+ expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
82
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull(),
83
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull(),
84
+ },
85
+ (t) => [index('idx_verification_identifier').on(t.identifier)],
86
+ );
87
+
88
+ // ── JWT plugin: jwks ─────────────────────────────────────────────────────────
89
+ export const jwks = pgTable('jwks', {
90
+ id: text('id').primaryKey(),
91
+ publicKey: text('public_key').notNull(),
92
+ privateKey: text('private_key').notNull(),
93
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull(),
94
+ });
95
+
96
+ // ── Organization plugin: organization ────────────────────────────────────────
97
+ export const organization = pgTable('organization', {
98
+ id: text('id').primaryKey(),
99
+ name: text('name').notNull(),
100
+ slug: text('slug').unique(),
101
+ logo: text('logo'),
102
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull(),
103
+ metadata: text('metadata'),
104
+ });
105
+
106
+ // ── Organization plugin: member ───────────────────────────────────────────────
107
+ export const member = pgTable(
108
+ 'member',
109
+ {
110
+ id: text('id').primaryKey(),
111
+ organizationId: text('organization_id')
112
+ .notNull()
113
+ .references(() => organization.id, { onDelete: 'cascade' }),
114
+ userId: text('user_id')
115
+ .notNull()
116
+ .references(() => user.id, { onDelete: 'cascade' }),
117
+ role: text('role').notNull(),
118
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull(),
119
+ },
120
+ (t) => [index('idx_member_org').on(t.organizationId), index('idx_member_user').on(t.userId)],
121
+ );
122
+
123
+ // ── Organization plugin: invitation ──────────────────────────────────────────
124
+ export const invitation = pgTable(
125
+ 'invitation',
126
+ {
127
+ id: text('id').primaryKey(),
128
+ organizationId: text('organization_id')
129
+ .notNull()
130
+ .references(() => organization.id, { onDelete: 'cascade' }),
131
+ email: text('email').notNull(),
132
+ role: text('role'),
133
+ status: text('status').notNull(),
134
+ expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
135
+ inviterId: text('inviter_id')
136
+ .notNull()
137
+ .references(() => user.id, { onDelete: 'cascade' }),
138
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull(),
139
+ },
140
+ (t) => [index('idx_invitation_org').on(t.organizationId)],
141
+ );
142
+
143
+ // ── OIDC provider plugin: oauthApplication ──────────────────────────────────
144
+ export const oauthApplication = pgTable('oauth_application', {
145
+ id: text('id').primaryKey(),
146
+ name: text('name').notNull(),
147
+ icon: text('icon'),
148
+ metadata: text('metadata'),
149
+ clientId: text('client_id').notNull().unique(),
150
+ clientSecret: text('client_secret'),
151
+ redirectUrls: text('redirect_urls').notNull(),
152
+ type: text('type').notNull(),
153
+ disabled: boolean('disabled').default(false),
154
+ userId: text('user_id').references(() => user.id, { onDelete: 'cascade' }),
155
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull(),
156
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull(),
157
+ });
158
+
159
+ // ── OIDC provider plugin: oauthAccessToken ──────────────────────────────────
160
+ export const oauthAccessToken = pgTable(
161
+ 'oauth_access_token',
162
+ {
163
+ id: text('id').primaryKey(),
164
+ accessToken: text('access_token').notNull().unique(),
165
+ refreshToken: text('refresh_token').notNull().unique(),
166
+ accessTokenExpiresAt: timestamp('access_token_expires_at', { withTimezone: true }).notNull(),
167
+ refreshTokenExpiresAt: timestamp('refresh_token_expires_at', { withTimezone: true }).notNull(),
168
+ clientId: text('client_id').notNull(),
169
+ userId: text('user_id').references(() => user.id, { onDelete: 'cascade' }),
170
+ scopes: text('scopes').notNull(),
171
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull(),
172
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull(),
173
+ },
174
+ (t) => [
175
+ index('idx_oauth_access_token_client').on(t.clientId),
176
+ index('idx_oauth_access_token_user').on(t.userId),
177
+ ],
178
+ );
179
+
180
+ // ── OIDC provider plugin: oauthConsent ──────────────────────────────────────
181
+ export const oauthConsent = pgTable(
182
+ 'oauth_consent',
183
+ {
184
+ id: text('id').primaryKey(),
185
+ clientId: text('client_id').notNull(),
186
+ userId: text('user_id')
187
+ .notNull()
188
+ .references(() => user.id, { onDelete: 'cascade' }),
189
+ scopes: text('scopes').notNull(),
190
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull(),
191
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull(),
192
+ consentGiven: boolean('consent_given').notNull(),
193
+ },
194
+ (t) => [
195
+ index('idx_oauth_consent_client').on(t.clientId),
196
+ index('idx_oauth_consent_user').on(t.userId),
197
+ ],
198
+ );
@@ -123,7 +123,9 @@ export const builtAgents = pgTable(
123
123
  .notNull()
124
124
  .default('draft'),
125
125
  gatewayId: uuid('gateway_id').references(() => gateway.id, { onDelete: 'cascade' }),
126
- tenantId: uuid('tenant_id'),
126
+ // Org-owned (Phase 4: no global drafts). Matches the NOT NULL constraint in
127
+ // 20260606224238_built_agents_tenant_not_null.sql.
128
+ tenantId: uuid('tenant_id').notNull(),
127
129
  createdBy: text('created_by'),
128
130
  publishedAt: timestamp('published_at', { withTimezone: true }),
129
131
  createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
@@ -1,4 +1,14 @@
1
- import { pgTable, uuid, text, timestamp, index, uniqueIndex } from 'drizzle-orm/pg-core';
1
+ import {
2
+ pgTable,
3
+ uuid,
4
+ text,
5
+ timestamp,
6
+ boolean,
7
+ integer,
8
+ index,
9
+ uniqueIndex,
10
+ } from 'drizzle-orm/pg-core';
11
+ import { sql } from 'drizzle-orm';
2
12
  import { gateway } from './gateway.js';
3
13
  import { profiles } from './profiles.js';
4
14
 
@@ -19,19 +29,70 @@ export const channels = pgTable(
19
29
  .notNull()
20
30
  .references(() => gateway.id, { onDelete: 'cascade' }),
21
31
  type: text('type', { enum: ['discord', 'whatsapp', 'telegram'] }).notNull(),
32
+ // Gateway account key (phone/handle, e.g. '+51906090526') — the join to the
33
+ // gateway's per-account config. Nullable for legacy rows; the natural upsert
34
+ // key for a linked account is (tenant_id, gateway_id, type, account_id).
35
+ accountId: text('account_id'),
22
36
  label: text('label').notNull(),
23
37
  credentials: text('credentials').notNull().default(''),
24
38
  credentialsIv: text('credentials_iv').notNull().default(''),
25
39
  credentialsMeta: text('credentials_meta').notNull().default('{}'),
40
+ // Observed coarse status (kept for existing consumers / hub badge).
26
41
  status: text('status', { enum: ['active', 'inactive', 'pairing'] })
27
42
  .notNull()
28
43
  .default('inactive'),
44
+
45
+ // --- Intent (user-configured rules; one concern per column). See
46
+ // specs/2026-06-19-linked-channels-config-restructure.md ---
47
+ // Should the gateway hold a live session (runtime enable/disable).
48
+ enabled: boolean('enabled').notNull().default(true),
49
+ // Reply behavior. 'none' = noAgent (never reply, even owner/self); 'bound' =
50
+ // reply only where a channel_bindings row matches. No 'auto' on purpose.
51
+ replies: text('replies', { enum: ['none', 'bound'] }).notNull().default('none'),
52
+ // DM sender access gate (consulted only when replies='bound'). [] = nobody,
53
+ // ['*'] = anyone. Replaces the old dm_policy enum (derivable from this).
54
+ allowFrom: text('allow_from').array().notNull().default(sql`'{}'`),
55
+ groupAllowFrom: text('group_allow_from').array().notNull().default(sql`'{}'`),
56
+ // Groups reply only when @-mentioned.
57
+ requireMention: boolean('require_mention').notNull().default(true),
58
+
59
+ // --- Observed (gateway-reported, read-only) ---
60
+ reconnectCount: integer('reconnect_count').notNull().default(0),
61
+ lastSeenAt: timestamp('last_seen_at', { withTimezone: true }),
62
+ lastError: text('last_error'),
63
+
29
64
  createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
30
65
  updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
31
66
  },
32
67
  (t) => [
33
68
  index('idx_channels_tenant_gateway').on(t.tenantId, t.gatewayId),
34
69
  uniqueIndex('channels_uniq_type_label').on(t.tenantId, t.gatewayId, t.type, t.label),
70
+ // Upsert key for gateway-account sync (account_id is the gateway account key).
71
+ uniqueIndex('channels_uniq_type_account').on(t.tenantId, t.gatewayId, t.type, t.accountId),
72
+ ],
73
+ );
74
+
75
+ /**
76
+ * Agent routing for a channel. A channel with NO rows here resolves to no agent
77
+ * (receive-only). agent_id NULL on a row is an explicit noAgent binding. Match
78
+ * specificity orders resolution: dm_peer > group > catchall (no priority column).
79
+ */
80
+ export const channelBindings = pgTable(
81
+ 'channel_bindings',
82
+ {
83
+ id: uuid('id').primaryKey().defaultRandom(),
84
+ tenantId: uuid('tenant_id').notNull(),
85
+ channelId: text('channel_id')
86
+ .notNull()
87
+ .references(() => channels.id, { onDelete: 'cascade' }),
88
+ matchKind: text('match_kind', { enum: ['catchall', 'dm_peer', 'group'] }).notNull(),
89
+ matchPeer: text('match_peer'),
90
+ agentId: text('agent_id'), // null = explicit noAgent
91
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
92
+ },
93
+ (t) => [
94
+ index('idx_channel_bindings_channel').on(t.channelId),
95
+ uniqueIndex('channel_bindings_uniq').on(t.channelId, t.matchKind, t.matchPeer),
35
96
  ],
36
97
  );
37
98
 
@@ -9,6 +9,10 @@ import { profiles } from './profiles.js';
9
9
  export const gateway = pgTable('gateway', {
10
10
  id: uuid('id').primaryKey().defaultRandom(),
11
11
  legacyServerId: text('legacy_server_id'),
12
+ // Owning org (soft ref to organizations.id). Used by server-token ingest auth
13
+ // (resolveServerTokenAuth) to resolve the tenant — the Turso `servers.tenant_id`
14
+ // equivalent. Nullable during the Turso→Supabase gateway-token cutover bake.
15
+ orgId: uuid('org_id'),
12
16
  name: text('name').notNull(),
13
17
  url: text('url').notNull(),
14
18
  tokenCiphertext: text('token_ciphertext').notNull().default(''),
@@ -34,7 +34,12 @@ export {
34
34
  agentBuiltSkills,
35
35
  builtTools,
36
36
  } from './builder.js';
37
- export { channels, channelAssignments, channelIdentities } from './channels.js';
37
+ export {
38
+ channels,
39
+ channelAssignments,
40
+ channelIdentities,
41
+ channelBindings,
42
+ } from './channels.js';
38
43
  export { sessions, sessionTasks } from './sessions.js';
39
44
  export { missions, tasks } from './missions.js';
40
45
  export { chatMessages } from './chat-messages.js';
@@ -50,3 +55,19 @@ export { workspaceMembership } from './workspace-membership.js';
50
55
  export type { WorkspaceMembership, NewWorkspaceMembership } from './workspace-membership.js';
51
56
  // Org-scoped agent memory corpus (pgvector) — RAG retrieval + hub visualization.
52
57
  export { agentMemories } from './agent-memories.js';
58
+ // Better Auth tables (Postgres) — Turso→Supabase Better Auth cutover (Track B).
59
+ // Export names mirror Better Auth's model names so the auth factory can pass
60
+ // this module straight to the drizzle adapter (provider: 'pg').
61
+ export {
62
+ user,
63
+ session,
64
+ account,
65
+ verification,
66
+ jwks,
67
+ organization,
68
+ member,
69
+ invitation,
70
+ oauthApplication,
71
+ oauthAccessToken,
72
+ oauthConsent,
73
+ } from './auth.js';
@@ -26,5 +26,11 @@ export const bugs = sqliteTable(
26
26
  createdAt: integer('created_at').notNull(),
27
27
  updatedAt: integer('updated_at').notNull(),
28
28
  },
29
- (t) => [index('idx_bugs_tenant').on(t.tenantId), index('idx_bugs_server').on(t.serverId)],
29
+ (t) => [
30
+ index('idx_bugs_tenant').on(t.tenantId),
31
+ index('idx_bugs_server').on(t.serverId),
32
+ // Covers the tenant-scoped bug list (bug.service.ts listBugs): tenant_id,
33
+ // ordered by created_at desc.
34
+ index('idx_bugs_tenant_created').on(t.tenantId, t.createdAt),
35
+ ],
30
36
  );
@@ -19,8 +19,8 @@ export const connectionEvents = sqliteTable(
19
19
  reason: text('reason'),
20
20
  occurredAt: integer('occurred_at').notNull(),
21
21
  },
22
- (t) => [
23
- index('idx_conn_events_tenant').on(t.tenantId),
24
- index('idx_conn_events_server').on(t.serverId),
25
- ],
22
+ // No service reads or writes this table. The server index was pure write-tax for
23
+ // a query pattern that never runs — dropped. The tenant index is retained so the
24
+ // organization onDelete: cascade stays cheap.
25
+ (t) => [index('idx_conn_events_tenant').on(t.tenantId)],
26
26
  );
@@ -33,7 +33,6 @@ export { marketplaceAgents } from './marketplace-agents.js';
33
33
  export { marketplaceInstalls } from './marketplace-installs.js';
34
34
  export { workshopSaves } from './workshop-saves.js';
35
35
  export { deviceIdentities } from './device-identities.js';
36
- export { flows } from './flows.js';
37
36
  export { userServers } from './user-servers.js';
38
37
  export { userAgents } from './user-agents.js';
39
38
  export { channels } from './channels.js';
@@ -23,10 +23,9 @@ export const reliabilityEvents = sqliteTable(
23
23
  occurredAt: integer('occurred_at').notNull(),
24
24
  createdAt: integer('created_at').notNull(),
25
25
  },
26
- (t) => [
27
- index('idx_rel_events_server_cat_time').on(t.serverId, t.category, t.occurredAt),
28
- index('idx_rel_events_server_time').on(t.serverId, t.occurredAt),
29
- index('idx_rel_events_server_sev_time').on(t.serverId, t.severity, t.occurredAt),
30
- index('idx_rel_events_tenant').on(t.tenantId),
31
- ],
26
+ // No service reads or writes this table (the reliability dashboard streams live
27
+ // data over WS from the gateway, not from the DB). The three server_* composite
28
+ // indexes were pure write-tax for query patterns that never run — dropped. The
29
+ // tenant index is retained so the organization onDelete: cascade stays cheap.
30
+ (t) => [index('idx_rel_events_tenant').on(t.tenantId)],
32
31
  );
@@ -27,6 +27,9 @@ export const unifiedEvents = sqliteTable(
27
27
  index('idx_unified_events_tenant').on(t.tenantId),
28
28
  index('idx_unified_events_server_cat_time').on(t.serverId, t.category, t.occurredAt),
29
29
  index('idx_unified_events_server_time').on(t.serverId, t.occurredAt),
30
+ // Covers the severity-filtered event list (events.service.ts listEvents):
31
+ // server_id + severity, ordered by occurred_at desc.
32
+ index('idx_unified_events_server_sev_time').on(t.serverId, t.severity, t.occurredAt),
30
33
  index('idx_unified_events_correlation').on(t.correlationId),
31
34
  uniqueIndex('idx_unified_events_dedup').on(t.tenantId, t.serverId, t.localEventId),
32
35
  ],
@@ -1,14 +0,0 @@
1
- import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
2
-
3
- export const flows = sqliteTable('flows', {
4
- id: text('id').primaryKey(),
5
- name: text('name').notNull(),
6
- nodes: text('nodes').notNull().default('[]'), // JSON string of FlowNode[]
7
- edges: text('edges').notNull().default('[]'), // JSON string of FlowEdge[]
8
- userId: text('user_id'), // owner — null for pre-migration rows (treated as shared)
9
- tenantId: text('tenant_id'), // tenant scope — null for pre-migration rows
10
- createdAt: integer('created_at').notNull(),
11
- updatedAt: integer('updated_at').notNull(),
12
- active: integer('active', { mode: 'boolean' }).notNull().default(false),
13
- config: text('config').notNull().default('{}'),
14
- });