cvx-kit 0.1.15 → 0.1.17

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 (58) hide show
  1. package/README.md +67 -40
  2. package/dist/querying/filters.d.mts +10 -1
  3. package/dist/querying/filters.mjs +1 -1
  4. package/dist/querying/filters.mjs.map +1 -1
  5. package/dist/querying/planner.d.mts +1 -0
  6. package/dist/querying/planner.mjs +1 -1
  7. package/dist/querying/planner.mjs.map +1 -1
  8. package/dist/querying/relationships.d.mts +1 -1
  9. package/dist/querying/relationships.mjs +1 -1
  10. package/dist/querying/relationships.mjs.map +1 -1
  11. package/dist/querying.d.mts +9 -8
  12. package/dist/querying.mjs.map +1 -1
  13. package/dist/relations-public.d.mts +2 -2
  14. package/dist/relations.d.mts +74 -25
  15. package/dist/relations.mjs +1 -1
  16. package/dist/relations.mjs.map +1 -1
  17. package/dist/zod-table.d.mts +59 -2
  18. package/dist/zod-table.mjs +1 -1
  19. package/dist/zod-table.mjs.map +1 -1
  20. package/package.json +12 -3
  21. package/AGENTS.md +0 -47
  22. package/CHANGELOG.md +0 -337
  23. package/ROADMAP.md +0 -126
  24. package/docs/agent-tools.md +0 -41
  25. package/docs/approvals.md +0 -201
  26. package/docs/architecture.md +0 -124
  27. package/docs/auth.md +0 -443
  28. package/docs/command-context.md +0 -150
  29. package/docs/commands-legacy.md +0 -513
  30. package/docs/commands.md +0 -173
  31. package/docs/conventions.md +0 -345
  32. package/docs/crud.md +0 -66
  33. package/docs/domain-contracts.md +0 -148
  34. package/docs/domain-operations.md +0 -615
  35. package/docs/effect.md +0 -403
  36. package/docs/llms.md +0 -324
  37. package/docs/maintainability.md +0 -124
  38. package/docs/migration.md +0 -351
  39. package/docs/oxlint.md +0 -260
  40. package/docs/querying.md +0 -329
  41. package/docs/tenancy.md +0 -176
  42. package/docs/triggers.md +0 -77
  43. package/docs/upgrading.md +0 -287
  44. package/docs/webhooks.md +0 -58
  45. package/docs/zod-table.md +0 -278
  46. package/scripts/convex-config-bridge.mjs +0 -44
  47. package/scripts/convex-smoke-env.mjs +0 -33
  48. package/scripts/extract-changelog.mjs +0 -29
  49. package/scripts/owned-process.mjs +0 -140
  50. package/scripts/owned-process.test.mjs +0 -107
  51. package/scripts/smoke-packed-components.mjs +0 -341
  52. package/scripts/smoke-packed-effect.mjs +0 -439
  53. package/scripts/smoke-packed-querying.mjs +0 -298
  54. package/scripts/test-packed-auth.mjs +0 -72
  55. package/scripts/test-packed-effect.mjs +0 -482
  56. package/scripts/test-packed-ids.mjs +0 -150
  57. package/scripts/test-packed-oxlint.mjs +0 -167
  58. package/scripts/test-packed-querying.mjs +0 -185
