convex-ddd-architecture
Original:🇺🇸 English
Translated
Use when structuring or refactoring Convex codebases with Domain-Driven Design boundaries, repository abstractions, adapters for external APIs, and transaction-safe workflows.
35installs
Sourcesebas5384/agentic-stuff
Added on
NPX Install
npx skill4agent add sebas5384/agentic-stuff convex-ddd-architectureTags
Translated version includes tags in frontmatterSKILL.md Content
View Translation Comparison →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 access spreading outside repositories
ctx.db - 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 implementationNaming 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 ), split into a directory after growth
_workflows.ts
Quick Reference
| Concern | Rule |
|---|---|
| Convex imports | Import |
| Function exports | One function per file with |
| Domain model shape | Include |
| Persistence boundary | Access DB through repositories in |
| External integrations | Keep translation in actions; business decisions stay in mutations/aggregates |
| Schema | Compose root schema from each sub-domain |
Core Patterns
1) Custom Functions Boundary
Always import , , from , not from . See custom-functions.md.
mutationqueryinternalMutationcustomFunctions.ts_generated/servertypescript
// ✅ 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:
typescript
// 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 suffix:
.defaulttypescript
import { api } from "@/convex/_generated/api";
useMutation(api.combat.mutations.createBattle.default);
useQuery(api.economy.queries.getHeroProfile.default);Avoid named exports like - this creates redundant paths like .
export const createBattleapi.combat.mutations.createBattle.createBattle3) Schema Composition
Compose schema from sub-domain tables:
typescript
// 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 and bypassing shared wrappers
_generated/server - 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
- custom-functions.md
- domain-models.md
- value-objects.md
- repositories.md
- adapters.md
- triggers.md
- migrations.md
- learnings.md
- eslint-rules.md
- examples.md