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.dbaccess 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/serverand 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)