cvx-kit 0.1.14 → 0.1.16
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.
- package/README.md +64 -45
- package/dist/querying/cursors.d.mts +5 -0
- package/dist/querying/cursors.mjs +1 -1
- package/dist/querying/cursors.mjs.map +1 -1
- package/dist/querying.d.mts +20 -6
- package/dist/querying.mjs +1 -1
- package/dist/querying.mjs.map +1 -1
- package/dist/relations.mjs +1 -1
- package/dist/relations.mjs.map +1 -1
- package/dist/zod-table.d.mts +59 -2
- package/dist/zod-table.mjs +1 -1
- package/dist/zod-table.mjs.map +1 -1
- package/package.json +10 -3
- package/AGENTS.md +0 -47
- package/CHANGELOG.md +0 -324
- package/ROADMAP.md +0 -126
- package/docs/agent-tools.md +0 -41
- package/docs/approvals.md +0 -201
- package/docs/architecture.md +0 -124
- package/docs/auth.md +0 -443
- package/docs/command-context.md +0 -150
- package/docs/commands-legacy.md +0 -513
- package/docs/commands.md +0 -173
- package/docs/conventions.md +0 -345
- package/docs/crud.md +0 -66
- package/docs/domain-contracts.md +0 -148
- package/docs/domain-operations.md +0 -615
- package/docs/effect.md +0 -403
- package/docs/llms.md +0 -324
- package/docs/maintainability.md +0 -124
- package/docs/migration.md +0 -351
- package/docs/oxlint.md +0 -254
- package/docs/querying.md +0 -293
- package/docs/tenancy.md +0 -176
- package/docs/triggers.md +0 -77
- package/docs/upgrading.md +0 -287
- package/docs/webhooks.md +0 -58
- package/docs/zod-table.md +0 -278
- package/scripts/convex-config-bridge.mjs +0 -44
- package/scripts/convex-smoke-env.mjs +0 -33
- package/scripts/extract-changelog.mjs +0 -29
- package/scripts/owned-process.mjs +0 -140
- package/scripts/owned-process.test.mjs +0 -107
- package/scripts/smoke-packed-components.mjs +0 -341
- package/scripts/smoke-packed-effect.mjs +0 -439
- package/scripts/smoke-packed-querying.mjs +0 -296
- package/scripts/test-packed-auth.mjs +0 -72
- package/scripts/test-packed-effect.mjs +0 -482
- package/scripts/test-packed-ids.mjs +0 -150
- package/scripts/test-packed-oxlint.mjs +0 -167
- 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.
|
package/docs/maintainability.md
DELETED
|
@@ -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.
|