# nucleus > Convex module structure conventions, validation, and engineering decisions - Author: Chris Poulter - Repository: augmented-os/nucleus - Version: 20260125125451 - Stars: 0 - Forks: 0 - Last Updated: 2026-02-06 - Source: https://github.com/augmented-os/nucleus - Web: https://mule.run/skillshub/@@augmented-os/nucleus~nucleus:20260125125451 --- --- name: nucleus description: Convex module structure conventions, validation, and engineering decisions --- # Convex Architecture Skill Guidelines for creating and maintaining Convex modules in this codebase. ## On Skill Load (REQUIRED) When this skill loads, you MUST: 1. Run `nucleus init` 2. If "command not found": run `npm install -g nucleus-osx` then retry 3. If "Not authenticated": tell user to open the Nucleus app and sign in with GitHub 4. On success, respond with something like: "I've loaded the engineering decisions and will validate all edits against the rules. If we need to create or update decisions, I can draft changes but you'll need to approve them. What would you like to work on?" Do NOT list commands or explain the system - just confirm you're ready and ask what they need. --- ## Quick Reference | Task | Command / Resource | |------|-------------------| | **Validation (Code)** | | | Validate single file | `nucleus validate --file ` (hooks) | | Validate all modules | `nucleus validate --all` | | Validate one module | `nucleus validate --module convex/foo` | | JSON output (CI) | `nucleus validate --all --json` | | Convex build check | `npx convex dev --once --typecheck disable` | | **Decisions (View)** | | | List decisions | `nucleus decisions list` | | View decision | `nucleus decisions get ` | | **Decisions (Edit)** | | | Create new | Create `.nucleus/decisions/-name.yaml` | | Edit existing | Edit `.nucleus/decisions/-*.yaml` | | | Hook validates + syncs as draft | | Approve | Inbox → Approve (human only, via app) | | **Auth** | | | Login | Run Nucleus app → Sign in with GitHub | | Logout | Nucleus app only (no CLI logout) | | **Development** | | | Seed decisions | `npx convex run seeds/decisions/seedADR0000:seed` | ## CRITICAL: Required Reading The validation system has two layers: | Layer | Location | Purpose | |-------|----------|---------| | **Database** | Convex `decisions` + `rules` tables | Active ADRs with enabled rules (source of truth) | | **DSL Types** | `apps/nucleus-osx/src/core/validation/types.ts` | Available check types | | **Seed Scripts** | `convex/seeds/decisions/` | Reference implementations | ### Investigating Validation Errors When you see a validation error like `ADR0000_INDEX_NAMING`: 1. **Identify the decision code** (first 4 digits: `0000`) 2. **Query the decision**: ```bash nucleus decisions get 0000 ``` 3. **Check the rule's spec** for: - `suppressionMarker`: Valid exception pattern (e.g., `@global-table`) - `fix`: How to resolve the violation - `fileGlob` / `excludeGlob`: Which files are checked 4. **Reference the seed script** for full context: - `convex/seeds/decisions/seedADR0000.ts` - Contains `documentation`, `validExamples`, `invalidExamples` **Common Mistake**: Looking for ADR files in `decisions/` directory. The source of truth is the Convex database. Seed scripts are reference implementations. **Documentation:** - [decisions.md](decisions.md) - How to work with decision YAML files (create, edit, publish workflow) --- ## Universal Requirements (ALL Modules) Every module must have these files: | File | Purpose | Required? | |------|---------|-----------| | `CLAUDE.md` | Module documentation for AI assistants | **Required** | | `schema.ts` | Table definitions (or `@schema-consumer` stub) | **Required** | | `api.ts` | Public entry point at module root | Recommended | ### CLAUDE.md - Documents module purpose, structure, and key concepts - Required for all modules - **Must have YAML frontmatter** with `module` and `description` fields ```markdown --- module: rentals description: Manages rental properties and their groups tier: core dependencies: ['system/organizations'] --- # Module Name Brief description of what this module does. ## Key Files - `schema.ts` - Table definitions - `api.ts` - Public API entry point - `functions/` - Convex functions ## API Overview Available queries and mutations... ``` ### schema.ts - Export as `*Tables` object (e.g., `rentalsTables`) - Include `organizationId` for multi-tenant tables - Name indexes with `by_` prefix: `by_field1_and_field2` - Use `@schema-consumer` tag if module owns no tables ```typescript // @schema-consumer - This module uses tables from other modules export const myTables = {}; ``` ### api.ts - Thin wrapper that re-exports public queries/mutations - No business logic, database access, or complex operations - Re-export from `functions/publicQueries.ts` if present --- ## Module Entry Points Each module with a `functions/` directory should have two entry points for clean API paths: | File | Purpose | API Path | |------|---------|----------| | `api.ts` | Public functions (queries, mutations, actions) | `api.*.api.*` | | `functions.ts` | Internal re-exports from `functions/` | `internal.*.functions.*` | ### Why Two Entry Points? **Without `functions.ts`** (deep paths): ```typescript // Verbose and hard to read internal.revenue.reports.functions.internalMutations.computation.computeAllReportsForRental ``` **With `functions.ts`** (flat paths): ```typescript // Clean and consistent internal.revenue.reports.functions.computeAllReportsForRental ``` ### api.ts Pattern Re-exports public queries and mutations for external/frontend access: ```typescript // convex/revenue/reports/api.ts export { getFilterOptions, getReportData, getPeriodDetail, } from "./functions/publicQueries"; export { refreshForReservations, backfillAll, } from "./functions/publicMutations"; ``` Frontend uses: `api.revenue.reports.api.getFilterOptions` ### functions.ts Pattern Re-exports internal functions for flat internal access: ```typescript // convex/revenue/reports/functions.ts /** * Internal function re-exports for flat access. * * Instead of: internal.revenue.reports.functions.internalMutations.computation.foo * Use: internal.revenue.reports.functions.foo */ // From internalMutations/computation export { computeAllReportsForRental, computeRentalReportsForPeriods, } from "./functions/internalMutations/computation"; // From internalMutations/aggregation export { aggregateSinglePeriod, getUniquePeriods, } from "./functions/internalMutations/aggregation"; ``` Backend uses: `internal.revenue.reports.functions.computeAllReportsForRental` --- ## Root-Level Files These files may appear at the module root (outside `functions/`): | File | Purpose | When to Use | |------|---------|-------------| | `schema.ts` | Table definitions | **Required** for all modules | | `CLAUDE.md` | Module documentation | **Required** for all modules | | `api.ts` | Public entry point | Modules with public API | | `functions.ts` | Internal re-exports | Modules with `functions/` directory | | `workflows.ts` | Durable workflows | Long-running operations | | `seed.ts` | One-time seeding | Data initialization | | `testSetup.ts` | Testing utilities | Complex test fixtures | **Important:** Use `workflows.ts` (plural), not `workflow.ts`. --- ## Function Organization Choose based on module complexity: ### Flat (Simple modules) For modules with <10 functions total: ``` myModule/ ├── CLAUDE.md ├── schema.ts ├── api.ts # Re-exports publicQueries + thin mutation/action wrappers ├── publicQueries.ts # Public queries └── internal.ts # Internal functions (internalQuery/Mutation/Action) ``` ### Split (Medium modules) For modules with 10-30 functions: ``` myModule/ ├── CLAUDE.md ├── schema.ts ├── api.ts └── functions/ ├── publicQueries.ts ├── internalQueries.ts # Optional ├── internalMutations.ts └── internalActions.ts # If external APIs ``` ### Directories (Complex modules) For modules with 30+ functions: ``` myModule/ ├── CLAUDE.md ├── schema.ts ├── api.ts ├── functions/ │ ├── publicQueries.ts │ └── internalMutations/ │ ├── sync.ts │ ├── calculations.ts │ └── workflows.ts └── _logic/ # Pure business logic ├── calculations.ts └── __tests__/ └── calculations.test.ts ``` --- ## _logic/ Directory (Optional) If a module has business logic that can be tested without database: | Has _logic/? | Requirements | |--------------|--------------| | No | Fine - not all modules need pure logic separation | | Yes | Must be **pure** (no ctx, no db) AND have `__tests__/` with **80% coverage** | ### Pure Logic Rules Files in `_logic/` must NOT: - Import `MutationCtx`, `QueryCtx`, or `ActionCtx` - Import from `@/_generated/server` - Call `ctx.db.*` ```typescript // _logic/calculations.ts - CORRECT export function calculateTotal(items: Item[]): number { return items.reduce((sum, item) => sum + item.amount, 0); } // _logic/calculations.ts - WRONG export function calculateTotal(ctx: QueryCtx, items: Item[]): number { // No ctx allowed in _logic/! } ``` ### _logic/ Testing - Create `_logic/__tests__/` directory - 80% coverage required for `_logic/` files - Pure unit tests (no mocking needed) --- ## File Naming Rules Function types must match file names: | File | Purpose | Contains | |------|---------|----------| | `publicQueries.ts` or `publicQueries/` | Public query implementations | `query`, `orgQuery` with full logic | | `api.ts` | Thin wrappers only | Re-exports + mutation/action wrappers | | `internalQueries.ts` or `internalQueries/` | Backend-only queries | `internalQuery` | | `internalMutations.ts` or `internalMutations/` | Backend-only mutations | `internalMutation` | | `internalActions.ts` or `internalActions/` | Backend-only actions | `internalAction` | **Key principle:** - **Queries**: Can be public with full logic (`publicQueries.ts`) - read-only is safe - **Mutations/Actions**: Business logic in `internal*.ts`, thin wrappers in `api.ts` **Critical: API Path Generation vs File Imports** There are two different ways to reference internal functions: **File imports** - use actual filename: ```typescript import { helper } from "./internalMutations"; // ✅ Actual file path ``` **API references** - Convex strips "internal" prefix: | File Name | Generated API Path | |-----------|-------------------| | `internalQueries.ts` | `internal.*.queries.*` | | `internalMutations.ts` | `internal.*.mutations.*` | | `internalActions.ts` | `internal.*.actions.*` | ```typescript // ❌ WRONG - "internalMutations" doesn't exist in API ctx.runMutation(internal.myModule.functions.internalMutations.doSomething, {...}) // ✅ CORRECT - Convex strips "internal" prefix ctx.runMutation(internal.myModule.functions.mutations.doSomething, {...}) ``` --- ## Import Conventions - Cross-module: Use `@/` alias (e.g., `@/system/organizations/context`) - Intra-module: Use relative paths (e.g., `../_logic/calculations`) - API references: `api.*` for public, `internal.*` for internal - _logic/ imports: Only within same module (no cross-module _logic/ imports) --- ## DSL Check Types The validation system uses a JSON DSL with 24+ check types. ### Common Check Types | Type | Purpose | Example | |------|---------|---------| | `file_exists` | Require file presence | CLAUDE.md required | | `file_not_exists` | Forbid file presence | No legacy files | | `content_match` | Regex must match | Must have return validator | | `content_not_match` | Regex must NOT match | No ctx.db in _logic/ | | `directory_structure` | Required/forbidden subdirs | functions/ required | | `schema_field` | Required field in tables | organizationId required | | `schema_index` | Index naming pattern | Prefix with `by_` | | `import_restriction` | Forbidden/allowed imports | No cross-module _logic | ### Check Spec Structure ```typescript { version: "2.0", id: string, // Unique ID (used in error codes) type: string, // Check type from table above severity: "error" | "warning" | "info", message: string, // Error message (supports {placeholders}) fix?: string, // How to fix the violation suppressionMarker?: string, // Comment pattern to suppress } ``` ### Suppression Markers Add code comments to suppress specific checks: | Marker | Purpose | |--------|---------| | `@filter-ok` | Filter without index is intentional | | `@global-table` | Table intentionally has no organizationId | | `@schema-consumer` | Module owns no tables | | `@api-ok` | Complex api.ts logic is justified | ```typescript // @global-table - Cross-tenant configuration export const globalConfigTables = defineTable({ key: v.string(), value: v.any(), }); ``` --- ## Validation ### Running Validation ```bash # Single file (used by hooks) nucleus validate --file convex/system/users/schema.ts # All modules nucleus validate --all # Specific module nucleus validate --module convex/revenue/rates # JSON output (for CI) nucleus validate --all --json ``` ### How It Works 1. CLI reads auth token from `~/.nucleus/auth.json` 2. Fetches enabled rules from Convex (cached) 3. Discovers modules in `convex/` directory 4. DSL interpreter evaluates each rule's CheckSpec 5. Findings aggregated with decision context 6. Exit code: 0 = pass, 1 = errors, 2 = hook failure ### Output Format ``` ═══════════════════════════════════════════════════════════════ CONVEX MODULE VALIDATION ═══════════════════════════════════════════════════════════════ SUMMARY ─────── Total modules: 25 With _logic/: 3 (80% coverage required) ✓ Passed: 20 ⚠ Warnings: 4 ✗ Errors: 1 ERRORS ────── ADR 0006: Module Documentation Requirements │ Decision: Every module requires CLAUDE.md documenting its purpose. └─ ✗ MISSING_CLAUDE_MD convex/core/newModule/ CLAUDE.md is required for all modules Fix: Create CLAUDE.md with module/description frontmatter ``` ### Hooks Integration Validation runs automatically on file edits via Claude Code hooks: ```bash # PostToolUse hook (auto-installed) nucleus validate --file "$file_path" ``` Exit code 2 blocks the edit → immediate feedback. ### Convex Build Check **Important:** TypeScript checks don't catch all Convex bundler errors. Always run after modifying Convex functions: ```bash npx convex dev --once --typecheck disable ``` | Error Type | Example | Fix | |------------|---------|-----| | Ambiguous function name | File `sendMessage.ts` exports `sendMessage` | Rename export to `send` | | Module resolution | Convex bundler can't resolve import | Check relative paths | | Invalid API references | Using `internalMutations` instead of `mutations` | Remember Convex strips "internal" prefix | ### CI Integration ```yaml - name: Validate architecture run: nucleus validate --all --json > results.json - name: Check results run: | if jq -e '.errors > 0' results.json; then exit 1 fi ``` --- ## Testing ### Commands ```bash pnpm test # Run all tests pnpm test convex/revenue/rates # Specific module pnpm test:coverage # With coverage pnpm test:watch # Watch mode ``` ### When to Add Tests | Condition | Tests Required | |-----------|----------------| | Module has `_logic/` | Yes - 80% coverage in `_logic/__tests__/` | | Module has complex business logic | Recommended | | Module is simple CRUD | Optional | --- ## Key Decisions Decisions are stored in Convex and enforced via DSL rules. ```bash # List active decisions nucleus decisions list # View decision details nucleus decisions get 0001 ``` ### Decision Index | Code | Title | Tags | |------|-------|------| | 0000 | General Conventions | convex, multi-tenancy | | 0001 | Module Directory Structure | convex, organization | | 0002 | Functions vs _logic Separation | testing, architecture | | 0003 | API Entry Point Pattern | api, architecture | | 0004 | Internal Function Exposure | security, api | | 0005 | Test Organization | testing | | 0006 | Module Documentation Requirements | documentation | | 0007 | Workflow Component Patterns | workflows | | 0008 | Submodule Depth Limits | organization | | 0009 | Validator and Type Organization | typescript | | 0010 | Schema Ownership Pattern | schema | | 0011 | Node.js Runtime Separation | node, runtime | **Full details**: `nucleus decisions get ` or Web UI **Seed scripts**: `convex/seeds/decisions/seedADR.ts` --- ## Examples in Codebase | Module | Notable For | |--------|-------------| | `revenue/rates/` | Full _logic/ structure with tests | | `core/rentals/` | Standard module with workflows | | `integrations/hostaway/` | Integration with submodules | | `interfaces/website/` | Simple interface module |