package/docs/llms.md DELETED
@@ -1,324 +0,0 @@
1
- # cvx-kit — condensed reference for LLMs and coding agents
2
-
3
- > Paste this file (installed at `node_modules/cvx-kit/docs/`) into an agent's
4
- > context when it works on a cvx-kit application. Detailed docs:
5
- > `architecture.md`, `conventions.md`, `zod-table.md`, `querying.md`, `auth.md`,
6
- > `commands.md`, `effect.md`, `domain-operations.md`, `triggers.md`, `approvals.md`, `maintainability.md`,
7
- > `migration.md` (same directory).
8
- > Structure, file anatomy, and naming rules → `conventions.md` is
9
- > authoritative. Restructuring an existing raw project → `migration.md`.
10
- > Upgrading from 0.0.x → `upgrading.md`. New 0.1.0 helpers: `crud.md`,
11
- > `webhooks.md`, `agent-tools.md`; stream/relationship pagination in `querying.md`,
12
- > table DTO and pagination helpers in `zod-table.md`.
13
- > Consumer lint configuration and coverage → `oxlint.md`. The optional
14
- > `cvx-kit/oxlint` plugin checks the statically detectable conventions;
15
- > behavioral guarantees still need architecture and runtime tests.
16
-
17
- > **Placeholders.** Names like `documents`, `users`, `history`, `title`,
18
- > `ownerId`, and `documentPublish` in the examples below are illustrative
19
- > only — replace them with the application's own entities and fields. No
20
- > specific table, entity, field, or workflow is required by the kit. Names in
21
- > `<angle brackets>` are always placeholders.
22
-
23
- cvx-kit is a reusable Convex application kit: zod table boundaries, auth-aware
24
- function constructors, a trigger registry, an audited command protocol
25
- (Foundation component), and a declarative approvals component. Peer deps:
26
- `convex ^1.46.0`, `zod ^4.5.4`; command implementations also install
27
- `effect >=4.0.1 <5` and import `cvx-kit/effect`.
28
-
29
- **Command API selection:** use `createEffectFoundation` and declare handlers
30
- inside `command({...})`. The older `Foundation.Command` Promise facade is
31
- deprecated. Its compatibility examples live in `commands-legacy.md`; do not
32
- copy that API into new implementations.
33
-
34
- ## Non-negotiable rules
35
-
36
- 1. **One `zodTable` per entity**, in `domain/<entity>/schema.ts`. Never write
37
- `defineTable` inline or a second zod object for the same entity.
38
- 2. **Raw `query`/`mutation`/`action`/`internal*` builders appear only in
39
- `convex/functions.ts`** (the `createAuthFunctions` call). All other
40
- functions use `authQuery/authMutation/authAction`, `roleQuery/...`,
41
- `adminQuery/...` (public) or `systemQuery/systemMutation/systemAction` (internal),
42
- built once with `createEffectAuthFunctions(config, policies)` from `cvx-kit/effect`
43
- for shared Effect handles, or individually wrapped with `effectZodApiBuilder`.
44
- This is what guarantees auth, triggers, and bounded reads.
45
- 3. **Every public query returns DTOs** via `<table>.toPublicDto(row)` or the
46
- querying engine. For IDs use `toPublicDto(row, true)` with `dto(true)`; for
47
- computed fields use matching `extend` schemas and explicit `values`.
48
- `publicDto` remains the default schema. Never manually append IDs to a DTO.
49
- 4. **Every read is bounded**: `ctx.include(ctx.db.query('t')).matching(...)
50
- .execute(limit)` with `1 ≤ limit ≤ 100`. `.resolve()` falls back to a full
51
- table scan — avoid it. Alternatively use the independent querying engine
52
- with table/registry `paginationOpts(selection)` and matching `pageDto`.
53
- Declare relationships in root `relations.ts`, configure `createQueryEngine`
54
- in root `querying.ts`, and paginate inside authenticated domain Query handlers.
55
- Apply eligibility before limits; many relationships return bounded pages.
56
- `order` reverses native index order; `.when(value, index)` selects an index
57
- without binding fields. Unknown contexts fail closed and budget exhaustion
58
- throws. See `querying.md` and the DTO/pagination rules in `oxlint.md`.
59
- 5. **State changes are Effect commands**: declare
60
- `Command({ context, operations: ({ command }) => ({ ... }), defaults })`.
61
- Each `command({ input, result, classification, handler, audit })` owns its
62
- handler; handlers receive `(input, context)`. Execute with
63
- `commands.exec(operation, input, host)` and return the lazy Effect through a
64
- shared API builder. Never pass a handler to `exec` or run a nested
65
- `Effect.runPromise`. Permission uses the injected `checkPermission(host, ...)`;
66
- guards receive `(context, input)`. Per-operation middleware is one callback,
67
- `next()` returns an Effect, and `next({ context })` enriches downstream ctx.
68
- Order: input → context → permission → prepare/replay → middleware → default
69
- guard → operation guard → handler → result parse → aggregates/audit → completion.
70
- Audit writes stay in the mutation transaction. Failures must escape the
71
- mutation for rollback; replay skips middleware, guards, handler, and audit.
72
- 6. **Never write timestamps by hand.** `createdAt`/`updatedAt` are maintained
73
- by the `timestamps` trigger; `archivedAt` is the app-controlled soft-delete
74
- marker.
75
- 7. **Vocabularies are named readonly tuples in `constants.ts`** —
76
- `z.enum([...])` with an inline literal is banned in kit-style codebases.
77
- 8. **Components are consumed only through their client facades**
78
- (`Foundation`, `Approvals`) — no deep imports, no touching component
79
- tables, no reaching private children (`components.approvals.workflow`).
80
- 9. **Operation names are `domain.verb` lowercase-dotted; error codes are
81
- UPPER_SNAKE** — otherwise observability silently drops the events.
82
- 10. **Row-level security is configured, not hand-rolled**: the optional
83
- `security` config on `createAuthFunctions` takes role-level `rules`
84
- (works standalone) and/or `tenancy` (a table registry; adds
85
- `ctx.tenant`, deny-default isolation, and pairs with `tenantTable` +
86
- `tenantOwnership`). Multi-tenant apps stamp inserts with
87
- `tenant: ctx.tenant` and re-verify client ids with
88
- `requireTenantReference`. See `tenancy.md`.
89
-
90
- ## Exports map
91
-
92
- | Import | Provides |
93
- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
94
- | `cvx-kit` | Non-Effect helpers and component clients; import the optional Effect facade explicitly from `cvx-kit/effect`. |
95
- | `cvx-kit/zod-table` | `zodTable`, `tenantTable`, `createModule`, `paginated`, `zodVariantTable`, `jsonSafeZid`, `TIMESTAMP_FIELDS` |
96
- | `cvx-kit/effect` | Recommended command/query API: `createEffectFoundation`, `createEffectAuthFunctions`, `effectApiBuilder`, `effectZodApiBuilder`, `createEffectCrud`, idempotency/workflow adapters and operation tools. |
97
- | `cvx-kit/auth` | `createAuthFunctions` (incl. optional `security` RLS config), `createInclude`, `defaultRoleMap` |
98
- | `cvx-kit/tenancy` | `createTenantRules`, `composeRules`, `requireTenantReference`, `assertTenantOwned`, `TENANT_FIELD` |
99
- | `cvx-kit/crud` | Compatibility CRUD factory using the deprecated Promise command facade; use `createEffectCrud` from `cvx-kit/effect` for new domains. |
100
- | `cvx-kit/state-machine` | `createStateMachine` — typed transitions from constants tuples; `assert` drops into command guards |
101
- | `cvx-kit/middleware` | `rateLimit` — packaged middleware over an injected rate-limiter instance; keyed by `ctx.tenant`, missing key = config error |
102
- | `cvx-kit/webhooks` | `createWebhookBoundary`, `recordWebhookEvent`, `webhookEventsTable` — raw-body verify, natural-key dedup in the mutation |
103
- | `cvx-kit/agent-tools` | `createAgentTools` — tool records from table masks; mutation handlers route through command executors |
104
- | `cvx-kit/triggers` | `createTriggers`, `timestamps`, `appendOnly`, `noDelete`, `tenantOwnership`, `Triggers` |
105
- | `cvx-kit/errors` | `KitError`, `defaultErrors`, `ErrorFactory` |
106
- | `cvx-kit/components/foundation` | Component client and types; `Foundation.Command` is deprecated. The component and other capabilities remain available. New command registries use `cvx-kit/effect`. |
107
- | `cvx-kit/components/approvals` | `Approvals` client; default export = component config for `app.use` |
108
- | `cvx-kit/test` | `registerFoundation(t)`, `registerApprovals(t)` for convex-test |
109
-
110
- ## Minimal app wiring (the five root files)
111
-
112
- ```ts
113
- // convex/convex.config.ts
114
- import { defineApp } from 'convex/server'
115
- import foundation from 'cvx-kit/components/foundation/convex.config'
116
- import approvals from 'cvx-kit/components/approvals/convex.config'
117
- const app = defineApp()
118
- app.use(foundation)
119
- app.use(approvals)
120
- export default app
121
-
122
- // convex/triggers.ts
123
- import { createTriggers, timestamps, appendOnly } from 'cvx-kit/triggers'
124
- export const triggers = createTriggers<DataModel>()
125
- timestamps(triggers, 'documents')
126
- appendOnly(triggers, 'history')
127
-
128
- // convex/functions.ts
129
- export const {
130
- authQuery,
131
- authMutation: baseAuthMutation,
132
- authAction,
133
- adminQuery,
134
- adminMutation,
135
- adminAction,
136
- roleQuery,
137
- roleMutation,
138
- roleAction,
139
- systemQuery,
140
- systemMutation,
141
- systemAction,
142
- include,
143
- } = createAuthFunctions<DataModel>({
144
- query,
145
- mutation,
146
- action,
147
- internalQuery,
148
- internalMutation,
149
- internalAction, // from ./_generated/server
150
- getAuthUser: (ctx) => authKit.getAuthUser(ctx),
151
- mapRole: defaultRoleMap, // 'member'→'writer'; reader|writer|admin pass
152
- adminRoles: ['admin'],
153
- triggers,
154
- verifyMembership: async ({ userId, organizationId }) => {
155
- /* live check; actions only */
156
- },
157
- resolveOrganization: async ({ ctx, identity, user }) => {
158
- /* org+role from app tables; overrides claims; null/throw ⇒ FORBIDDEN */
159
- },
160
- })
161
-
162
- // convex/functions.ts — wrap configured auth constructors once, after auth/RLS/triggers
163
- // Import Context from effect and effectZodApiBuilder from cvx-kit/effect.
164
- // Place this after the registry declaration or import the registry from its module.
165
- export const authMutation = effectZodApiBuilder(baseAuthMutation, {
166
- context: (ctx) => ({ commands: commands.withContext(ctx) }),
167
- })
168
-
169
- // convex/foundation.ts
170
- // Import createEffectFoundation from cvx-kit/effect.
171
- import type { MutationCtx } from './_generated/server'
172
- export type CommandHost = MutationCtx & { actor: { userId: string } }
173
- export const { Command, Query, observability } = createEffectFoundation({
174
- observability: {
175
- enabled: () => process.env.OBS === 'true',
176
- classifyError, // → { outcome: 'denied'|'failed', errorCode }
177
- },
178
- writeAudit: (ctx: CommandHost, entry) => writeAuditEntry(ctx, entry),
179
- })
180
-
181
- // convex/approvals.ts
182
- export const approvals = new Approvals(components.approvals)
183
- ```
184
-
185
- ## Entity pattern
186
-
187
- ```ts
188
- // domain/documents/schema.ts
189
- export const documents = zodTable(
190
- 'documents',
191
- (id) => ({
192
- title: z.string(),
193
- ownerId: id('users'),
194
- secretNote: z.string(),
195
- }),
196
- {
197
- commandFields: ['title'], // what a command may say
198
- publicFields: ['title', 'ownerId'], // the DTO allowlist
199
- },
200
- )
201
-
202
- // convex/schema.ts (via domain/table.ts)
203
- defineSchema({ documents: documents.table.index('by_owner', ['ownerId']) })
204
-
205
- // domain/documents/commands.ts
206
- export const renameInput = documents.commandInput.extend({ id: zid('documents') })
207
- export const renameResult = z.object({ ok: z.literal(true) }).strict()
208
- export const commands = Command({
209
- context: (host: CommandHost) => host,
210
- operations: ({ command }) => ({
211
- 'documents.rename': command({
212
- input: renameInput,
213
- result: renameResult,
214
- classification: 'business',
215
- handler: async (input, ctx) => {
216
- await ctx.db.patch(input.id, { title: input.title })
217
- return { ok: true as const }
218
- },
219
- audit: ({ command }, ctx) => ({
220
- operation: 'documents.rename',
221
- actorId: ctx.actor.userId,
222
- aggregate: { type: 'document', id: command.id },
223
- }),
224
- }),
225
- }),
226
- })
227
-
228
- // api/documents.ts — thin public adapter
229
- export const rename = authMutation({
230
- args: renameInput,
231
- returns: renameResult,
232
- handler: (ctx, input) => ctx.commands.exec('documents.rename', input),
233
- })
234
- export const list = authQuery({
235
- args: { limit: z.number() },
236
- returns: z.array(documents.publicDto),
237
- handler: (ctx, { limit }) =>
238
- ctx
239
- .include(ctx.db.query('documents'))
240
- .matching('by_owner', (ix) => ix.eq('ownerId', ctx.actor.userId))
241
- .execute(limit, (rows) => rows.map(documents.toPublicDto)),
242
- })
243
- ```
244
-
245
- ## Auth ctx contents
246
-
247
- Authenticated handlers receive frozen `ctx.identity` (JWT), `ctx.user.id`,
248
- `ctx.org = { organizationId, role }`, `ctx.role`, `ctx.actor = { userId,
249
- organizationId, role }`, and `ctx.include`. Queries/mutations trust the JWT;
250
- **actions live-verify membership and fail closed**. Use `ctx.actor` as the
251
- canonical actor for audits/approvals.
252
-
253
- ## Approvals in one block
254
-
255
- ```ts
256
- export const publishApproval = approvals.define({
257
- name: 'documentPublish',
258
- steps: [
259
- approvals.decision('managerDecision', {
260
- decisions: ['approved', 'rejected'],
261
- quorum: { kind: 'count', approvals: 1 },
262
- makerChecker: true,
263
- expiresAfterMs: 604_800_000,
264
- }),
265
- approvals.branch('applyDecision', { approvedStepKey: 'publish', rejectedStepKey: 'notify' }),
266
- approvals.mutation('publish', {
267
- handler: internal.domain.documents.approval_functions.applyDecision,
268
- }),
269
- approvals.notify('notify', {
270
- handler: internal.domain.documents.approval_functions.notifyRejected,
271
- }),
272
- ],
273
- })
274
- // start(ctx, { scopeRef, resourceType, resourceRef, requester: ctx.actor, metadata? })
275
- // decide(ctx, { runId, decision, reason? }, ctx.actor) · status/evidence/list/cancel/restart
276
- ```
277
-
278
- Callback handlers are `systemMutation`/`systemAction`, receive opaque strings,
279
- and must `normalizeId` + re-check `resourceType` + verify the resource still
280
- points at `runId`.
281
-
282
- ## Footguns (top 8)
283
-
284
- 1. `t.run(ctx => ctx.db...)` in convex-test and raw builders bypass ALL
285
- triggers — seeding only.
286
- 2. `executeResultBoundary` returns a typed failure **only when the
287
- transaction has no effects yet**; after any write/schedule it rethrows for
288
- rollback. Handled-vs-thrown is decided by transaction state.
289
- 3. Observability drops events on identifier-regex mismatch — see rule 9.
290
- 4. Mis-declared `serverFields` silently make server-owned fields
291
- client-writable; masks are the security boundary.
292
- 5. `include(...).resolve()` silently falls back to `fullTableScan()`.
293
- 6. Command audit actors come from trusted host context, never client input.
294
- Commands with writes must run through mutation builders with wrapped `ctx.db`.
295
- 7. Changing approval steps without bumping `compatibilityKey` lets old runs
296
- be driven by an incompatible shape.
297
- 8. When composing ctx manually, spread `wrapDB(ctx)` first — later spreads
298
- carrying `db` restore the unwrapped database.
299
-
300
- Optional request context can be added through the existing Effect API adapter's
301
- `context` callback. Bind selected registries with `commands.withContext(value)`
302
- and consume `ctx.commands.exec(name, input)` through the application's existing
303
- function helper names. Keep dependencies local to each feature; explicit execution
304
- and input-only command handlers remain supported. See [optional context injection](./command-context.md).
305
-
306
- ## Shared auth and Effect context
307
-
308
- Configure `createEffectAuthFunctions` once in `convex/functions.ts` using a typed
309
- `AuthFunctionsConfig<DataModel, Role>` and inferred factory generics. Supply six
310
- explicit policies: `query`, `mutation`, `action`, `systemQuery`, `systemMutation`,
311
- and `systemAction`; use `{}` for no additions. Each supports `services`,
312
- `context`, and `mapError`. All auth/role/admin constructors share the policy
313
- for their kind. Services and context initialize only after successful auth and
314
- role checks, using the secured DB and refreshed live action membership.
315
-
316
- Bind query registries only in queries. Mutations can bind queries and commands
317
- with their secured triggered writer. Actions have no DB; bind action-compatible
318
- registries or delegate with `runMutation`/`runQuery`. Each registered mutation
319
- has its own transaction and JWT auth; an action is not one transaction. Configure
320
- internal system authority separately, without fabricated actors or inherited
321
- public handles. Return plain records with handles under named keys; native/auth
322
- key collisions are rejected, including inherited keys at runtime. Keep bound
323
- handles request-local, return lazy Effects, and let failures escape mutations
324
- for rollback. See `auth.md` for a packed-compiled shared factory example.
@@ -1,124 +0,0 @@
1
- # Maintaining a Convex application built on cvx-kit
2
-
3
- Practices that keep a cvx-kit backend maintainable over years, taken from the
4
- origin application. The theme throughout: make every rule either _structural_
5
- (the code can't be written any other way) or _executable_ (a test fails when
6
- it's violated). Conventions that live only in documentation decay.
7
-
8
- ## Schema and indexes
9
-
10
- - `convex/schema.ts` stays at ~4 lines: `defineSchema(domainTables)`.
11
- Per-domain `table.ts` files attach indexes to `<zodTable>.table` and
12
- `domain/table.ts` merges the maps. Shapes (zod) and topology (indexes) are
13
- deliberately separate files.
14
- - **Index naming**: `by_<field>_and_<field>...`, camelCase field names in
15
- declaration order, nested paths flattened with `_`
16
- (`by_assignedToUserId_and_state_status` for `['assignedToUserId',
17
- 'state.status']`). A reader should reconstruct the index definition from its
18
- name.
19
- - Every read goes through `ctx.include(...)` with an explicit limit ≤ 100.
20
- If a query can't be served by an index, that's a schema problem, not a
21
- reason to `fullTableScan`.
22
-
23
- ## Schema evolution without downtime
24
-
25
- Convex validates the whole table against the schema on push, so evolution is
26
- additive:
27
-
28
- 1. **New fields arrive `.optional()`** with a comment explaining the epoch:
29
- `// Optional only for documents created before priority was frozen.`
30
- 2. Deploy code that writes the new field on every new write and tolerates its
31
- absence on reads.
32
- 3. Backfill if needed (a `systemMutation` batch or `@convex-dev/migrations`),
33
- then — and only then — consider tightening the schema.
34
- 4. Never rename in place: add the new field, dual-write, backfill, drop the
35
- old one release(s) later.
36
-
37
- zod-table makes step 1 cheap: change the shape once and every boundary mask
38
- updates together, with `tsc` pointing at every consumer that must adapt.
39
-
40
- ## Function hygiene
41
-
42
- - Raw `query`/`mutation`/`action` builders appear **only** in `functions.ts`.
43
- Everything public is `auth*`/`role*`/`admin*`; everything
44
- internal/scheduled is `system*`. This is what makes triggers, auth, and
45
- bounded reads impossible to forget.
46
- - Public functions in `api/` are thin: parse args (zod-table masks), call the
47
- domain executor with `ctx.actor`, project through `toPublicDto`. Business
48
- logic that lives in an adapter can't be reused by workflows or callbacks —
49
- keep it in the domain.
50
- - Vocabularies (states, kinds, decisions) are single-owner readonly tuples in
51
- `constants.ts`. Grep for the tuple name to find every consumer.
52
-
53
- ## Audit and observability as maintenance tools
54
-
55
- - Every state change is a command with a mandatory classification and audit
56
- derivation — so "what happened to this record" is a query over the audit
57
- log, not archaeology. Keep operation names stable (`domain.verb`); they are
58
- your telemetry and audit vocabulary.
59
- - Observability is payload-free by construction — you can leave it on in
60
- production and grep durable one-line JSON events
61
- (`event: 'command.execution'`) without PII review.
62
- - Set retention deliberately: audit entries carry the classification as their
63
- retention category; the approvals component ships `cleanupAudit` for its own
64
- trail.
65
-
66
- ## Testing strategy
67
-
68
- Three test layers, all in-memory via `convex-test` on `edge-runtime`:
69
-
70
- 1. **Behavioral tests** — drive functions through the real constructors
71
- (`t.withIdentity(...)`), assert DTO shapes and trigger effects. Remember:
72
- `t.run(ctx => ctx.db...)` bypasses triggers — use it for seeding, not for
73
- asserting write behavior.
74
- 2. **Architecture tests** — glob the source and assert the conventions
75
- (no inline tables/enums, no deep imports, no sibling-domain imports, one
76
- `app.use` per component, no raw builders outside `functions.ts`). Prove
77
- each rule against a synthetic violating fixture first.
78
- 3. **Boundary/copyability tests** — components contain no host imports and no
79
- vendor/env references; optionally `cp -r` the component to a tmpdir and
80
- run `tsc` to prove copy-portability.
81
-
82
- Consumers register the kit's components on their test instance:
83
-
84
- ```ts
85
- import { convexTest } from 'convex-test'
86
- import { registerApprovals, registerFoundation } from 'cvx-kit/test'
87
-
88
- const t = convexTest(schema, modules)
89
- registerFoundation(t)
90
- registerApprovals(t)
91
- ```
92
-
93
- (`cvx-kit/test` ships as source and needs a vitest-compatible runner —
94
- `import.meta.glob`.) Nested components used by approvals register their own
95
- test modules the same way (`@convex-dev/workflow/test`,
96
- `convex-audit-log/test`). Stub required env vars (declared on
97
- `defineApp({ env })`) in test setup.
98
-
99
- ## Operational habits
100
-
101
- - **Env flags over redeploys**: observability is toggled by an env var read
102
- at call time (`enabled: () => env.OBS === 'true'`).
103
- - **Fail closed**: membership verification errors reject; unknown roles
104
- reject; unreadable transaction metrics rethrow. Preserve this bias in app
105
- code.
106
- - **Soft deletes** (`archivedAt` + `noDelete`) for anything a human might ask
107
- about later; `appendOnly` for evidence tables. Hard deletes are a data-loss
108
- bug you opt into per table.
109
- - **Component upgrades**: components own their tables; upgrade them like
110
- services — read the component's migration notes, bump, run codegen, run the
111
- suite. Never write to a component's tables from the host.
112
-
113
- ## Checklist for a new entity
114
-
115
- 1. `domain/<entity>/schema.ts` — `zodTable` with the three masks thought
116
- through (`serverFields`, `commandFields`, `publicFields`).
117
- 2. `domain/<entity>/table.ts` — indexes, named `by_...`.
118
- 3. Register in `domain/table.ts`; add `timestamps(triggers, '<table>')` and
119
- any `appendOnly`/`noDelete` discipline.
120
- 4. `constants.ts` — vocabularies as readonly tuples.
121
- 5. `commands.ts` — operation registry (`domain.verb`, classification, audit)
122
- - executors.
123
- 6. `api/<entity>.ts` — thin `auth*` adapters, DTO projection.
124
- 7. Tests: behavior + add the entity to the architecture globs.