cvx-kit 0.1.15 → 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 +59 -38
- 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 -337
- 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 -260
- package/docs/querying.md +0 -329
- 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 -298
- 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/migration.md
DELETED
|
@@ -1,351 +0,0 @@
|
|
|
1
|
-
# Migrating an existing Convex project to the cvx-kit architecture
|
|
2
|
-
|
|
3
|
-
This guide takes a "raw" Convex project — the typical shape where every file at
|
|
4
|
-
the root of `convex/` exports public queries and mutations, tables are defined
|
|
5
|
-
inline in `schema.ts`, and validators are `v.*` objects repeated per function —
|
|
6
|
-
and restructures it into the cvx-kit architecture.
|
|
7
|
-
|
|
8
|
-
This is an **opinionated infrastructure**. The rules below are not
|
|
9
|
-
suggestions; they are the contract. Where the kit makes something structural
|
|
10
|
-
(auth, triggers, bounded reads, mandatory audit), the migration's job is to
|
|
11
|
-
remove every code path that circumvents the structure. Names in `<angle
|
|
12
|
-
brackets>` are placeholders for your own vocabulary — no specific entity,
|
|
13
|
-
table, field, or workflow is mandated, only the roles and rules.
|
|
14
|
-
|
|
15
|
-
Migrate incrementally: each step leaves the app deployable, and old and new
|
|
16
|
-
shapes can coexist while you move entity by entity.
|
|
17
|
-
|
|
18
|
-
---
|
|
19
|
-
|
|
20
|
-
## 1. The target structure — complete
|
|
21
|
-
|
|
22
|
-
```
|
|
23
|
-
convex/
|
|
24
|
-
convex.config.ts # component mounting. Nothing else.
|
|
25
|
-
schema.ts # defineSchema(domainTables). Nothing else.
|
|
26
|
-
functions.ts # the single createAuthFunctions(...) call
|
|
27
|
-
triggers.ts # the single trigger registry + helper registrations
|
|
28
|
-
foundation.ts # the single createEffectFoundation(...) facade
|
|
29
|
-
<component>.ts # one facade per mounted component (audit.ts, approvals.ts, pool.ts, workflows.ts, ...)
|
|
30
|
-
auth.ts # identity-provider client wiring
|
|
31
|
-
auth.config.ts # Convex auth providers config
|
|
32
|
-
http.ts # HTTP router (webhook endpoints delegate to domains)
|
|
33
|
-
crons.ts # cron declarations only; targets are internal domain functions
|
|
34
|
-
|
|
35
|
-
api/ # ═══ THE ONLY PUBLIC SURFACE ═══
|
|
36
|
-
<entity>.ts # thin adapters: auth* constructors → domain executors → DTOs
|
|
37
|
-
|
|
38
|
-
domain/ # ═══ ALL BUSINESS LOGIC ═══
|
|
39
|
-
table.ts # merges per-entity table maps into domainTables
|
|
40
|
-
shared/ # genuinely cross-entity code ONLY
|
|
41
|
-
schema.ts # cross-entity zod helpers (if any)
|
|
42
|
-
<provider>.ts # one configured vendor client per provider (sql, cache, email, ...)
|
|
43
|
-
<entity>/ # one directory per entity/aggregate
|
|
44
|
-
schema.ts # the zodTable declaration — shapes and masks, nothing else
|
|
45
|
-
table.ts # index topology attached to <zodTable>.table
|
|
46
|
-
constants.ts # finite vocabularies: UPPER_SNAKE readonly tuples
|
|
47
|
-
commands.ts # operation registry + command executors
|
|
48
|
-
queries.ts # domain read logic
|
|
49
|
-
rules.ts # pure domain invariants/predicates (no ctx, no db)
|
|
50
|
-
shared.ts # helpers shared by multiple files of THIS entity
|
|
51
|
-
<workflow>.ts # approval/workflow definitions this entity owns
|
|
52
|
-
<workflow>_functions.ts # internal callback targets (system* constructors)
|
|
53
|
-
actions.ts # external-side-effect logic (via shared provider clients)
|
|
54
|
-
__tests__/ # behavioral tests for this entity
|
|
55
|
-
|
|
56
|
-
components/ # only if the app authors its own portable components
|
|
57
|
-
<name>/ # own convex.config.ts, schema.ts, _generated/, __tests__/
|
|
58
|
-
|
|
59
|
-
__tests__/ # ═══ APP-WIDE GUARDRAILS ═══
|
|
60
|
-
setup.ts # env stubs for vars declared on defineApp({ env })
|
|
61
|
-
architecture.test.ts # the executable conventions (see §6)
|
|
62
|
-
facades.test.ts # each app.use exactly once; no private-child access
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### The root rule
|
|
66
|
-
|
|
67
|
-
**The root of `convex/` contains configuration and facades only.** Every root
|
|
68
|
-
file is a _declaration point_ — the single place one piece of infrastructure
|
|
69
|
-
is configured — and none of them exports a public function or contains
|
|
70
|
-
business logic. If a client can call it, it lives in `api/`. If it decides
|
|
71
|
-
anything about the business, it lives in `domain/`. If `ls convex/*.ts` shows
|
|
72
|
-
anything else, the migration is not done.
|
|
73
|
-
|
|
74
|
-
### Hard prohibitions (grep-able, test-enforceable)
|
|
75
|
-
|
|
76
|
-
These patterns must not exist anywhere after migration — each one is a hole in
|
|
77
|
-
a structural guarantee:
|
|
78
|
-
|
|
79
|
-
| Forbidden | Where it's allowed instead | What it breaks otherwise |
|
|
80
|
-
| ------------------------------------------------------------------------------------------------- | -------------------------------------------------- | --------------------------------- |
|
|
81
|
-
| `query(`, `mutation(`, `action(`, `internalMutation(`, `internalAction(` from `_generated/server` | `functions.ts` only | auth, triggers, bounded reads |
|
|
82
|
-
| `defineTable(` | `domain/<entity>/table.ts` via `<zodTable>.table` | single source of truth per entity |
|
|
83
|
-
| `defineSchema(` | root `schema.ts` only | schema assembly |
|
|
84
|
-
| inline `z.enum([...])` / repeated literal unions | tuple in `constants.ts` | vocabulary ownership |
|
|
85
|
-
| `.collect()` / unbounded reads in public paths | `ctx.include(...).execute(limit)` | read bounds |
|
|
86
|
-
| `makeFunctionReference` | generated `internal` / `api` | refactoring safety |
|
|
87
|
-
| deep imports of a component's internals | the component's client facade | component boundary |
|
|
88
|
-
| sibling-domain imports (`domain/a` → `domain/b`) | `domain/shared/` or the public API | ownership |
|
|
89
|
-
| `ctx.db.insert/patch/delete` on a mounted component's tables | the component's client methods | component persistence |
|
|
90
|
-
| vendor SDK instantiation inside a domain | one shared client in `domain/shared/<provider>.ts` | connection discipline |
|
|
91
|
-
| `new Date().toISOString()` / hand-written timestamp fields | the `timestamps` trigger | server-owned lifecycle |
|
|
92
|
-
|
|
93
|
-
---
|
|
94
|
-
|
|
95
|
-
## 2. Where each kind of existing code goes
|
|
96
|
-
|
|
97
|
-
A raw project's root files usually mix several kinds of code. Pull them apart:
|
|
98
|
-
|
|
99
|
-
| You have today | It becomes |
|
|
100
|
-
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
101
|
-
| `export const get/list = query({...})` at root | adapter in `api/<entity>.ts` + read logic in `domain/<entity>/queries.ts` |
|
|
102
|
-
| `export const create/update = mutation({...})` at root | adapter in `api/<entity>.ts` + operation in `domain/<entity>/commands.ts` |
|
|
103
|
-
| `internalMutation`/`internalAction` helpers | `system*` functions in `domain/<entity>/<purpose>_functions.ts` |
|
|
104
|
-
| inline `defineTable({...})` in `schema.ts` | `zodTable` in `domain/<entity>/schema.ts` + indexes in `domain/<entity>/table.ts` |
|
|
105
|
-
| `v.union(v.literal(...))` / inline `z.enum` repeated around | one `UPPER_SNAKE` tuple in `domain/<entity>/constants.ts` |
|
|
106
|
-
| validation/business `if`-chains inside handlers | pure predicates in `domain/<entity>/rules.ts` |
|
|
107
|
-
| shared grab-bag (`helpers.ts`, `utils.ts`, `lib.ts`) | split by owner into `domain/<entity>/shared.ts`; only genuinely cross-entity code into `domain/shared/` |
|
|
108
|
-
| ad-hoc vendor clients (HTTP SDKs, caches, SQL) | one configured client per provider in `domain/shared/<provider>.ts` |
|
|
109
|
-
| webhook handlers with logic in `http.ts` | route in `http.ts`, logic in `domain/<entity>/actions.ts` |
|
|
110
|
-
| cron handlers with logic in `crons.ts` | declaration in `crons.ts`, target `system*` function in the domain |
|
|
111
|
-
| audit/log/history tables mirroring component state | the component's own persistence via its facade |
|
|
112
|
-
|
|
113
|
-
---
|
|
114
|
-
|
|
115
|
-
## 3. Naming — the full convention set
|
|
116
|
-
|
|
117
|
-
Naming is part of the architecture: several kit mechanisms key off it at
|
|
118
|
-
runtime (observability drops non-conforming events), and the rest makes the
|
|
119
|
-
codebase greppable.
|
|
120
|
-
|
|
121
|
-
- **Directories**: `domain/<entity>/` uses the plural camelCase table name.
|
|
122
|
-
One entity, one directory — no `domain/misc/`.
|
|
123
|
-
- **Tables**: plural camelCase (`<entities>`).
|
|
124
|
-
- **Indexes**: `by_<field>` / `by_<field>_and_<field>`, fields in declaration
|
|
125
|
-
order, nested paths flattened with `_` (`by_ownerId_and_state_status` for
|
|
126
|
-
`['ownerId', 'state.status']`). A reader must be able to reconstruct the
|
|
127
|
-
index definition from its name.
|
|
128
|
-
- **Command operations**: `<entity>.<verb>` lowercase dotted
|
|
129
|
-
(`invoices.approve`). Must match `/^[A-Za-z][A-Za-z0-9_.-]{0,159}$/` or
|
|
130
|
-
observability drops the events.
|
|
131
|
-
- **Error codes**: `UPPER_SNAKE`, matching `/^[A-Z][A-Z0-9_]{0,95}$/`. Define
|
|
132
|
-
the full set once (one owner), not per callsite.
|
|
133
|
-
- **Vocabulary tuples**: `UPPER_SNAKE_CASE` exported `as const`; derived type
|
|
134
|
-
`PascalCase` singular (`const <ENTITY>_STATES = [...] as const;
|
|
135
|
-
type <Entity>State = (typeof <ENTITY>_STATES)[number]`).
|
|
136
|
-
- **Domain executors**: `execute<Verb><Entity>` (`executeApproveInvoice`) —
|
|
137
|
-
the grep-able seam between adapters and domain.
|
|
138
|
-
- **Internal callback files**: `<purpose>_functions.ts` next to the
|
|
139
|
-
`<purpose>.ts` that references them.
|
|
140
|
-
- **Public function names** are `api/<entity>:fn` — clients never reference a
|
|
141
|
-
root-level path.
|
|
142
|
-
|
|
143
|
-
---
|
|
144
|
-
|
|
145
|
-
## 4. Step-by-step migration
|
|
146
|
-
|
|
147
|
-
### Step 0 — Inventory
|
|
148
|
-
|
|
149
|
-
List every exported function and classify it: public read, public write,
|
|
150
|
-
internal, cron target, HTTP handler. List every table, every index, every
|
|
151
|
-
literal string union, every vendor client instantiation. This is the
|
|
152
|
-
checklist; entities with the fewest dependencies migrate first.
|
|
153
|
-
|
|
154
|
-
### Step 1 — Mount the kit, declare the root files
|
|
155
|
-
|
|
156
|
-
Install `cvx-kit`, mount `foundation` (and `approvals` if needed) in
|
|
157
|
-
`convex.config.ts`, run codegen. Create `functions.ts`, `triggers.ts`,
|
|
158
|
-
`foundation.ts`, and one facade per mounted component. Declare required env
|
|
159
|
-
vars on `defineApp({ env: { ... } })` so deploys fail fast, and stub them in
|
|
160
|
-
`__tests__/setup.ts`. Nothing breaks: raw functions keep working alongside
|
|
161
|
-
the new constructors.
|
|
162
|
-
|
|
163
|
-
### Step 2 — Convert tables entity by entity
|
|
164
|
-
|
|
165
|
-
For each table:
|
|
166
|
-
|
|
167
|
-
1. `domain/<entity>/schema.ts`: a `zodTable` mirroring the current shape,
|
|
168
|
-
`v.*` converted to zod. Decide the three masks **deliberately** —
|
|
169
|
-
`serverFields` (server-assigned), `commandFields` (what a write request
|
|
170
|
-
may say), `publicFields` (what a client may see). This is where implicit
|
|
171
|
-
trust becomes explicit policy; a mis-declared mask is a security bug, so
|
|
172
|
-
review masks like auth code.
|
|
173
|
-
2. `domain/<entity>/table.ts`: move the indexes, renamed to the convention.
|
|
174
|
-
3. Register in `domain/table.ts`; shrink root `schema.ts` toward
|
|
175
|
-
`defineSchema(domainTables)`.
|
|
176
|
-
4. The kit's timestamps are `.optional()`, so existing rows validate without
|
|
177
|
-
backfill. Register `timestamps(triggers, '<table>')`; new writes maintain
|
|
178
|
-
`createdAt`/`updatedAt`. Backfill later if needed. Existing hand-rolled
|
|
179
|
-
timestamp fields (e.g. ISO strings) stay in the shape for now — migrate
|
|
180
|
-
the data separately; never couple data migration to restructuring.
|
|
181
|
-
5. Apply write discipline per table while you're here: `appendOnly` for
|
|
182
|
-
evidence tables (votes, history), `noDelete` + `archivedAt` for anything a
|
|
183
|
-
human might ask about later. Hard deletes are opt-in per table, not the
|
|
184
|
-
default.
|
|
185
|
-
|
|
186
|
-
### Step 3 — Move vocabularies to `constants.ts`
|
|
187
|
-
|
|
188
|
-
Every reusable finite vocabulary gets one owner: an exported tuple in the
|
|
189
|
-
owning entity's `constants.ts`. Schemas, validators, filters, transition
|
|
190
|
-
maps, and tests consume the tuple. Subsets and ordered presentations derive
|
|
191
|
-
from the canonical tuple, typed against its value type so an invalid literal
|
|
192
|
-
cannot compile. A file-local tuple is acceptable **only** for private
|
|
193
|
-
implementation mechanics that can never appear in another schema, contract,
|
|
194
|
-
persisted document, or UI state — and it is still `UPPER_SNAKE_CASE`.
|
|
195
|
-
|
|
196
|
-
### Step 4 — Split public functions into adapter + domain logic
|
|
197
|
-
|
|
198
|
-
For each public function at the root:
|
|
199
|
-
|
|
200
|
-
1. **Extract the domain logic.** Reads → `domain/<entity>/queries.ts`.
|
|
201
|
-
Writes → an operation in the entity's command registry with
|
|
202
|
-
classification and audit derivation (see `commands.md`) — deciding _what
|
|
203
|
-
gets audited and as what_ is a product decision; make it explicitly, not
|
|
204
|
-
as an afterthought. Pure conditions the handler checks
|
|
205
|
-
(state-transition legality, permission-beyond-role predicates) →
|
|
206
|
-
`rules.ts`, so they're unit-testable without a ctx.
|
|
207
|
-
2. **Write the adapter** in `api/<entity>.ts` with `authQuery`/
|
|
208
|
-
`authMutation`/`role*`/`admin*`. Adapters do exactly four things: parse
|
|
209
|
-
args with the entity's boundary schemas, authorize via the constructor
|
|
210
|
-
choice, call the domain executor with `ctx.actor`, project through
|
|
211
|
-
`toPublicDto`. **Adapter budget: if it exceeds ~10 lines of glue or grows
|
|
212
|
-
a private helper, logic is leaking — move it into the domain.**
|
|
213
|
-
3. **Retire the old path.** Public names change to `api/<entity>:fn`; update
|
|
214
|
-
client call sites per entity, or keep a temporary re-export file at the
|
|
215
|
-
old path and delete it in the final step.
|
|
216
|
-
|
|
217
|
-
Anything "public-but-really-internal" (only called by your own scheduled
|
|
218
|
-
functions or other backend code) must stop being public: convert to `system*`
|
|
219
|
-
in the domain.
|
|
220
|
-
|
|
221
|
-
### Step 5 — Convert internal functions and the wiring files
|
|
222
|
-
|
|
223
|
-
- Replace raw `internalMutation`/`internalAction` with `systemMutation`/
|
|
224
|
-
`systemAction` from `functions.ts`, located in the owning domain's
|
|
225
|
-
`<purpose>_functions.ts`. Scheduled and callback writes now run under the
|
|
226
|
-
same trigger regime as user writes.
|
|
227
|
-
- `crons.ts` becomes declarations only, targeting `internal.domain.<entity>.
|
|
228
|
-
<purpose>_functions.*`.
|
|
229
|
-
- `http.ts` becomes routing only: verify the webhook signature, parse the
|
|
230
|
-
payload at the boundary, delegate to a domain action/function.
|
|
231
|
-
|
|
232
|
-
### Step 6 — Bound the reads
|
|
233
|
-
|
|
234
|
-
Replace `.collect()` and unbounded `.take(n)` in public paths with
|
|
235
|
-
`ctx.include(...).execute(limit)` (limit ≤ 100). Where a query can't be
|
|
236
|
-
served by an index, **add the index** — a scan is a schema problem, not a
|
|
237
|
-
query-code problem. Paginated surfaces cross the boundary with the convex
|
|
238
|
-
pagination validators bridged into zod (`convexToZod(paginationOptsValidator)`
|
|
239
|
-
and a `paginationResultValidator` built from the row validator), not with
|
|
240
|
-
hand-rolled cursor objects.
|
|
241
|
-
|
|
242
|
-
### Step 7 — Enforce everything with tests
|
|
243
|
-
|
|
244
|
-
Port the executable-architecture pattern. `__tests__/architecture.test.ts`
|
|
245
|
-
globs all production sources and asserts the Hard Prohibitions table above;
|
|
246
|
-
**each rule is proved against a synthetic violating fixture first** so a
|
|
247
|
-
broken checker cannot silently pass. `facades.test.ts` asserts each
|
|
248
|
-
`app.use(x)` appears exactly once and no file reaches a component's private
|
|
249
|
-
children. From this point the migration cannot regress — and neither can
|
|
250
|
-
future contributors or coding agents.
|
|
251
|
-
|
|
252
|
-
---
|
|
253
|
-
|
|
254
|
-
## 5. File conduct — how every source file is organized
|
|
255
|
-
|
|
256
|
-
From the origin project's code conduct. Apply to new code and files being
|
|
257
|
-
substantially changed; don't mechanically rewrite untouched files.
|
|
258
|
-
|
|
259
|
-
**File order** — readable from public purpose to private implementation:
|
|
260
|
-
|
|
261
|
-
1. Imports.
|
|
262
|
-
2. Exported types, schemas, constants, and public functions — the API.
|
|
263
|
-
3. File-local types and constants supporting the implementation.
|
|
264
|
-
4. Private helpers, as **named function declarations**, at the bottom.
|
|
265
|
-
|
|
266
|
-
```ts
|
|
267
|
-
import { parseInput } from './schema'
|
|
268
|
-
|
|
269
|
-
export async function createProjection(input: unknown) {
|
|
270
|
-
return buildProjection(parseInput(input))
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function buildProjection(input: ProjectionInput) {
|
|
274
|
-
// implementation
|
|
275
|
-
}
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
Never place private helper implementations above the exported API. Prefer
|
|
279
|
-
`function` declarations over helper arrow-constants so helpers can live at
|
|
280
|
-
the bottom.
|
|
281
|
-
|
|
282
|
-
**Ownership.** Put behavior beside the entity that owns it. Consumers (api
|
|
283
|
-
adapters, cron targets, workflow callbacks) stay thin: validate, authorize,
|
|
284
|
-
call the owning operation, translate the result. A consumer accumulating
|
|
285
|
-
entity rules or a pile of private helpers is logic asking to move into the
|
|
286
|
-
domain.
|
|
287
|
-
|
|
288
|
-
**No convenience re-exports.** Import a function from the file that owns it;
|
|
289
|
-
never re-export to shorten a path. Barrel files (`index.ts` re-exporting a
|
|
290
|
-
directory) are banned inside `convex/` — they blur ownership and create
|
|
291
|
-
import cycles.
|
|
292
|
-
|
|
293
|
-
**Components own their persistence.** Never mirror a mounted component's
|
|
294
|
-
internal state (audit events, workflow steps, job results) into application
|
|
295
|
-
tables unless the product domain independently requires a durable business
|
|
296
|
-
record. Log through the facade; don't build a receipts table for it.
|
|
297
|
-
|
|
298
|
-
**Comments state constraints, not narration.** The accepted pattern for
|
|
299
|
-
schema evolution is a one-line epoch comment on an `.optional()` field
|
|
300
|
-
explaining why it's optional. Comments that restate the next line are noise.
|
|
301
|
-
|
|
302
|
-
---
|
|
303
|
-
|
|
304
|
-
## 6. Documentation the migrated repo must carry
|
|
305
|
-
|
|
306
|
-
An opinionated codebase documents its opinions, or they die at the first
|
|
307
|
-
new contributor. Minimum set:
|
|
308
|
-
|
|
309
|
-
- **`CODE_CONDUCT.md`** (or equivalent) — the repo's own conventions: file
|
|
310
|
-
order, ownership layout, vocabulary rules, naming. This guide's §3 and §5
|
|
311
|
-
are the starting template; adapt, don't link.
|
|
312
|
-
- **`CONCEPTS.md`** — the domain glossary: every entity, state, and
|
|
313
|
-
vocabulary word with its business meaning. LLMs and new hires read this
|
|
314
|
-
first; keep entries one paragraph.
|
|
315
|
-
- **`CLAUDE.md` / `AGENTS.md`** — thin pointers: the stack, the three
|
|
316
|
-
commands that matter (test, typecheck, deploy), and links to
|
|
317
|
-
`CODE_CONDUCT.md` + `node_modules/cvx-kit/src/docs/llms.md`. Agent files
|
|
318
|
-
that duplicate the conduct doc drift; keep them as an index.
|
|
319
|
-
- **Architecture tests as documentation** — the test names in
|
|
320
|
-
`architecture.test.ts` are the enforceable summary of the conduct doc;
|
|
321
|
-
a rule that exists in prose but not in the suite is a request, not a rule.
|
|
322
|
-
|
|
323
|
-
---
|
|
324
|
-
|
|
325
|
-
## 7. Definition of done
|
|
326
|
-
|
|
327
|
-
The migration is complete when every box checks:
|
|
328
|
-
|
|
329
|
-
- [ ] `ls convex/*.ts` shows only configuration and facades (§1 table).
|
|
330
|
-
- [ ] Every public function lives in `api/` and uses `auth*`/`role*`/`admin*`.
|
|
331
|
-
- [ ] Every internal function uses `system*` and lives in its domain.
|
|
332
|
-
- [ ] `functions.ts` is the only file importing `_generated/server` builders.
|
|
333
|
-
- [ ] Every table is a `zodTable` with deliberately reviewed masks; indexes
|
|
334
|
-
named by convention; timestamps trigger registered.
|
|
335
|
-
- [ ] Every vocabulary is a single-owner tuple in a `constants.ts`.
|
|
336
|
-
- [ ] Every state change is a command operation with classification + audit.
|
|
337
|
-
- [ ] Every public read is bounded (`include` + limit, or bridged pagination).
|
|
338
|
-
- [ ] No entry in the Hard Prohibitions table greps positive.
|
|
339
|
-
- [ ] `architecture.test.ts` + `facades.test.ts` exist, each rule fixture-proved.
|
|
340
|
-
- [ ] `CODE_CONDUCT.md` and `CONCEPTS.md` exist; agent files point at them.
|
|
341
|
-
- [ ] Transitional re-exports deleted; clients call `api/<entity>:fn` paths.
|
|
342
|
-
|
|
343
|
-
## Migration order that works
|
|
344
|
-
|
|
345
|
-
1. Root files + kit mounting (step 1) — zero behavioral change.
|
|
346
|
-
2. The smallest, least-referenced entity end-to-end (steps 2–6) — shakes out
|
|
347
|
-
masks, role mapping, and conventions on a low-stakes surface.
|
|
348
|
-
3. Architecture tests (step 7) — lock the rules in **before** the bulk.
|
|
349
|
-
4. Remaining entities in dependency order, one PR each.
|
|
350
|
-
5. Delete transitional re-exports and remaining root function files; run the
|
|
351
|
-
definition-of-done checklist.
|
package/docs/oxlint.md
DELETED
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
# Oxlint rules for cvx-kit applications
|
|
2
|
-
|
|
3
|
-
`cvx-kit/oxlint` is a TypeScript-authored plugin bundled with the package. It
|
|
4
|
-
checks applications using the kit against `conventions.md`, the authoritative
|
|
5
|
-
source for folder structure and file anatomy. The package repository has a
|
|
6
|
-
separate, unpublished plugin for its own `src/` layout.
|
|
7
|
-
|
|
8
|
-
The consumer plugin was introduced in 0.1.5; the initial 0.1.4 plugin targeted
|
|
9
|
-
library authoring. The five architecture rules below were added in 0.1.6. See `upgrading.md` when migrating that configuration.
|
|
10
|
-
|
|
11
|
-
## Configuration
|
|
12
|
-
|
|
13
|
-
Install Oxlint as a development dependency and load the plugin in
|
|
14
|
-
`.oxlintrc.json`. The root README includes a configuration enabling all rules. The coverage table
|
|
15
|
-
below lists all 23 rule names.
|
|
16
|
-
Loading a plugin does not enable its rules. For example:
|
|
17
|
-
|
|
18
|
-
```json
|
|
19
|
-
{
|
|
20
|
-
"jsPlugins": [{ "name": "cvx", "specifier": "cvx-kit/oxlint" }],
|
|
21
|
-
"rules": {
|
|
22
|
-
"cvx/no-raw-builders": "error",
|
|
23
|
-
"cvx/domain-import-boundaries": "error",
|
|
24
|
-
"cvx/thin-api-adapters": "error"
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
With Vite+, place those fields under `lint` in `vite.config.ts`. The plugin
|
|
30
|
-
exports a default plugin object, named `rules`, and the `RuleName` type. It is
|
|
31
|
-
tested with Oxlint 1.87.0 and Vite+ 1.1.0.
|
|
32
|
-
|
|
33
|
-
All rules default to `convex/` relative to the linter's working directory.
|
|
34
|
-
For another location, set `convexDir` on **each enabled rule**:
|
|
35
|
-
|
|
36
|
-
```json
|
|
37
|
-
{ "cvx/no-raw-builders": ["error", { "convexDir": "src/server/convex" }] }
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
Source rules ignore files outside that directory, `_generated/`, `__tests__/`,
|
|
41
|
-
and test/spec files. `project-structure` reads the whole backend, including tests. They do not
|
|
42
|
-
offer automatic fixes for architectural moves.
|
|
43
|
-
|
|
44
|
-
## Coverage
|
|
45
|
-
|
|
46
|
-
Every name below has the `cvx/` prefix in lint configuration.
|
|
47
|
-
|
|
48
|
-
| Rule | Detects | Documentation |
|
|
49
|
-
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
|
|
50
|
-
| `project-structure` | Root folder allowlist, domain ownership, colocated runnable tests, misplaced tests, symlinks, parse failures, runtime domain-module cycles, and shared implementations with only one domain consumer. | Folder structure and Tests |
|
|
51
|
-
| `root-wiring-only` | Unknown root files without a component facade, root function declarations, nondelegating callbacks, branches, loops, direct writes/network calls, and inline root schema construction. | Root files |
|
|
52
|
-
| `application-orchestration` | Files without two domain dependencies, provider construction, external runtime dependencies, database access, validator construction, and business branches. | Module grain |
|
|
53
|
-
| `internal-function-ownership` | Internal/system registrations at root, in public API adapters, or in domain/shared. | Folder structure |
|
|
54
|
-
| `domain-file-responsibilities` | Impure rules, query writes/mutations, database access in declarative files, misplaced direct external I/O, and imports that violate these roles. | File anatomy |
|
|
55
|
-
| `root-facade-ownership` | `createAuthFunctions`, foundation factories, `createTriggers`, `defineRelations`, and `createQueryEngine` outside their root facades, or repeated in one file. | `conventions.md`, Root files |
|
|
56
|
-
| `no-raw-builders` | Runtime imports of raw Convex builders outside `functions.ts`; namespace calls to raw generic builders. | `auth.md`; `llms.md`, Non-negotiable rules |
|
|
57
|
-
| `no-handwritten-references` | Calls to Convex `makeFunctionReference`, including import aliases. | `conventions.md`, Import rules |
|
|
58
|
-
| `domain-import-boundaries` | Reverse dependencies into `api/`, domain imports of `application/`, forbidden sibling imports, and API adapters importing another domain module. | `conventions.md`, Module grain and Import rules |
|
|
59
|
-
| `public-functions-in-api` | Calls to root `auth*`, `role*`, or `admin*` constructors outside `api/`. | `conventions.md`, Folder structure |
|
|
60
|
-
| `thin-api-adapters` | `if`, `switch`, ternary branches, top-level private helpers, and direct `ctx.db` writes in `api/`. | `conventions.md`, The adapter file |
|
|
61
|
-
| `no-inline-enums` | Literal arrays passed to imported Zod `enum`. Use named vocabulary tuples. | `conventions.md`, Vocabularies |
|
|
62
|
-
| `no-unbounded-reads` | Recognized unbounded database/stream/relationship reads and eligibility `.filter()` calls after bounded take/execute/paginate results. | `llms.md`, Bounded reads; `zod-table.md` |
|
|
63
|
-
| `no-manual-timestamps` | Explicit `createdAt`/`updatedAt` fields in object literals passed to direct `ctx.db` writes. | `triggers.md` |
|
|
64
|
-
| `component-boundaries` | Local portable components importing host/sibling files; host imports bypassing component client facades; private package component subpaths. | `architecture.md`, Injection, not import |
|
|
65
|
-
| `no-component-env` | Direct `process.env` access, process import aliases, environment destructuring, and named env imports inside local portable components. | `conventions.md`, Import rules |
|
|
66
|
-
| `schema-file-boundaries` | Host `defineTable`, misplaced `defineSchema` or kit table constructors, and misplaced `.table.index/searchIndex/vectorIndex` chains. | `conventions.md`, Shapes and topology |
|
|
67
|
-
| `no-internal-reexports` | Re-exports and import-then-export aliases inside `convex/`. | `conventions.md`, Universal file order |
|
|
68
|
-
| `public-api-first` | Private function declarations before the last export. | `conventions.md`, Universal file order |
|
|
69
|
-
| `named-private-helpers` | Top-level private arrow/function-expression variables. | `conventions.md`, Universal file order |
|
|
70
|
-
| `no-manual-id-projection` | Adding `id`/`_id` to a recognized table DTO through object spreads or property assignments. | Public DTO parameters in `zod-table.md` |
|
|
71
|
-
| `no-copied-pagination` | Zod/native validator objects copying `numItems` + `cursor` or `page` + `isDone` + `continueCursor`. | Pagination options in `zod-table.md`; `querying.md` |
|
|
72
|
-
| `reserved-domain-file-anatomy` | Reserved domain files without their expected declarative schema/contracts/table or Command/Query registry shape. | Reserved domain files below |
|
|
73
|
-
|
|
74
|
-
## Allowed patterns
|
|
75
|
-
|
|
76
|
-
- `functions.ts` imports raw builders to configure `createAuthFunctions`.
|
|
77
|
-
Type-only imports such as `MutationCtx` from `_generated/server` are valid
|
|
78
|
-
elsewhere. `httpAction` remains available for HTTP routing.
|
|
79
|
-
- Domain modules may import sibling `rules.ts` and `queries.ts`, as explicitly
|
|
80
|
-
allowed by the module-grain section. Cross-module writes go through the
|
|
81
|
-
optional `application/` layer. `domain/table.ts` only imports module table maps
|
|
82
|
-
and combines them with `createModule` from `cvx-kit/zod-table`.
|
|
83
|
-
- API adapters may delegate and perform the documented bounded inline query
|
|
84
|
-
and DTO projection. `system*` constructors may live in domain modules.
|
|
85
|
-
- Authored components under `convex/components/<name>/` may use their own raw
|
|
86
|
-
builders and raw tables. Host-specific facade, timestamp, and bounded-read
|
|
87
|
-
rules do not apply there. Configuration imports for mounting components are
|
|
88
|
-
allowed from root `convex.config.ts`.
|
|
89
|
-
- Host schemas use kit table constructors in `domain/<module>/schema.ts` and
|
|
90
|
-
attach indexes in `domain/<module>/table.ts`. `schema.ts` assembles them.
|
|
91
|
-
- `archivedAt` remains application-controlled. Singular `db.get` reads and
|
|
92
|
-
bounded `execute`/`paginate` calls are allowed.
|
|
93
|
-
|
|
94
|
-
## DTO and pagination diagnostics
|
|
95
|
-
|
|
96
|
-
Enable the new diagnostics alongside the existing architecture rules:
|
|
97
|
-
|
|
98
|
-
```json
|
|
99
|
-
{
|
|
100
|
-
"rules": {
|
|
101
|
-
"cvx/no-manual-id-projection": "error",
|
|
102
|
-
"cvx/no-copied-pagination": "error",
|
|
103
|
-
"cvx/no-unbounded-reads": "error",
|
|
104
|
-
"cvx/root-facade-ownership": "error",
|
|
105
|
-
"cvx/root-wiring-only": "error",
|
|
106
|
-
"cvx/reserved-domain-file-anatomy": "error"
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
### `no-manual-id-projection`
|
|
112
|
-
|
|
113
|
-
Recognizes a table DTO projection/parse, including supported binding aliases,
|
|
114
|
-
then reports adding `id` or `_id` by a spread or property assignment:
|
|
115
|
-
|
|
116
|
-
```ts
|
|
117
|
-
// Reported
|
|
118
|
-
const output = { ...tasks.toPublicDto(row), _id: row._id }
|
|
119
|
-
const parsed = tasks.publicDto.parse(input)
|
|
120
|
-
parsed.id = row._id
|
|
121
|
-
|
|
122
|
-
// Use the matching projection and return boundary
|
|
123
|
-
const outputWithId = tasks.toPublicDto(row, true)
|
|
124
|
-
const returns = tasks.dto(true)
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
The helper returns `_id`, not an `id` alias. For computed fields use the
|
|
128
|
-
validated `extend`/`values` API described in [zod-table.md](./zod-table.md).
|
|
129
|
-
The rule depends on recognized table/DTO provenance; it does not treat every
|
|
130
|
-
object named `dto` as a kit boundary or prove all runtime projection paths safe.
|
|
131
|
-
|
|
132
|
-
### `no-copied-pagination`
|
|
133
|
-
|
|
134
|
-
Reports recognized `z.object`/Convex `v.object` shapes containing both `numItems`
|
|
135
|
-
and `cursor`, or all three of `page`, `isDone`, and `continueCursor`:
|
|
136
|
-
|
|
137
|
-
```ts
|
|
138
|
-
// Reported
|
|
139
|
-
const options = z.object({ numItems: z.number(), cursor: z.string().nullable() })
|
|
140
|
-
const result = z.object({
|
|
141
|
-
page: z.array(tasks.publicDto),
|
|
142
|
-
isDone: z.boolean(),
|
|
143
|
-
continueCursor: z.string(),
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
// Table query input and relation-aware output
|
|
147
|
-
const options = tasks.paginationOpts({ status: true })
|
|
148
|
-
const result = relations.tables.tasks.pageDto({})
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
For ordinary native pagination, retain `paginationOptsValidator` (bridge through
|
|
152
|
-
`convexToZod` when needed) or `paginated(tasks.dto())`. These preserve native
|
|
153
|
-
validators instead of copying their current fields. Application-specific fields
|
|
154
|
-
can extend the derived Zod object. Use the engine's `paginationOpts` for its
|
|
155
|
-
selected filters and order; a native options validator alone supplies neither.
|
|
156
|
-
|
|
157
|
-
### `no-unbounded-reads`
|
|
158
|
-
|
|
159
|
-
The existing rule now follows recognized native, kit `include`, stream, and
|
|
160
|
-
relationship read chains. It reports unbounded `collect()`/`resolve()` where
|
|
161
|
-
supported provenance is known, and `.filter()` on recognized bounded results:
|
|
162
|
-
|
|
163
|
-
```ts
|
|
164
|
-
// Reported: eligible rows outside the first 25 can be missed
|
|
165
|
-
const rows = await ctx.db.query('tasks').take(25)
|
|
166
|
-
const eligible = rows.filter((task) => task.status === 'open')
|
|
167
|
-
|
|
168
|
-
// Put eligibility into the query before page filling
|
|
169
|
-
const page = await querying(ctx).tasks.where({ status: 'open' }).paginate(args)
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
Choose an indexed native constraint where appropriate, or express the predicate
|
|
173
|
-
through the querying engine. Many relationships use bounded pages and separate
|
|
174
|
-
continuation endpoints. There is no automatic predicate-movement fix: moving a
|
|
175
|
-
predicate can change authorization, ordering, and continuation semantics.
|
|
176
|
-
|
|
177
|
-
### Relation and engine facade ownership
|
|
178
|
-
|
|
179
|
-
`root-facade-ownership` requires `defineRelations` in `convex/relations.ts` and
|
|
180
|
-
`createQueryEngine` in `convex/querying.ts`, each configured once. The same rule
|
|
181
|
-
continues to enforce auth, triggers, and foundation facades. Declare one global
|
|
182
|
-
engine as `createQueryEngine({ schema, relations })`, then bind it with
|
|
183
|
-
`querying(ctx)` inside handlers. Avoid rebuilding engines per request or table.
|
|
184
|
-
Cursor key parsing belongs to the engine's server environment loader; advanced
|
|
185
|
-
application-wide overrides use the optional second argument. This setup guidance
|
|
186
|
-
is a convention; the rule currently checks facade ownership and repeated factory
|
|
187
|
-
calls, rather than proving module-scope placement or environment parsing.
|
|
188
|
-
|
|
189
|
-
`root-wiring-only` and the whole-project checker permit these root files only
|
|
190
|
-
as narrow declarative factory configurations. `relations.ts` can import domain
|
|
191
|
-
schema/table declarations (including the domain table assembly); `querying.ts`
|
|
192
|
-
can import domain query/contract modules where permitted. Neither file is a
|
|
193
|
-
place for endpoint handlers, business branches, or direct database operations.
|
|
194
|
-
Domain contracts derive validators, domain Query registries own handlers, and
|
|
195
|
-
`api/` adapters delegate through authenticated constructors. See
|
|
196
|
-
[querying.md](./querying.md) and [conventions.md](./conventions.md).
|
|
197
|
-
|
|
198
|
-
## What still needs review and tests
|
|
199
|
-
|
|
200
|
-
These are syntax and lexical-binding checks, not whole-program validation.
|
|
201
|
-
Import-boundary rules resolve relative imports and tsconfig aliases (including
|
|
202
|
-
extended configs) using the package resolver. Symlinked source is rejected by
|
|
203
|
-
the project scan. Computed import paths are not resolved. Named import aliases and namespaces are recognized where documented;
|
|
204
|
-
General variable reassignment, arbitrary wrappers, and destructured database
|
|
205
|
-
methods are not whole-program traced. DTO/read checks do follow supported local
|
|
206
|
-
initializers, aliases, awaited results, and recognized helper chains; unrelated
|
|
207
|
-
user functions and unknown provenance remain outside that bounded analysis. Database checks
|
|
208
|
-
recognize the documented `ctx.db` member shape, not its runtime type.
|
|
209
|
-
|
|
210
|
-
The plugin does not prove DTO redaction, complete command/audit coverage,
|
|
211
|
-
tenant/RLS correctness, unique table ownership across files, required facade
|
|
212
|
-
existence, exactly one component mount, numeric read limits, or vocabulary
|
|
213
|
-
tuple ownership. Run the behavioral tests described in `maintainability.md`. The package owns
|
|
214
|
-
structural enforcement; applications do not need copied architecture tests. A clean lint run is not
|
|
215
|
-
evidence of those runtime guarantees.
|
|
216
|
-
|
|
217
|
-
## Whole-project validation
|
|
218
|
-
|
|
219
|
-
Enable `cvx/project-structure` and lint the **whole** configured backend. The
|
|
220
|
-
scan reports on `convex.config.ts`, or the first non-test source file if no
|
|
221
|
-
configuration file exists. That anchor must be part of the lint invocation;
|
|
222
|
-
changed-file-only runs and cached lint runs can skip project checks. Use a full,
|
|
223
|
-
uncached lint run in CI. The scan ignores generated code, reads current files on
|
|
224
|
-
every invocation, and does not execute application code.
|
|
225
|
-
|
|
226
|
-
For standalone tooling, including an empty/missing backend, use the same packaged
|
|
227
|
-
checker directly:
|
|
228
|
-
|
|
229
|
-
```ts
|
|
230
|
-
import { checkArchitecture } from 'cvx-kit/oxlint'
|
|
231
|
-
|
|
232
|
-
const issues = checkArchitecture({ cwd: process.cwd(), convexDir: 'convex' })
|
|
233
|
-
for (const issue of issues) console.error(`${issue.file}: ${issue.message}`)
|
|
234
|
-
if (issues.length) process.exitCode = 1
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
No fixed module scaffold is required. Type-only modules and grouping folders
|
|
238
|
-
need no test directory. Runtime schema/constants files do; root wiring shares
|
|
239
|
-
root tests. Test files must be directly inside the owning `__tests__/` directory.
|
|
240
|
-
Recognized test entrypoints are `test`/`it` globals and imports from Vitest,
|
|
241
|
-
Vite+, Bun, Node, or Jest, including import aliases and namespaces. Empty,
|
|
242
|
-
TODO, skipped, and conditionally skipped/run tests do not satisfy the minimum.
|
|
243
|
-
A nonempty callback is a presence check, not evidence of meaningful assertions,
|
|
244
|
-
execution, or coverage. Run the tests in CI.
|
|
245
|
-
|
|
246
|
-
Cycle detection operates on runtime ESM imports/re-exports and literal dynamic
|
|
247
|
-
imports between `domain/<module>` owners, including nested files. Type-only
|
|
248
|
-
imports do not create runtime cycle edges. Arbitrary CommonJS wrappers, computed
|
|
249
|
-
imports, implicit generated function-reference calls, and value flow are not
|
|
250
|
-
whole-program analyzed. Rules report recognizable violations; clean lint does
|
|
251
|
-
not prove arbitrary business logic is absent.
|
|
252
|
-
|
|
253
|
-
Shared ownership follows static runtime imports through shared helpers. A shared
|
|
254
|
-
file reached from only one domain is reported for relocation. Unused code,
|
|
255
|
-
type-only consumers, and computed dependencies are not treated as proof of shared
|
|
256
|
-
ownership; their ownership still needs review.
|
|
257
|
-
|
|
258
|
-
## Reserved domain files
|
|
259
|
-
|
|
260
|
-
Enable `cvx/reserved-domain-file-anatomy` to require exported Command/Query registries, declarative contracts, zodTable shapes, and table topology or createModule assembly. Constructor provenance is traced through imported aliases, namespaces, direct Foundation factories, and the root foundation.ts facade. Pair it with `cvx/domain-file-responsibilities` to reject handlers and database/runtime work in contracts.ts. Backend scope and test/generated exclusions apply; absent files are not required.
|