Awesome-omni-skill axolotl-federation
Axolotl micro-federation architecture - config, schema merging, mergeAxolotls, cross-module dependencies, best practices, and troubleshooting
install
source · Clone the upstream repo
git clone https://github.com/diegosouzapw/awesome-omni-skill
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/axolotl-federation" ~/.claude/skills/diegosouzapw-awesome-omni-skill-axolotl-federation && rm -rf "$T"
manifest:
skills/development/axolotl-federation/SKILL.mdsource content
Micro-Federation
- Each domain has its own
,schema.graphql
,models.ts
, andaxolotl.tsresolvers/
merges schemas into a single supergraphaxolotl build- Each module has its own generated
for local type safetymodels.ts - 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.
// every module's axolotl.ts — type-only, no 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>(); // root src/axolotl.ts — WITH context builder (runs once at startup) const yogaAdapter = graphqlYogaWithContextAdapter<AppContext>(async (initial) => { // verifyAuth here — runs per-request 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
fields combined across modulesSubscription - Shared field definitions must match exactly
No extend type
— Use type
Declarations
extend typetypeNever use
. Axolotl merges by name, not SDL extension.extend type
# ✅ CORRECT — plain type in each module # src/modules/users/schema.graphql type AuthorizedUserQuery { me: User @resolver } # src/modules/posts/schema.graphql type AuthorizedUserQuery { posts: [Post!]! @resolver } # → merged: AuthorizedUserQuery has both `me` and `posts` # ❌ WRONG 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:
# users/schema.graphql # posts/schema.graphql type User { type User { _id: String! _id: String! # must match email: String! } } type Post { owner: User! } # → merged User has _id + email
mergeAxolotls
Behavior
mergeAxolotls- 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 /*, postsResolvers */);
Auth Gateway & Cross-Module Access
- Auth module exclusively owns
/Query.user
— never duplicate in domain modulesMutation.user - Domain modules add fields to
/AuthorizedUserQueryAuthorizedUserMutation - Domain resolvers access auth via
(set by context builder):context.authUser
export default createResolvers({ AuthorizedUserQuery: { posts: async ([, , context]) => { return prisma.post.findMany({ where: { authorId: context.authUser!._id } }); }, }, });
When to Run axolotl build
axolotl buildRun
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.