skills/sebas5384/agentic-stuff/convex-ddd-architecture

convex-ddd-architecture

SKILL.md

Convex DDD Architecture

Reference skill for organizing Convex projects with DDD and Hexagonal architecture. It keeps domain logic isolated from database and external API concerns so changes remain local and safer to evolve.

When to Use

Use this skill when work includes one or more of these signals:

  • New Convex sub-domain design (schema, queries, mutations, domain, adapters)
  • Legacy Convex code migration toward DDD/Hexagonal boundaries
  • Business rules drifting into handlers instead of aggregates
  • Direct ctx.db access spreading outside repositories
  • External API calls requiring retries, orchestration, or translation layers
  • Team-level need for consistent file layout and naming in Convex projects

Do not use this as a strict template for tiny prototypes where speed matters more than architectural boundaries.

Project Shape

./convex/
  _generated/                       # Auto-generated by Convex (do not edit)
  _shared/                          # Cross-domain utilities
    _libs/
      aggregate.ts                  # Base aggregate interface
      repository.ts                 # Base repository interface
  _triggers.ts                      # Central trigger registry
  customFunctions.ts                # Wrapped mutation/query exports
  schema.ts                         # Composed schema from all sub-domains
  [subDomainName]/                  # Each sub-domain folder (camelCase)
    _libs/
      stripeClient.ts               # Libs or helpers
    _tables.ts                      # Database schema tables
    _triggers.ts                    # Sub-domain trigger handlers
    _seeds.ts                       # Seeds for models
    _workflows.ts                   # Convex workflows
    queries/
      [queryName].ts                # One query per file, export default
    mutations/
      [mutationName].ts             # One mutation per file, export default
    domain/
      [modelName].model.ts          # Model schema, types, Aggregate
      [modelName].repository.ts     # Repository interface
    adapters/
      [actionName].action.ts        # External API actions
      [modelName].repository.ts     # Repository implementation

Naming Rules

  • Files: Use camelCase (contactRepository.ts, sendInvoice.action.ts)
  • Underscore prefix: For non-domain files (_tables.ts, _triggers.ts)
  • Directory vs file: Start with a file (for example _workflows.ts), split into a directory after growth

Quick Reference

Concern Rule
Convex imports Import mutation, query, internalMutation from customFunctions.ts
Function exports One function per file with export default
Domain model shape Include _id, _creationTime, plus New<Model> without system fields
Persistence boundary Access DB through repositories in adapters/
External integrations Keep translation in actions; business decisions stay in mutations/aggregates
Schema Compose root schema from each sub-domain _tables export

Core Patterns

1) Custom Functions Boundary

Always import mutation, query, internalMutation from customFunctions.ts, not from _generated/server. See custom-functions.md.

// ✅ Correct
import { mutation } from "../../customFunctions";

// ❌ Wrong - bypasses trigger integration
import { mutation } from "../../_generated/server";

2) API Path Convention

One function per file with named definition and default export:

// convex/combat/mutations/createBattle.ts
import { mutation } from "../../customFunctions";
import { v } from "convex/values";

const createBattle = mutation({
  args: { heroId: v.id("heroProfiles") },
  handler: async (ctx, args) => {
    // ...
  },
});

export default createBattle;

Frontend usage with .default suffix:

import { api } from "@/convex/_generated/api";
useMutation(api.combat.mutations.createBattle.default);
useQuery(api.economy.queries.getHeroProfile.default);

Avoid named exports like export const createBattle - this creates redundant paths like api.combat.mutations.createBattle.createBattle.

3) Schema Composition

Compose schema from sub-domain tables:

// convex/schema.ts
import { defineSchema } from "convex/server";
import { combatTables } from "./combat/_tables";
import { economyTables } from "./economy/_tables";

export default defineSchema({
  ...combatTables,
  ...economyTables,
});

4) Domain + Repository + Adapter Roles

  • Domain models and aggregates define invariants (domain-models.md)
  • Repositories isolate persistence logic (repositories.md)
  • Actions adapt external DTOs and call mutations for business transitions (adapters.md)
  • Triggers and workflows orchestrate reliable side effects (triggers.md)

5) Workflow and Trigger Safety

  • Prefer one-way flow: UI mutation -> scheduled action/workflow -> mutation -> reactive query
  • Keep trigger handlers lightweight; schedule async work when possible
  • Treat trigger code as transaction-sensitive

Common Mistakes

  • Importing handlers directly from _generated/server and bypassing shared wrappers
  • Writing business rules in actions or handlers instead of aggregates
  • Updating records with ad-hoc field mutations rather than aggregate transitions
  • Returning raw records where aggregate behavior is expected
  • Introducing required schema fields without staged migration strategy (migrations.md)

Supporting References

Weekly Installs
29
First Seen
13 days ago
Installed on
opencode29
gemini-cli29
github-copilot29
codex29
kimi-cli29
cursor29