| name | axolotl-federation |
| description | Axolotl micro-federation architecture - config, schema merging, mergeAxolotls, cross-module dependencies, best practices, and troubleshooting |
Micro-Federation
- Each domain has its own
schema.graphql, models.ts, axolotl.ts, and resolvers/
axolotl build merges schemas into a single supergraph
- Each module has its own generated
models.ts for local type safety
- All modules built and deployed together (not distributed microservices)
Module Adapter
All modules use graphqlYogaWithContextAdapter<AppContext>() for typed context. Only root passes a context builder.
import { graphqlYogaWithContextAdapter } from '@aexol/axolotl-graphql-yoga';
import type { AppContext } from '@/src/context.js';
const yogaAdapter = graphqlYogaWithContextAdapter<AppContext>();
export const { createResolvers } = Axolotl(yogaAdapter)<Models, unknown>();
const yogaAdapter = graphqlYogaWithContextAdapter<AppContext>(async (initial) => {
return { ...initial, authUser };
});
export const { createResolvers, adapter } = Axolotl(yogaAdapter)<Models, Scalars>();
Schema Merging Rules
- Types merged by name — fields from all modules combined; conflicting field types cause build failure
- Root types auto-merged —
Query, Mutation, Subscription fields combined across modules
- Shared field definitions must match exactly
No extend type — Use type Declarations
Never use extend type. Axolotl merges by name, not SDL extension.
type AuthorizedUserQuery {
me: User @resolver
}
type AuthorizedUserQuery {
posts: [Post!]! @resolver
}
extend type AuthorizedUserQuery {
posts: [Post!]! @resolver
}
Applies to ALL shared types: Query, Mutation, AuthorizedUserQuery, AuthorizedUserMutation, and any shared domain types.
Sharing Types Across Modules
Modules can declare the same type name — shared fields must match exactly, unique fields are merged:
type User { type User {
_id: String! _id: String!
email: String! }
} type Post { owner: User! }
mergeAxolotls Behavior
- Non-overlapping resolvers — combined into one map
- Overlapping resolvers — executed in parallel, results deep-merged
- Subscriptions — first-wins; define each in exactly ONE module
import { mergeAxolotls } from '@aexol/axolotl-core';
export default mergeAxolotls(authResolvers, usersResolvers );
Auth Gateway & Cross-Module Access
- Auth module exclusively owns
Query.user / Mutation.user — never duplicate in domain modules
- Domain modules add fields to
AuthorizedUserQuery / AuthorizedUserMutation
- Domain resolvers access auth via
context.authUser (set by context builder):
export default createResolvers({
AuthorizedUserQuery: {
posts: async ([, , context]) => {
return prisma.post.findMany({ where: { authorId: context.authUser!._id } });
},
},
});
When to Run axolotl build
Run cd backend && npx @aexol/axolotl build after any schema/type/field change or federation config change.
Custom Scalars
Declare scalar Secret in each module schema that uses it. Map once in root axolotl.ts:
Axolotl(yogaAdapter)<Models<{ Secret: number }>, Scalars>();
See axolotl-server skill for createScalars implementation.