Architecture
The mental model behind noboil — schema-first CRUD generation, branded types, lifecycle hooks, and escape hatches.
The mental model. Read this once and you can predict noboil's behavior instead of memorizing its API.
Schema-first design
You define a Zod schema once. Everything else — the database table definition, the CRUD endpoints, the TypeScript types for input validation and query results — is derived from that single source of truth.
import { schema } from 'noboil/convex/schema'
import { boolean, object, string } from 'zod/v4'
export const s = schema({
owned: {
blog: object({
title: string().min(1),
body: string(),
published: boolean().default(false)
})
}
})The schema is the contract. Change it, and the table shape, validators, and endpoint signatures all update. There is no separate v.object(...) definition to keep in sync.
Schema branding
noboil uses TypeScript's structural type system against you (in a good way). Each schema slot stamps the Zod object with an opaque brand that the table factory checks at the type level.
The brand registry below is auto-generated from lib/noboil/src/convex/server/types.ts (SchemaHintMap):
| Brand | Maker → Factory + Wrapper |
|---|---|
base | Created by makeBase() → use cacheCrud() + baseTable() |
kv | Created by makeKv() → use kv() + kvTable() |
log | Created by makeLog() → use log() + logTable() |
orgDef | Created by makeOrg() → pass to setup({ orgSchema }) |
org | Created by makeOrgScoped() → use orgCrud() + orgTable() |
owned | Created by makeOwned() → use crud() + ownedTable() |
quota | Created by makeQuota() → use quota() + quotaTable() |
singleton | Created by makeSingleton() → use singletonCrud() + singletonTable() |
Auto-injected fields and indexes per brand (sourced from lib/noboil/src/shared/factory-meta.ts):
| Slot | Brand | Wrapper | Auto-injected fields | Indexes | Description |
|---|---|---|---|---|---|
base | base | baseTable / cacheCrud | updatedAt? (optional) | (none — keyed by upstream id) | External-data cache (TTL, refresh, invalidate) |
children | child | childTable / childCrud | parentId, updatedAt | by_parent | Child-of-parent CRUD with cascade options |
kv | kv | kvTable / kv | key, updatedAt, createdAt, deletedAt? (when softDelete) | by_key (unique) | Named key → value, public reads + role-gated writes (kv) |
log | log | logTable / log | parent, seq, userId, createdAt, idempotencyKey?, deletedAt? (when softDelete) | by_parent_seq, by_idempotency | Append-only event log with atomic seq + idempotency (log) |
orgScoped | org | orgTable / orgCrud | userId, orgId, updatedAt | by_org, by_org_user | Org-scoped CRUD with membership + role checks |
org | orgDef | orgTables (via setup) | (n/a — org definition itself) | (n/a) | The org definition (passed as orgSchema to noboil()) |
owned | owned | ownedTable / crud | userId, updatedAt | by_user | User-owned CRUD |
quota | quota | quotaTable / quota | owner, timestamps[] | by_owner (unique) | Sliding-window rate limit primitive (quota) |
singleton | singleton | singletonTable / singletonCrud | userId, updatedAt | by_user | One row per user (get + upsert) |
Passing an owned-branded schema where the factory expects orgScoped is a compile-time error. The SchemaTypeError type produces a message like:
Schema mismatch: expected org-scoped (makeOrgScoped), got user-owned (makeOwned).
Created by makeOrgScoped() -> use orgCrud() + orgTable()When to use which:
owned— most tables. Each row belongs to one user.orgScoped— rows belong to an organization. Queries scope byorgId; mutations verify org membership and role.base— shared/global data with no ownership (e.g., a cache of external API responses). No auth required on reads or writes.singleton— exactly one row per user (e.g., user profile, user preferences).children— rows that have a foreign key to a parent table; access control inherits from the parent.log— append-only event tables (messages, votes, audit). Atomic per-parentseq, idempotent appends, soft-delete + restore, no-update by default. See log factory.kv— string-keyed global state (banner, feature flags, system status). Public reads, role-gated writes, optional key whitelist, conflict detection. See kv factory.quota— sliding-window rate limits per owner (anti-spam, ballot-stuffing, API throttling).check/record/consumetriad with hooks. See quota factory.
The noboil() entry point
noboil() is the single entry point. Call it once with a config object that includes a tables callback mapping each table name to a registered table:
import { noboil } from 'noboil/convex/server'
import { action, internalMutation, internalQuery, mutation, query } from './_generated/server'
import { getAuthUserId } from '@convex-dev/auth/server'
import { s } from './s'
export const api = noboil({
query, mutation, action, internalQuery, internalMutation, getAuthUserId,
orgSchema: s.team,
hooks: { /* global hooks */ },
middleware: [auditLog(), inputSanitize()],
tables: ({ table }) => ({
blog: table(s.blog, { rateLimit: { max: 10, window: 60_000 }, search: 'content' }),
wiki: table(s.wiki, { acl: true, softDelete: true }),
profile: table(s.profile),
movie: table(s.movie, { key: 'tmdbId', ttl: 86_400 })
})
})The table() helper detects each schema's brand at runtime (via a __bs marker written by schema()) and dispatches to the matching factory under the hood. The table name comes from the object key — no need to repeat it.
api is then used by per-module files to expose endpoints:
// convex/blog.ts
import { api } from '../lazy'
export const { create, update, rm, pub: { list, read, search } } = api.blogLower-level path: setup() + crud()
If you want explicit control over which factories are called, use setup() directly:
const { crud, orgCrud, childCrud, cacheCrud, singletonCrud, pq, q, m, cq, cm } = setup({
query, mutation, action, internalQuery, internalMutation, getAuthUserId
})
const { create, update, rm, pub: { list, read } } = crud('blog', s.blog, { rateLimit: { max: 10, window: 60_000 } })noboil() is built on top of this. Use whichever style you prefer.
Factories
| Factory | Input | Returns | Purpose |
|---|---|---|---|
crud(table, schema, opts?) | OwnedSchema | { create, update, rm, pub, auth, ... } | User-owned tables |
orgCrud(table, schema, opts?) | OrgSchema | { create, update, rm, list, read, ... } | Org-scoped tables |
childCrud(table, meta, opts?) | child config | { create, update, rm, list, get, ... } | Tables with a foreign key to a parent |
cacheCrud(opts) | BaseSchema + key field | { load, get, invalidate, refresh, ... } | External data cache with TTL |
singletonCrud(table, schema, opts?) | SingletonSchema | { get, upsert } | One row per user |
Custom builders
| Builder | Auth required | Context | Use for |
|---|---|---|---|
pq | No (viewer ID resolved but optional) | viewerId, withAuthor | Public queries — anyone can call, viewer info available if logged in |
q | Yes (throws if unauthenticated) | user, viewerId, get, withAuthor | Authenticated queries with ownership checks |
m | Yes | user, create, patch, delete, get | Authenticated mutations with ownership checks |
cq | No | empty | Context-free queries (internal tooling, cron jobs) |
cm | No | empty | Context-free mutations (internal tooling, cron jobs) |
Auth/pub split
Every crud() call returns two read APIs: pub and auth.
export const { create, update, rm, pub, auth } = crud('blog', owned.blog)
// In your Convex module, re-export:
export const { list, read, search } = pub // no auth required
// auth.list, auth.read, auth.search // require loginpub.list,pub.read,pub.search— callable by anyone, including unauthenticated users. Use for public-facing content.auth.list,auth.read,auth.search— require a logged-in user.auth.listautomatically scopes to the current user's rows.create,update,rm— always require authentication. Mutations verify the caller owns the document before allowing changes.
You can attach default where filters to either side:
crud('blog', owned.blog, {
pub: { where: { published: true } }, // public list only shows published
auth: { where: { archived: false } }, // authed list hides archived
})Hooks and middleware
Lifecycle hooks
Six hooks fire around each mutation. before* hooks can transform data; after* hooks are side-effect-only.
| Hook | Fires | Receives | Returns |
|---|---|---|---|
beforeCreate | Before insert | { data } | Modified data |
afterCreate | After insert | { data, id } | void |
beforeUpdate | Before patch | { id, patch, prev } | Modified patch |
afterUpdate | After patch | { id, patch, prev } | void |
beforeDelete | Before delete | { id, doc } | void |
afterDelete | After delete | { id, doc } | void |
Global vs per-table hooks
Global hooks are passed to setup() and run on every table. Their context includes a table field so you can branch:
setup({
// ...
hooks: {
afterCreate: (ctx, { id }) => {
console.log(`Created ${id} in ${ctx.table}`)
},
},
})Per-table hooks are passed in the options of each CRUD factory:
crud('blog', owned.blog, {
hooks: {
beforeCreate: (_ctx, { data }) => ({ ...data, slug: slugify(data.title) }),
},
})When both exist, global hooks run first. For before* hooks, the global hook's output is piped into the per-table hook.
Built-in middleware
Middleware is syntactic sugar over global hooks with a name field and an operation field in context. Pass them to setup():
setup({
// ...
middleware: [auditLog(), inputSanitize(), slowQueryWarn({ threshold: 300 })],
})| Middleware | What it does |
|---|---|
auditLog(opts?) | Logs create/update/delete with table, userId, and id. verbose: true includes payload. |
inputSanitize(opts?) | Strips HTML/script tags from string fields in beforeCreate and beforeUpdate. |
slowQueryWarn(opts?) | Warns when a mutation exceeds threshold ms (default 500). |
Multiple middleware compose left-to-right. They merge into the global hooks chain via composeMiddleware().
The where clause query language
All list, read, and search endpoints accept an optional where parameter. It is a typed object derived from your schema shape.
Field equality
// Exact match
where: { published: true }
// Multiple fields (AND)
where: { published: true, category: "tech" }Comparison operators
For numeric fields, use comparison operator objects instead of a direct value:
where: { rating: { $gte: 4 } }
where: { price: { $between: [10, 50] } }Available operators: $gt, $gte, $lt, $lte, $between.
The own filter
where: { own: true }Shorthand for "only rows where userId matches the authenticated caller." This works on pub endpoints too — if the caller is logged in, it scopes; if not, it is ignored.
OR groups
where: {
published: true,
or: [
{ category: "tech" },
{ category: "science" },
],
}Top-level fields are AND-ed. The or array creates a disjunction — each entry is a WhereGroupOf<S> with the same shape as the top-level (minus or itself).
Escape hatches
Custom queries alongside generated ones
The pq, q, m, cq, and cm builders returned by setup() are standard zCustomQuery/zCustomMutation wrappers. Use them in the same file as your generated CRUD:
export const { create, update, rm, pub } = crud('blog', owned.blog)
// Custom query alongside generated ones
export const trending = pq({
args: { limit: z.number().default(10) },
handler: async (ctx, { limit }) => {
// Full access to ctx.db — write any query you want
return ctx.db.query('blog')
.filter(q => q.eq(q.field('published'), true))
.order('desc')
.take(limit)
},
})pq gives you viewer info without requiring auth. q requires auth and gives you ctx.user and ctx.get (ownership-checked document fetch). m gives you ctx.create, ctx.patch, ctx.delete with automatic timestamps and ownership.
Ejecting individual tables
If a table outgrows the CRUD pattern, stop calling the factory for that table and write raw Convex functions using pq/q/m. The schema branding and table definitions are independent of the CRUD layer — you keep ownedTable(owned.blog) in your schema even after ejecting the CRUD for blog. See the ejecting guide for a full walkthrough.
Architecture overview
graph TD
S[schema] -->|brands with __bs| N[noboil]
N -->|dispatches by brand| F[crud / orgCrud / cacheCrud / singletonCrud / childCrud]
F -->|generates| E[create / list / read / update / rm endpoints]
E -->|consumed by| H[useList / useMutate / useForm hooks]
H -->|renders| C[Form / AutoForm / typed fields]SpacetimeDB dev loop
After editing your schema or reducer logic:
sequenceDiagram
participant Dev as Developer
participant CLI as SpacetimeDB CLI
participant Mod as SpacetimeDB Module
participant Gen as spacetime generate
participant App as Next.js App
Dev->>CLI: bun spacetime:publish
CLI->>Mod: Compile + deploy module
Mod-->>CLI: Module published
Dev->>Gen: bun spacetime:generate
Gen-->>Dev: TypeScript bindings updated
Dev->>App: bun dev
App-->>Dev: Client reconnects with new schemaSchema changes require both steps. If you only publish, the module updates but the client TypeScript types are stale. If you only generate, you get updated types but the server module doesn't match.
Bundled shadcn UI
readonly/ui/ is synced from cnsync — never edit by hand. All demos and the generated forms render against this set.
55 top-level components: accordion, alert, alert-dialog, aspect-ratio, avatar, badge, breadcrumb, button, button-group, calendar, card, carousel, chart, checkbox, collapsible, combobox, command, context-menu, dialog, direction, drawer, dropdown-menu, empty, field, hover-card, input, input-group, input-otp, item, kbd, label, menubar, native-select, navigation-menu, pagination, popover, progress, radio-group, resizable, scroll-area, select, separator, sheet, sidebar, skeleton, slider, sonner, spinner, switch, table, tabs, textarea, toggle, toggle-group, tooltip
- ai-elements (48):
agent,artifact,attachments,audio-player,canvas,chain-of-thought,checkpoint,code-block,commit,confirmation,connection,context,controls,conversation,edge,environment-variables,file-tree,image,inline-citation,jsx-preview,message,mic-selector,model-selector,node,open-in-chat,package-info,panel,persona,plan,prompt-input,queue,reasoning,sandbox,schema-display,shimmer,snippet,sources,speech-input,stack-trace,suggestion,task,terminal,test-results,tool,toolbar,transcription,voice-selector,web-preview
Demo coverage matrix
Auto-generated grid showing every registered table, the noboil options it ships with in lazy.ts, and which of the 5 vertical demo apps consume it (per database). Use this to find a working example of any feature.
15 tables across 10 demo apps. ✓ = the demo's frontend imports from this table.
| Table | Options | cvx-blog | cvx-chat | cvx-movie | cvx-org | cvx-poll | stdb-blog | stdb-chat | stdb-movie | stdb-org | stdb-poll |
|---|---|---|---|---|---|---|---|---|---|---|---|
blog | pub, rateLimit, search | ✓ | — | — | — | — | ✓ | — | — | — | — |
blogProfile | — | ✓ | — | — | — | — | ✓ | — | — | — | — |
chat | cascade, pub, rateLimit | — | ✓ | — | — | — | — | ✓ | — | — | — |
message | pub | — | ✓ | — | — | — | — | ✓ | — | — | — |
movie | key | — | — | ✓ | — | — | — | — | ✓ | — | — |
org | unique | — | — | — | ✓ | — | — | — | — | ✓ | — |
orgProfile | — | — | — | — | ✓ | — | — | — | — | ✓ | — |
poll | — | — | — | — | — | ✓ | — | — | — | — | ✓ |
pollProfile | — | — | — | — | — | ✓ | — | — | — | — | ✓ |
pollVoteQuota | — | — | — | — | — | ✓ | — | — | — | — | ✓ |
project | acl, cascade | — | — | — | ✓ | — | — | — | — | ✓ | — |
siteConfig | softDelete | — | — | — | — | ✓ | — | — | — | — | ✓ |
task | acl, aclFrom | — | — | — | ✓ | — | — | — | — | ✓ | — |
vote | softDelete | — | — | — | — | ✓ | — | — | ✓ | — | ✓ |
wiki | acl, rateLimit, softDelete | — | — | — | ✓ | — | — | — | — | ✓ | — |
noboil() config options
Auto-generated from NoboilOptions in lib/noboil/src/convex/server/noboil.ts. Pass these to noboil({ ... }) at the entry point.
Plus tables: ({ table }) => ({ ... }) callback (always required). 11 top-level options on SetupConfig:
| Field | Type | Required |
|---|---|---|
action | ActionBuilder<DM, 'public'> | required |
getAuthUserId | `(ctx: never) => Promise<null \ | string>` |
internalMutation | MutationBuilder<DM, 'internal'> | required |
internalQuery | QueryBuilder<DM, 'internal'> | required |
mutation | MutationBuilder<DM, 'public'> | required |
query | QueryBuilder<DM, 'public'> | required |
hooks? | GlobalHooks | optional |
middleware? | Middleware[] | optional |
orgCascadeTables? | OrgCascadeTableConfig<DM>[] | optional |
orgSchema? | ZodObject | optional |
strictFilter? | boolean | optional |
Built-in middleware
Auto-generated list of every middleware factory exported from noboil/{convex,spacetimedb}/server. Compose them in the middleware: [...] array of noboil({ ... }).
3 middleware factories (combine via middleware: [a(), b()] in noboil({ ... })). Description column auto-extracted from leading JSDoc.
| Factory | Options arg | Convex | SpacetimeDB | Description |
|---|---|---|---|---|
auditLog | `opts?: { logLevel?: 'debug' \ | 'info'; verbose?: boolean }` | ✓ | ✓ |
inputSanitize | opts?: \{ fields?: string[] \} | ✓ | ✓ | Strips control chars + zero-width chars from string fields before insert/update. Restrict to specific fields via opts.fields. |
slowQueryWarn | opts?: \{ threshold?: number \} | ✓ | ✓ | Emits warn-level log when any reducer-driven mutation exceeds threshold ms (default 500ms). |
Schema relationships
Mermaid graph derived from every parent: '...', aclFrom: { table: '...' }, cascade: [{ table: '...' }], and foreignKey reference in backend/convex/s.ts.
16 tables, 5 relationships (parent / cascade / aclFrom). Color = factory slot.
graph LR
subgraph base
movie["movie"]
end
subgraph children
message["message"]
schema["schema"]
end
subgraph kv
siteConfig["siteConfig"]
schema["schema"]
end
subgraph log
vote["vote"]
schema["schema"]
end
subgraph org
team["team"]
end
subgraph orgScoped
project["project"]
task["task"]
wiki["wiki"]
end
subgraph owned
blog["blog"]
chat["chat"]
poll["poll"]
end
subgraph quota
pollVote["pollVote"]
end
subgraph singleton
blogProfile["blogProfile"]
orgProfile["orgProfile"]
pollProfile["pollProfile"]
end
message -->|parent| chat
children -->|parent| chat
log -->|parent| poll
chat -->|cascade| message
task -->|aclFrom| project
style movie fill:#fef3c7,stroke:#333
style message fill:#fce7f3,stroke:#333
style schema fill:#fce7f3,stroke:#333
style siteConfig fill:#e0e7ff,stroke:#333
style schema fill:#e0e7ff,stroke:#333
style vote fill:#dcfce7,stroke:#333
style schema fill:#dcfce7,stroke:#333
style team fill:#fef9c3,stroke:#333
style project fill:#fed7aa,stroke:#333
style task fill:#fed7aa,stroke:#333
style wiki fill:#fed7aa,stroke:#333
style blog fill:#dbeafe,stroke:#333
style chat fill:#dbeafe,stroke:#333
style poll fill:#dbeafe,stroke:#333
style pollVote fill:#fee2e2,stroke:#333
style blogProfile fill:#f3e8ff,stroke:#333
style orgProfile fill:#f3e8ff,stroke:#333
style pollProfile fill:#f3e8ff,stroke:#333Schema fields
Every user-defined Zod field per table, parsed from backend/convex/s.ts. Auto-injected fields (userId, updatedAt, _creationTime) live in auto-fields above and are not duplicated here.
Auto-extracted from backend/convex/s.ts. 12 tables, 44 user-defined fields (auto-injected fields like userId/updatedAt are added by factories — see auto-fields above).
slot: base
movie — 14 field(s)
| Field | Zod chain |
|---|---|
backdrop_path | string().nullable() |
budget | number().nullable() |
genres | array(object(\{ id: number(), name: string() \})) |
original_title | string() |
overview | string() |
poster_path | string().nullable() |
release_date | string() |
revenue | number().nullable() |
runtime | number().nullable() |
tagline | string().nullable() |
title | string() |
tmdb_id | number() |
vote_average | number() |
vote_count | number() |
slot: children
message — 3 field(s)
| Field | Zod chain |
|---|---|
chatId | zid('chat') |
parts | array(messagePart) |
role | zenum(['user', 'assistant', 'system']) |
slot: kv
siteConfig — 0 field(s)
(no inline fields parsed — see source)
slot: log
vote — 0 field(s)
(no inline fields parsed — see source)
slot: org
team — 0 field(s)
(no inline fields parsed — see source)
slot: orgScoped
project — 4 field(s)
| Field | Zod chain |
|---|---|
description | string().optional() |
editors | array(zid('users')).max(100).optional() |
name | string().min(1) |
status | zenum(['active', 'archived', 'completed']).optional() |
task — 5 field(s)
| Field | Zod chain |
|---|---|
assigneeId | zid('users').nullable().optional() |
completed | boolean().optional() |
priority | zenum(['low', 'medium', 'high']).optional() |
projectId | zid('project') |
title | string().min(1) |
wiki — 6 field(s)
| Field | Zod chain |
|---|---|
content | string().optional() |
deletedAt | number().optional() |
editors | array(zid('users')).max(100).optional() |
slug | string() |
status | zenum(['draft', 'published']) |
title | string().min(1) |
slot: owned
blog — 7 field(s)
| Field | Zod chain |
|---|---|
attachments | files.max(5).optional() |
category | zenum(['tech', 'life', 'tutorial'], \{ error: 'Select a category' \}) |
content | string().min(3, 'At least 3 characters') |
coverImage | file.nullable().optional() |
published | boolean() |
tags | array(string()).max(5, 'Max 5 tags').optional() |
title | string().min(1, 'Required') |
chat — 2 field(s)
| Field | Zod chain |
|---|---|
isPublic | boolean() |
title | string().min(1) |
poll — 3 field(s)
| Field | Zod chain |
|---|---|
closedAt | number().nullable().optional() |
options | array(string().min(1)).min(2).max(10) |
question | string().min(1) |
slot: quota
pollVote — 0 field(s)
(no inline fields parsed — see source)
Demo route map
Every Next.js page.tsx file in web/{cvx,stdb}/{demo}/src/app/. Use this to find the runtime entry point that exercises a feature.
79 Next.js page.tsx routes across all demo apps.
Convex
| Demo | Routes |
|---|---|
blog | /, /[id], /[id]/edit, /dev, /login, /login/email, /pagination, /profile |
chat | /, /[id], /login, /login/email, /public |
movie | /, /fetch |
org | /, /dashboard, /invite/[token], /join/[slug], /login, /login/email, /members, /new, /onboarding, /projects, /projects/[projectId], /projects/[projectId]/edit, /projects/new, /settings, /wiki, /wiki/[wikiId], /wiki/[wikiId]/edit, /wiki/new |
poll | /, /[id], /[id]/edit, /login, /login/email, /profile |
SpacetimeDB
| Demo | Routes |
|---|---|
blog | /, /[id], /[id]/edit, /dev, /login, /login/email, /pagination, /profile |
chat | /, /[id], /login, /login/email, /public |
movie | /, /fetch |
org | /, /dashboard, /invite/[token], /join/[slug], /login, /login/email, /members, /new, /onboarding, /projects, /projects/[projectId], /projects/[projectId]/edit, /projects/new, /settings, /wiki, /wiki/[wikiId], /wiki/[wikiId]/edit, /wiki/new |
poll | /, /[id], /[id]/edit, /dev, /login, /login/email, /profile |
Table options inventory
How many tables in backend/{convex,spacetimedb}/lazy.ts enable each option, and which ones.
11 known table options scanned across both backend lazy.ts files. Numbers are how many tables enable each option.
| Option | cvx tables | stdb tables | Where (cvx) | Where (stdb) |
|---|---|---|---|---|
rateLimit | 4 | 2 | blog, chat, task, wiki | blog, chat |
search | 1 | 0 | blog | — |
softDelete | 3 | 3 | siteConfig, vote, wiki | siteConfig, vote, wiki |
pub | 2 | 2 | chat, message | blog, chat |
acl | 2 | 0 | project, wiki | — |
aclFrom | 1 | 0 | task | — |
cascade | 2 | 1 | chat, project | project |
key | 0 | 1 | — | movie |
unique | 0 | 1 | — | org |
ttl | 0 | 0 | — | — |
staleWhileRevalidate | 0 | 0 | — | — |
Doc-snippet syntax check
Every ```ts/tsx code fence in doc/content/docs/*.mdx is scanned by Bun.Transpiler so silent rot (a renamed export breaking an embedded snippet) shows up here.
Transpiler.scan() (from bun) over every ```ts/tsx code fence in doc/content/docs/*.mdx. Catches syntax-level rot when source code changes break embedded snippets.
557/560 blocks parseable (99%). Snippets without TypeScript-shaped syntax (config JSON, shell, mermaid) are skipped — they're counted as parseable but not actually checked.
Failures:
- doc/content/docs/base.mdx block #6: BuildMessage: Expected ";" but found ":"
- doc/content/docs/recipes.mdx block #86: BuildMessage: Expected "*/" to terminate multi-line comment
- doc/content/docs/singleton.mdx block #9: BuildMessage: Unexpected ...
Hook signature drift
When a hook signature in source diverges from how it's documented (e.g. arg order/name change), this generator flags it. Compares the first parameter name only — quick and high-precision.
Compares first argument names of every useXxx(...) call appearing in docs to the actual hook signature in lib/noboil/src/{convex,spacetimedb}/react/use-*.ts. Catches a common kind of doc rot.
31/31 hooks mentioned in docs. Of those, 0 have at least one const useX = (...)-style declaration in a code fence. 0 drift mismatches.
No drift detected.
Doc duplication audit
Long paragraphs that appear verbatim in 2+ .mdx files. Either consolidate or rewrite in place — duplication is maintenance debt.
Scans every .mdx for paragraphs ≥120 chars appearing in 2+ files. Catches accidental duplication that adds maintenance cost without adding info.
0 duplicate paragraph(s) found (across 33 doc files).
No duplicates above threshold — every long paragraph appears in exactly one file.
Factory parity audit
Every factory checked across both backends: source file present, at least one demo table registered, factory name referenced in tests, and dedicated/mentioning doc.
Per-factory parity. Each factory checked: source file present, at least one demo table registered in the entry point, table referenced by ≥1 demo app, factory name referenced in tests, and dedicated doc page.
9/9 factories at full parity.
| Slot (Brand) | Tables | Convex (src · reg · demos · tests) | SpacetimeDB (src · reg · demos · tests) | Docs | Status |
|---|---|---|---|---|---|
base (base) | 1 | ✓ src · 1/1 reg · 1 demos · ✓ tests | ✓ src · 1/1 reg · 1 demos · ✓ tests | ✓ | 🟢 |
children (child) | 1 | ✓ src · 1/1 reg · 1 demos · ✓ tests | ✓ src · 1/1 reg · 1 demos · ✓ tests | ✓ | 🟢 |
kv (kv) | 1 | ✓ src · 1/1 reg · 1 demos · ✓ tests | ✓ src · 1/1 reg · 1 demos · ✓ tests | ✓ | 🟢 |
log (log) | 1 | ✓ src · 1/1 reg · 1 demos · ✓ tests | ✓ src · 1/1 reg · 1 demos · ✓ tests | ✓ | 🟢 |
org (orgDef) | 1 | ✓ src · 1/1 reg · 1 demos · ✓ tests | ✓ src · 1/1 reg · 1 demos · ✓ tests | ✓ | 🟢 |
orgScoped (org) | 3 | ✓ src · 3/3 reg · 3 demos · ✓ tests | ✓ src · 3/3 reg · 3 demos · ✓ tests | ✓ | 🟢 |
owned (owned) | 3 | ✓ src · 3/3 reg · 3 demos · ✓ tests | ✓ src · 3/3 reg · 3 demos · ✓ tests | ✓ | 🟢 |
quota (quota) | 1 | ✓ src · 1/1 reg · 1 demos · ✓ tests | ✓ src · 1/1 reg · 1 demos · ✓ tests | ✓ | 🟢 |
singleton (singleton) | 3 | ✓ src · 3/3 reg · 3 demos · ✓ tests | ✓ src · 3/3 reg · 3 demos · ✓ tests | ✓ | 🟢 |
Factory depth (quantitative)
Beyond presence, this measures how much each factory has — source LOC, tests, hook LOC, doc mentions. Use it to spot uneven investment.
Quantitative depth per factory: source LOC, hook file LOC, test count (cases that reference the factory name), dedicated doc page LOC if any, and total mentions in all docs. Bigger numbers ≠ better quality, but large gaps signal uneven investment.
| Factory | src LOC (cvx/stdb) | hook LOC (cvx/stdb) | tests (cvx/stdb) | doc page | tabs (cvx/stdb) |
|---|---|---|---|---|---|
base | 251 / 175 | 91 / 86 | 18 / 25 | 236L | 4 / 4 ✓ |
child | 275 / 165 | 43 / 41 | 13 / 27 | 240L | 4 / 4 ✓ |
kv | 168 / 163 | 43 / 49 | 10 / 23 | 270L | 4 / 4 ✓ |
log | 392 / 274 | 77 / 78 | 12 / 17 | 357L | 5 / 5 ✓ |
orgScoped | 500 / 402 | 43 / 41 | 38 / 47 | 255L | 4 / 4 ✓ |
owned | 440 / 141 | 43 / 41 | 54 / 57 | 266L | 5 / 5 ✓ |
quota | 117 / 125 | 36 / 80 | 14 / 15 | 257L | 4 / 4 ✓ |
singleton | 76 / 115 | 30 / 41 | 23 / 30 | 227L | 4 / 4 ✓ |
Option/feature parity
Compares each factory's option surface (config keys it accepts) across both backends. Functional symmetry, not just documentation symmetry.
Per-factory option parity. For each factory, checks every expected option is textually referenced in both backends' factory file. The "intentional" column documents architecturally-justified backend-specific options (with rationale below).
52/62 option × backend cells satisfied. After intentional exemptions: 🟢 = no unaccounted-for gaps.
| Factory | cvx coverage | stdb coverage | intentional asym | status | cvx unaccounted | stdb unaccounted |
|---|---|---|---|---|---|---|
base | 5/5 | 2/5 | 3 | 🟢 | — | — |
child | 2/5 | 3/5 | 5 | 🟢 | — | — |
kv | 4/4 | 4/4 | 1 | 🟢 | — | — |
log | 3/3 | 3/3 | 3 | 🟢 | — | — |
orgScoped | 5/5 | 5/5 | 2 | 🟢 | — | — |
owned | 4/4 | 4/4 | 3 | 🟢 | — | — |
quota | 3/3 | 3/3 | 0 | 🟢 | — | — |
singleton | 1/2 | 1/2 | 2 | 🟢 | — | — |
Architectural backend-specific options (intentional)
Options that exist on one backend but not the other because the underlying database has a different runtime model:
base.fetcher(stdb-only): stdb cache fills client-side via reducers; server has no HTTP capabilitybase.hooks(stdb-only): stdb base uses table subscriptions; lifecycle hooks go on the wrapping reducerbase.staleWhileRevalidate(stdb-only): no server-side refresh in stdb model; SWR managed by client useCacheEntrychild.cascade(cvx-only): configured on parent table, not child — symmetric with stdbchild.rateLimit(cvx-only): shared with owned/orgScoped factories, not redeclared in child.tschild.softDelete(cvx-only): shared rule from CrudOptions, not redeclared in child.tschild.cascade(stdb-only): configured on parent table, not child — symmetric with cvxchild.pub(stdb-only): stdb uses subscription-based reads — pub-style filtering happens via subscription where clauses, not on child factorykv.keys(stdb-only): stdb kv uses constant string keys without runtime whitelist (typed via TS only)log.pub(stdb-only): stdb log uses subscription where clauses for visibility scopinglog.search(stdb-only): stdb log searches client-side over subscribed rowslog.withAuthor(stdb-only): stdb subscriptions return row data only; author lookup is a separate client-side joinorgScoped.aclFrom(stdb-only): stdb checks aclFrom in client-side query layer (subscription is owner-checked at the row, parent-derived ACL applied client-side)orgScoped.unique(stdb-only): stdb declares unique constraints via column attributes in module bindings, not via factory optionowned.acl(cvx-only): owned tables in cvx may opt into ACL; stdb keeps ACL strictly within orgScoped factoryowned.pub(stdb-only): stdb uses subscription where clauses for visibility scoping (no separate pub option needed)owned.search(stdb-only): stdb owned searches client-side over subscribed rowssingleton.hooks(cvx-only): cvx singletonCrud lifecycle hooks delegated to underlying mutation buildersingleton.rateLimit(stdb-only): stdb singleton has at most one row per user — rate-limit pressure is naturally bounded
Demo parity
Cross-backend audit of all 5 demo apps: routes, e2e tests, source LOC.
Per-demo parity audit. Each of the 5 demos compared across both backends: route count, e2e test count, source LOC. 5/5 demos at full parity. Backend-specific routes (intentional asymmetries) listed below.
| Demo | Routes (cvx/stdb) | E2E tests (cvx/stdb) | Source LOC (cvx/stdb) | Status |
|---|---|---|---|---|
blog | 8/8 ✓ | 52 / 52 ✓ | 881 / 945 ✓ | 🟢 |
chat | 5/5 ✓ | 26 / 26 ✓ | 488 / 410 ✓ | 🟢 |
movie | 2/2 ✓ | 14 / 14 ✓ | 303 / 524 ✓ | 🟢 |
org | 18/18 ✓ | 128 / 128 ✓ | 2366 / 2612 ✓ | 🟢 |
poll | 6/7 ✓ | 82 / 82 ✓ | 852 / 888 ✓ | 🟢 |
Backend-specific routes (intentional)
stdb/dev— SpacetimeDB SchemaPlayground dev tool (cvx has no equivalent component)moviesrc LOC asymmetry — stdb-movie does TMDB fetching client-side (no server-side action available); cvx delegates to action — architectural difference, see base.fetcher in option-parity above
Utility (non-factory) parity
Beyond factories: file upload, presence, org membership, helpers, middleware, setup, test helpers, RLS. Cross-backend audit.
Per-domain parity for non-factory utilities. Compares the export surface of each utility module across both backends. Shared exports = symbols present on both sides; cvx-only/stdb-only counts exclude documented architectural exemptions.
3/8 domains at full parity.
| Domain | shared | cvx exports | stdb exports | cvx-only (gap) | stdb-only (gap) | intentional asym | status |
|---|---|---|---|---|---|---|---|
| File upload | 4 | 4 | 4 | — | — | 0 | 🟢 |
| Presence | 4 | 4 | 4 | — | — | 0 | 🟢 |
| Org membership | 6 | 9 | 42 | — | CascadeTableConfig, OrgByUserIndexLike, OrgConfig, OrgExports, OrgFieldBuilders, OrgInviteByOrgIndexLike, OrgInviteByTokenIndexLike, OrgInvitePkLike, OrgInviteReducersConfig, OrgInviteReducersExports, OrgInviteRowLike, OrgInviteTableLike, OrgJoinReducersConfig, OrgJoinReducersExports, OrgJoinRequestByOrgIndexLike, OrgJoinRequestByOrgStatusIndexLike, OrgJoinRequestPkLike, OrgJoinRequestRowLike, OrgJoinRequestTableLike, OrgMemberByOrgIndexLike, OrgMemberPkLike, OrgMemberReducersConfig, OrgMemberReducersExports, OrgMemberRowLike, OrgMemberTableLike, OrgPkLike, OrgRole, OrgRowLike, OrgSlugIndexLike | 10 | 🟡 |
| Server helpers | 63 | 67 | 81 | ConvexErrorData, hk, isSoftDeleted | TypedFieldErrors, hkCtx | 18 | 🟡 |
| Middleware | 6 | 6 | 6 | — | — | 0 | 🟢 |
| Setup / entry | 2 | 6 | 5 | — | CrudDefaults, OrgTypeBuilders | 5 | 🟡 |
| Test helpers | 3 | 9 | 14 | OrgTestCrudConfig, TestAuthConfig | ErrorData, TestContext | 13 | 🟡 |
| RLS / subscriptions | 0 | 0 | 16 | — | FieldBuilder, RlsCategory, RlsPub, StdbDeps, StdbTable, TableFields, ZodBridgeT | 9 | 🟡 |
Intentional architectural asymmetries
- Org membership
makeInviteHandlers(cvx-only): cvx convention: handlers are server-side mutation builders (returns mutation defs) - Org membership
makeJoinHandlers(cvx-only): cvx convention: handlers are server-side mutation builders - Org membership
makeMemberHandlers(cvx-only): cvx convention: handlers are server-side mutation builders - Org membership
canEdit(stdb-only): stdb-side ACL helper invoked client-side from RLS where clause; cvx checks server-side via requireOrgRole - Org membership
makeInviteReducers(stdb-only): stdb convention: reducers are explicit table writers (parallel to cvx makeInviteHandlers) - Org membership
makeInviteToken(stdb-only): stdb invite tokens are generated reducer-side; cvx uses crypto.randomUUID inline - Org membership
makeJoinReducers(stdb-only): stdb convention: reducers (parallel to cvx makeJoinHandlers) - Org membership
makeMemberReducers(stdb-only): stdb convention: reducers (parallel to cvx makeMemberHandlers) - Org membership
makeOrgTables(stdb-only): stdb table-builder helpers; cvx tables defined declaratively in schema - Org membership
requireOrgMember(stdb-only): stdb auth check helper exposed for direct reducer use; cvx uses requireOrgRole/requireOrgEditor inside CRUD wrappers - Server helpers
handleConvexError(cvx-only): Convex-specific error wrapping; stdb uses SenderError class instead - Server helpers
applyPatch(stdb-only): reducer arg patch helper — Convex uses ctx.db.patch() directly - Server helpers
enforceRateLimit(stdb-only): stdb-side rate-limit enforcement helper — cvx uses checkRateLimit via setup - Server helpers
getFieldErrors(stdb-only): stdb field-error parsing — cvx uses ConvexError shape - Server helpers
getFirstFieldError(stdb-only): see getFieldErrors - Server helpers
getOwnedRow(stdb-only): stdb ownership-checked row fetch — cvx uses requireOwn via context - Server helpers
idFromWire(stdb-only): stdb id wire-format conversion (u32/u64) — cvx uses string Id throughout - Server helpers
idToWire(stdb-only): see idFromWire - Server helpers
identityEquals(stdb-only): stdb Identity comparison — cvx compares string ids with === - Server helpers
identityFromHex(stdb-only): stdb Identity hex conversion — cvx has no Identity type - Server helpers
identityToHex(stdb-only): see identityFromHex - Server helpers
makeError(stdb-only): stdb SenderError factory — cvx uses ConvexError - Server helpers
makeOptionalFields(stdb-only): stdb reducer arg builder helper - Server helpers
parseSenderMessage(stdb-only): stdb sender message parsing — cvx uses Convex auth context - Server helpers
pickPatch(stdb-only): stdb patch shape builder — cvx uses spread/pick inline - Server helpers
reducerArgs(stdb-only): stdb reducer arg mapper — cvx uses convex/values directly - Server helpers
resetRateLimitState(stdb-only): stdb rate-limit state reset — cvx uses internal mutation - Server helpers
timestampEquals(stdb-only): stdb Timestamp comparison — cvx uses number === - Setup / entry
api(cvx-only): cvx exports a unifiedapiproxy wrapping all functions — stdb exposestables/reducersdirectly - Setup / entry
mergeCacheHooks(cvx-only): cvx hook composition for cache factory — stdb uses inline closures - Setup / entry
mergeGlobalHooks(cvx-only): cvx hook composition for global hooks — stdb uses inline closures - Setup / entry
mergeHooks(cvx-only): see mergeGlobalHooks - Setup / entry
setupCrud(stdb-only): stdb-specific CRUD setup helper that registers reducers — cvx setup() returns helpers used per-table - Test helpers
TEST_EMAIL(cvx-only): convex-test deterministic email constant — stdb uses createTestUser instead - Test helpers
getOrgMembership(cvx-only): convex-test helper to inspect membership rows — stdb uses queryTable with filter - Test helpers
makeOrgTestCrud(cvx-only): convex-test wrapper that exposes server-side org-scoped CRUD with auth pre-baked — stdb tests use callReducer with asUser instead - Test helpers
makeTestAuth(cvx-only): convex-test auth helper — stdb uses asUser + connectAsTestUser pattern - Test helpers
asUser(stdb-only): stdb test helper to call reducer as a specific identity — cvx uses ctx.withIdentity - Test helpers
callReducer(stdb-only): stdb reducer-call wrapper — cvx uses ctx.mutation directly - Test helpers
cleanup(stdb-only): stdb test cleanup helper — cvx uses ctx.run - Test helpers
createTestUser(stdb-only): stdb deterministic test-user factory — cvx uses ensureTestUser - Test helpers
extractErrorData(stdb-only): stdb SenderError data parsing — cvx uses err.data directly - Test helpers
getErrorCode(stdb-only): see extractErrorData - Test helpers
getErrorDetail(stdb-only): see extractErrorData - Test helpers
getErrorMessage(stdb-only): see extractErrorData - Test helpers
queryTable(stdb-only): stdb test-time table query helper — cvx uses ctx.run + ctx.db.query - RLS / subscriptions
RLS_COL(stdb-only): stdb RLS column-name constants — cvx auth happens in handler, no constants - RLS / subscriptions
RLS_TBL(stdb-only): see RLS_COL - RLS / subscriptions
makeSchema(stdb-only): stdb table-builder for spacetimedb schema generation — cvx uses defineSchema - RLS / subscriptions
rlsChildSql(stdb-only): stdb child-table RLS where-clause builder - RLS / subscriptions
rlsJoinWhereSender(stdb-only): stdb join-based RLS builder - RLS / subscriptions
rlsSql(stdb-only): stdb generic RLS SQL where-clause builder - RLS / subscriptions
rlsWherePub(stdb-only): stdb pub-visibility RLS builder - RLS / subscriptions
rlsWhereSender(stdb-only): stdb sender-scoped RLS builder - RLS / subscriptions
zodToStdbFields(stdb-only): Zod → SpacetimeDB column-type mapper
Component parity
Per-file React component audit (form, fields, file upload, step form, error boundary, permission guard, editors, misc).
Per-file React component parity. Each *.tsx in lib/noboil/src/{convex,spacetimedb}/components/ cross-checked. Shared = symbol present in both files; -only = symbol present in only one.
7/9 component files at full parity.
| File | cvx | stdb | shared exports | cvx-only | stdb-only | status |
|---|---|---|---|---|---|---|
editors-section.tsx | ✓ 2L | ✓ 2L | 1 | — | — | 🟢 |
error-boundary.tsx | ✓ 10L | ✓ 10L | 1 | — | — | 🟢 |
fields.tsx | ✓ 20L | ✓ 26L | 5 | — | — | 🟢 |
file-field.tsx | ✓ 243L | ✓ 390L | 3 | — | FileApi, UploadOptions, UploadResponse | 🟡 |
form.tsx | ✓ 168L | ✓ 194L | 7 | — | — | 🟢 |
index.ts | ✓ 11L | ✓ 12L | 21 | — | UploadOptions, UploadResponse | 🟡 |
misc.tsx | ✓ 12L | ✓ 16L | 3 | — | — | 🟢 |
permission-guard.tsx | ✓ 25L | ✓ 25L | 1 | — | — | 🟢 |
step-form.tsx | ✓ 75L | ✓ 79L | 1 | — | — | 🟢 |
Mega parity (every file, every symbol)
Whole-repo audit of lib/noboil/src/{convex,spacetimedb}/ cross-backend. Walks every file, compares every export. The most exhaustive parity check — anything not on this list does not exist in the source.
Whole-repo audit. Walks every *.ts/.tsx/.json/.sh/.toml/.md/.yml/.yaml (excl. tests/codegen output via SKIP_DIRS) in 7 parallel cvx/stdb directory pairs (lib + backend + 5 demos). 150 file-level + 207 symbol-level architectural exemptions registered. Test files (*.test.ts) ARE included in file-presence check (symbol parity skipped for tests).
244 shared files · 86 cvx-only · 65 stdb-only · 31 files with cross-backend symbol divergence. Status: 🔴 104 unaccounted-for gap(s).
| Pair | shared | cvx-only | stdb-only | symbol gaps | status |
|---|---|---|---|---|---|
| lib/noboil/src | 84 | 47 (43 unaccounted) | 50 (28 unaccounted) | 31 | 🔴 |
| backend | 4 | 31 (0 unaccounted) | 6 (0 unaccounted) | 0 | 🟢 |
| web/blog | 32 | 1 (0 unaccounted) | 0 (0 unaccounted) | 0 | 🟢 |
| web/chat | 23 | 1 (0 unaccounted) | 1 (0 unaccounted) | 0 | 🟢 |
| web/movie | 16 | 1 (0 unaccounted) | 3 (2 unaccounted) | 0 | 🔴 |
| web/org | 48 | 2 (0 unaccounted) | 4 (0 unaccounted) | 0 | 🟢 |
| web/poll | 37 | 3 (0 unaccounted) | 1 (0 unaccounted) | 0 | 🟢 |
| script (naming pair) | 0 matched | 2 (0 unaccounted) | 4 (0 unaccounted) | — | 🟢 |
Unaccounted gaps
- lib/noboil/src: cvx-only file
__tests__/_budget-fakes.ts,__tests__/audit.test.ts,__tests__/budget.property.test.ts,__tests__/budget.synthetic.test.ts,__tests__/budget.test.ts,__tests__/builder.test.ts,__tests__/devtools.test.ts,__tests__/eslint-smoke.test.ts,__tests__/manifest.test.ts,__tests__/optimistic-store.test.tsx,__tests__/use-bulk-mutate.test.tsx,__tests__/use-form.test.tsx,__tests__/use-list.test.tsx,__tests__/use-mutate.test.ts,server/__tests__/setup-hooks.test.ts,server/__tests__/test-harness.test.ts,server/audit.ts,server/budget.ts,server/test-harness.ts,tools/__tests__/boundary.test.ts,tools/__tests__/dispatch.test.ts,tools/__tests__/error.test.ts,tools/__tests__/manifest.test.ts,tools/__tests__/parser.test.ts,tools/__tests__/step-sink.test.ts,tools/__tests__/to-dispatch-error.test.ts,tools/builder.ts,tools/caller-runtime.ts,tools/codegen/emit.ts,tools/codegen/extract-meta.ts,tools/codegen/index.ts,tools/codegen/scan.ts,tools/codegen/schema.ts,tools/define-provider.ts,tools/error.ts,tools/hermetic.ts,tools/http.ts,tools/index.ts,tools/manifest.ts,tools/parser.ts,tools/prompt-blocks.ts,tools/types.ts,tools/validate.ts - lib/noboil/src: stdb-only file
__tests__/check.test.ts,__tests__/migrate.test.ts,defaults.ts,react/__tests__/devtools.test.ts,react/__tests__/optimistic-store.test.tsx,react/__tests__/use-bulk-mutate.test.tsx,react/__tests__/use-list.test.tsx,react/__tests__/use-mutate.test.ts,react/use-hydrated.ts,server/__tests__/_helpers.ts,server/__tests__/cache-crud.test.ts,server/__tests__/child.test.ts,server/__tests__/crud.test.ts,server/__tests__/file.test.ts,server/__tests__/kv.test.ts,server/__tests__/log.test.ts,server/__tests__/org-crud.test.ts,server/__tests__/org-invites.test.ts,server/__tests__/org-join.test.ts,server/__tests__/org-members.test.ts,server/__tests__/org.test.ts,server/__tests__/presence.test.ts,server/__tests__/quota.test.ts,server/__tests__/rls.test.ts,server/__tests__/setup.test.ts,server/__tests__/singleton.test.ts,server/__tests__/stdb-tables.test.ts,server/__tests__/test.test.ts lib/noboil/src/components/file-field.tsx— cvx-only: — · stdb-only:FileApi,UploadOptions,UploadResponselib/noboil/src/components/index.ts— cvx-only: — · stdb-only:UploadOptions,UploadResponselib/noboil/src/index.ts— cvx-only:Api,ConflictData,ConvexErrorData,DefType,DevError,DevSubscription,ErrorData,ErrorHandler,FieldKind,FieldMeta,FieldMetaMap,FileKind,FormReturn,OrgContextValue,OrgDoc,OrgProviderProps,SoftDeleteOpts,ToastFn,ZodSchema· stdb-only:BaseSchema,DEFAULT_HTTP_URI,DEFAULT_PORT,DEFAULT_TOKEN_KEY,DEFAULT_WS_URI,InferCreate,InferReducerArgs,InferReducerInputs,InferReducerOutputs,InferReducerReturn,InferRow,InferRows,InferUpdate,OrgDefSchema,OrgSchema,OwnedSchema,Register,RegisteredDefaultError,RegisteredMeta,SchemaPhantoms,SingletonSchema,TOKEN_COOKIE_KEY,wsToHttplib/noboil/src/next/index.ts— cvx-only: — · stdb-only:ActiveOrgQuery,SqlQueryConfig,TableQueryConfiglib/noboil/src/react/devtools.ts— cvx-only: — · stdb-only:DevConnectionlib/noboil/src/react/error-toast.ts— cvx-only:ErrorHandler· stdb-only: —lib/noboil/src/react/form.ts— cvx-only: — · stdb-only:FormToastOption,Widenlib/noboil/src/react/index.ts— cvx-only:Api,ConvexCrudRefs,ConvexKvRefs,ConvexLogRefs,ConvexQuotaRefs,ConvexSingletonRefs,ListItems· stdb-only:ActiveOrgState,CreateSpacetimeClientOptions,ErrorData,ErrorHandler,FileRow,InfiniteListResult,InfiniteListWhere,KvRowBase,ListSort,ListWhere,LogRowBase,MutationFail,MutationOk,MutationResult,OrgMembership,PresenceHeartbeatArgs,QuotaRowBase,SingletonRowBase,SkipInfiniteListResult,SkipListResult,SortDirection,SortMap,SortObject,SpacetimeConnectionBuilder,SpacetimeConnectionFactory,StdbCrudRefs,StdbKvRefs,StdbLogRefs,StdbQuotaRefs,StdbSingletonRefs,TokenStore,TypedFieldErrors,UseListResult,WhereFieldValue,WhereGroup,Widen,getErrorDocsUrl,getErrorSuggestion,useStdbHydratedlib/noboil/src/react/org.tsx— cvx-only: — · stdb-only:ActiveOrgState,OrgMembershiplib/noboil/src/react/use-crud.ts— cvx-only:ConvexCrudRefs· stdb-only:StdbCrudRefslib/noboil/src/react/use-infinite-list.ts— cvx-only: — · stdb-only:InfiniteListResult,InfiniteListWhere,SkipInfiniteListResultlib/noboil/src/react/use-kv.ts— cvx-only:ConvexKvRefs· stdb-only:KvRowBase,StdbKvRefslib/noboil/src/react/use-list.ts— cvx-only:ListItems· stdb-only:ListWhere,SkipListResult,UseListResult,WhereGrouplib/noboil/src/react/use-log.ts— cvx-only:ConvexLogRefs· stdb-only:LogRowBase,StdbLogRefslib/noboil/src/react/use-presence.ts— cvx-only: — · stdb-only:PresenceHeartbeatArgs,PresenceRowlib/noboil/src/react/use-quota.ts— cvx-only:ConvexQuotaRefs· stdb-only:QuotaRowBase,StdbQuotaRefslib/noboil/src/react/use-singleton.ts— cvx-only:ConvexSingletonRefs· stdb-only:SingletonRowBase,StdbSingletonRefslib/noboil/src/react/use-upload.ts— cvx-only: — · stdb-only:UploadCallOptionslib/noboil/src/server/helpers.ts— cvx-only:ConvexErrorData,hk,isSoftDeleted· stdb-only:TypedFieldErrorslib/noboil/src/server/index.ts— cvx-only:AuditAppendInput,AuditExports,AuditHooks,AuditRow,BudgetAuditSummary,BudgetCheckResult,BudgetExports,BudgetHooks,BudgetReserveResult,ConvexErrorData,makeAudit,makeBudget,periodKeyFor· stdb-only:CrudDefaults,OrgRole,OrgTypeBuilders,StdbDeps,TestContext,TestUserlib/noboil/src/server/kv.ts— cvx-only: — · stdb-only:KvConfig,KvExports,KvHooks,KvOptions,KvRow,KvTableLikelib/noboil/src/server/log.ts— cvx-only: — · stdb-only:LogConfig,LogExports,LogHooks,LogOptions,LogRow,LogTableLikelib/noboil/src/server/org-crud.ts— cvx-only:OrgCrudOptions· stdb-only: —lib/noboil/src/server/org-invites.ts— cvx-only:InviteDocLike· stdb-only:OrgInviteByTokenIndexLike,OrgInvitePkLike,OrgInviteReducersConfig,OrgInviteReducersExports,OrgInviteRowLike,OrgInviteTableLike,OrgJoinRequestByOrgStatusIndexLike,OrgJoinRequestPkLike,OrgJoinRequestRowLike,OrgJoinRequestTableLike,OrgMemberRowLike,OrgMemberTableLike,OrgPkLike,OrgRowLikelib/noboil/src/server/org-join.ts— cvx-only:JoinRequestItem· stdb-only:OrgJoinReducersConfig,OrgJoinReducersExports,OrgJoinRequestByOrgStatusIndexLike,OrgJoinRequestPkLike,OrgJoinRequestRowLike,OrgJoinRequestTableLike,OrgMemberRowLike,OrgMemberTableLike,OrgPkLike,OrgRowLikelib/noboil/src/server/org-members.ts— cvx-only:OrgMemberItem· stdb-only:OrgMemberPkLike,OrgMemberReducersConfig,OrgMemberReducersExports,OrgMemberRowLike,OrgMemberTableLike,OrgPkLike,OrgRole,OrgRowLikelib/noboil/src/server/org.ts— cvx-only: — · stdb-only:CascadeTableConfig,OrgByUserIndexLike,OrgConfig,OrgExports,OrgFieldBuilders,OrgInviteByOrgIndexLike,OrgInviteRowLike,OrgJoinRequestByOrgIndexLike,OrgJoinRequestRowLike,OrgMemberByOrgIndexLike,OrgMemberRowLike,OrgRowLike,OrgSlugIndexLikelib/noboil/src/server/quota.ts— cvx-only: — · stdb-only:QuotaConfig,QuotaExports,QuotaRow,QuotaTableLikelib/noboil/src/server/setup.ts— cvx-only: — · stdb-only:CrudDefaults,OrgTypeBuilderslib/noboil/src/server/test.ts— cvx-only:OrgTestCrudConfig,TestAuthConfig· stdb-only:ErrorData,TestContextlib/noboil/src/zod.ts— cvx-only: — · stdb-only:UndefinedToOptional- web/movie: stdb-only file
src/app/_actions.ts,src/app/_movie-types.ts