Federation Feature Reference
Federation — Full feature reference including circuit breaker and NATS
FraiseQL federation means Apollo Federation v2 subgraphs. Each FraiseQL service defines its own GraphQL schema using Python decorators, compiles it to a Rust runtime, and exposes a standard Apollo Federation subgraph endpoint. A gateway — either the built-in fraiseql gateway or Apollo Router — composes these independent subgraphs into a unified supergraph for clients.
This is fundamentally different from multi-database querying. Each FraiseQL service has one database and one compiled schema. Cross-service data access happens through Apollo Federation’s type extension and @key mechanism — not through Python code.
FraiseQL’s CQRS architecture — where each type is a single JSONB view owned by exactly one subgraph — dramatically simplifies federation query planning compared to general-purpose gateways:
| Concern | General-purpose gateway | FraiseQL gateway |
|---|---|---|
| Routing granularity | Field-level (complex query planner) | Type-level (HashMap lookup) |
| Type ownership | Multiple subgraphs can contribute fields to the same type | Each type is fully owned by one subgraph |
| Query plan complexity | Dependency graph with @requires / @provides | Fan-out + batch _entities |
| Entity resolution | Per-field fetch with data dependency ordering | Batch _entities call per subgraph |
This works because FraiseQL enforces single-owner JSONB views: a User type maps to v_user in exactly one subgraph. There is no field-level splitting to coordinate. The gateway’s query planner is a HashMap<TypeName, SubgraphUrl> — orders of magnitude simpler than a general-purpose planner.
See Federation Gateway Guide for setup and configuration.
graph TD client["Client"]
subgraph SUPERGRAPH["Apollo Router (Supergraph)"] router["Apollo Router\n(query planning + routing)"] end
subgraph USER_SVC["User Service"] user_py["schema.py\n@fraiseql.type User"] user_compiled["schema.compiled.json"] user_rust["Rust Runtime\n:4001/graphql"] user_db["PostgreSQL\ntb_user, v_user"] end
subgraph ORDER_SVC["Order Service"] order_py["schema.py\n@fraiseql.type Order"] order_compiled["schema.compiled.json"] order_rust["Rust Runtime\n:4002/graphql"] order_db["PostgreSQL\ntb_order, v_order"] end
client -- "GraphQL query" --> router router -- "subgraph query" --> user_rust router -- "subgraph query" --> order_rust
user_py -- "fraiseql compile" --> user_compiled --> user_rust order_py -- "fraiseql compile" --> order_compiled --> order_rust
user_rust --> user_db order_rust --> order_dbFraiseQL Python is schema authoring only — no runtime Python executes queries:
Python @decorators → schema.json → fraiseql compile → schema.compiled.json → Rust runtimeEach service completes this pipeline independently. The Rust runtime is a fully Apollo Federation v2 compliant subgraph server.
Apollo Federation v2 uses @key directives to declare which fields uniquely identify an entity. FraiseQL emits these directives when you annotate your types.
import fraiseqlfrom fraiseql.scalars import ID, DateTime
@fraiseql.typeclass User: """Apollo Federation entity — resolvable by id.""" id: ID name: str email: str created_at: DateTime
@fraiseql.querydef user(id: ID) -> User | None: """Look up a single user.""" return fraiseql.config(sql_source="v_user")
@fraiseql.querydef users(limit: int = 20, offset: int = 0) -> list[User]: """List all users.""" return fraiseql.config(sql_source="v_user")
fraiseql.export_schema("schema.json")The corresponding fraiseql.toml declares this as a federation subgraph and names the entity key:
[project]name = "user-service"version = "1.0.0"
[fraiseql]schema_file = "schema.json"output_file = "schema.compiled.json"
[database]type = "postgresql"url = "${DATABASE_URL}"
[federation]enabled = trueentity_key = "id"The compiled schema exposes a standard Apollo Federation subgraph with _entities and _service fields, and User @key(fields: "id") in the SDL.
The Order service defines Order types that reference User by its federation key, without importing or calling the User service from Python:
import fraiseqlfrom fraiseql.scalars import ID, DateTime, Decimalfrom enum import Enum
@fraiseql.enumclass OrderStatus(Enum): PENDING = "pending" CONFIRMED = "confirmed" SHIPPED = "shipped" DELIVERED = "delivered" CANCELLED = "cancelled"
@fraiseql.typeclass Order: """An order placed by a user.""" id: ID user_id: ID # The federation join key — matches User.id status: OrderStatus total: Decimal created_at: DateTime
@fraiseql.inputclass CreateOrderInput: user_id: ID items: list[str]
@fraiseql.querydef order(id: ID) -> Order | None: """Look up a single order.""" return fraiseql.config(sql_source="v_order")
@fraiseql.querydef orders(limit: int = 20, offset: int = 0) -> list[Order]: """List all orders.""" return fraiseql.config(sql_source="v_order")
@fraiseql.mutation(sql_source="fn_create_order", operation="CREATE")def create_order(input: CreateOrderInput) -> Order: """Create a new order.""" pass
fraiseql.export_schema("schema.json")[project]name = "order-service"version = "1.0.0"
[fraiseql]schema_file = "schema.json"output_file = "schema.compiled.json"
[database]type = "postgresql"url = "${DATABASE_URL}"
[federation]enabled = trueentity_key = "id"# Declare that Order.user_id references User.id in the user-service subgraphexternal_references = [ { type = "User", key = "id", field = "user_id" }]The Apollo Router composes all subgraph schemas into a unified supergraph. It handles query planning — splitting a client query across the appropriate subgraphs and merging results.
supergraph: listen: 0.0.0.0:4000
subgraphs: users: routing_url: http://user-service:4001/graphql orders: routing_url: http://order-service:4002/graphql
# Federation introspection: Apollo Router fetches SDL from each subgraph# at startup to build the supergraph compositionTo compose the supergraph schema using the Rover CLI:
rover supergraph compose --config supergraph.yaml > supergraph.graphqlfederation_version: =2.4.0subgraphs: users: routing_url: http://user-service:4001/graphql schema: subgraph_url: http://user-service:4001/graphql orders: routing_url: http://order-service:4002/graphql schema: subgraph_url: http://order-service:4002/graphqlOnce composed, clients query the supergraph as if it were a single GraphQL API. The Apollo Router resolves Order.user by:
user_id)user_id as the @key to fetch User from the User service via the _entities query# Client queries the Apollo Router on :4000query GetOrderWithUser { order(id: "550e8400-e29b-41d4-a716-446655440000") { id status total user { id name email } }}No Python code handles the join. The Apollo Router manages cross-service data fetching based on the compiled supergraph schema.
Each FraiseQL service independently enforces its own authorization using fraiseql.field(requires_scope=...). Authorization is compile-time configuration baked into each service’s schema.compiled.json.
from typing import Annotatedimport fraiseqlfrom fraiseql.scalars import ID, DateTime
@fraiseql.typeclass User: id: ID name: str # Requires scope to access — enforced by the User service Rust runtime email: Annotated[str, fraiseql.field(requires_scope="read:User.email")] # Mask mode: returns null for callers without the scope phone: Annotated[str | None, fraiseql.field( requires_scope="read:User.phone", on_deny="mask" )] created_at: DateTimefrom typing import Annotatedimport fraiseqlfrom fraiseql.scalars import ID, Decimal, DateTime
@fraiseql.typeclass Order: id: ID user_id: ID status: str # Only billing team can see the raw total total: Annotated[Decimal, fraiseql.field(requires_scope="billing:read")] created_at: DateTimeEach FraiseQL service owns its own PostgreSQL database following the standard trinity pattern.
-- tb_user: write targetCREATE TABLE tb_user ( pk_user BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, id UUID DEFAULT gen_random_uuid() UNIQUE NOT NULL, identifier TEXT UNIQUE NOT NULL, -- email address as human key name TEXT NOT NULL, created_at TIMESTAMPTZ DEFAULT now() NOT NULL);
CREATE UNIQUE INDEX idx_tb_user_id ON tb_user(id);CREATE UNIQUE INDEX idx_tb_user_identifier ON tb_user(identifier);
-- v_user: read side, returns JSONB for FraiseQLCREATE VIEW v_user ASSELECT u.id, jsonb_build_object( 'id', u.id::text, 'identifier', u.identifier, 'name', u.name, 'email', u.identifier, 'created_at', u.created_at ) AS dataFROM tb_user u;-- tb_order: write targetCREATE TABLE tb_order ( pk_order BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, id UUID DEFAULT gen_random_uuid() UNIQUE NOT NULL, identifier TEXT UNIQUE NOT NULL, -- order number as human key fk_user BIGINT NOT NULL, -- internal FK to tb_user in user-service (denormalized) user_id UUID NOT NULL, -- public UUID exposed in GraphQL status TEXT NOT NULL DEFAULT 'pending', total NUMERIC(12,2) NOT NULL, created_at TIMESTAMPTZ DEFAULT now() NOT NULL);
CREATE UNIQUE INDEX idx_tb_order_id ON tb_order(id);CREATE INDEX idx_tb_order_user_id ON tb_order(user_id);
-- v_order: read sideCREATE VIEW v_order ASSELECT o.id, jsonb_build_object( 'id', o.id::text, 'identifier', o.identifier, 'user_id', o.user_id::text, 'status', o.status, 'total', o.total, 'created_at', o.created_at ) AS dataFROM tb_order o;Each service follows the same build and deployment pattern:
# User servicecd user-service/python schema.py # generates schema.jsonfraiseql compile # generates schema.compiled.jsonfraiseql-server --schema schema.compiled.json --port 4001
# Order servicecd order-service/python schema.py # generates schema.jsonfraiseql compile # generates schema.compiled.jsonfraiseql-server --schema schema.compiled.json --port 4002
# Apollo Routerrover supergraph compose --config supergraph.yaml > supergraph.graphqlrouter --config router.yaml --supergraph supergraph.graphqlOr with Docker Compose:
services: user-service: build: ./user-service environment: DATABASE_URL: postgresql://postgres:password@user-db:5432/users ports: - "4001:4001"
order-service: build: ./order-service environment: DATABASE_URL: postgresql://postgres:password@order-db:5432/orders ports: - "4002:4002"
apollo-router: image: ghcr.io/apollographql/router:latest volumes: - ./router.yaml:/dist/config/router.yaml - ./supergraph.graphql:/dist/config/supergraph.graphql ports: - "4000:4000" depends_on: - user-service - order-serviceThe pattern extends naturally. A Review service that references both User and Order:
import fraiseqlfrom fraiseql.scalars import ID, DateTime
@fraiseql.typeclass Review: """A product review left by a user on an order.""" id: ID user_id: ID # References User.id in user-service order_id: ID # References Order.id in order-service rating: int body: str created_at: DateTime
@fraiseql.querydef reviews(order_id: ID | None = None, limit: int = 20) -> list[Review]: """List reviews, optionally filtered by order.""" return fraiseql.config(sql_source="v_review")
@fraiseql.inputclass CreateReviewInput: order_id: ID rating: int body: str
@fraiseql.mutation( sql_source="fn_create_review", operation="CREATE", inject={"user_id": "jwt:sub"},)def create_review(input: CreateReviewInput) -> Review: """Create a review. User ID injected from JWT.""" pass
fraiseql.export_schema("schema.json")Add to supergraph.yaml and recompose — no changes needed to the existing User or Order services.
Each FraiseQL Rust runtime exposes Prometheus metrics independently. The Apollo Router exposes additional federation-level metrics.
[security.enterprise]enabled = truelog_level = "info"Apollo Router metrics to track:
apollo_router_http_request_duration_seconds — end-to-end latencyapollo_router_subgraph_request_duration_seconds{subgraph="users"} — per-subgraph latencyapollo_router_cache_hit_count — query plan cache efficiencyUUID (ID scalar) as federation keys — never expose internal pk_ integerspk_ + id UUID + identifier TEXT) in every tableFederation Feature Reference
Federation — Full feature reference including circuit breaker and NATS
Federation Configuration
Per-Service Configuration — TOML configuration for each FraiseQL subgraph
Federation + NATS Integration
Event-Driven Patterns — Cross-service event routing with NATS
Observers
Observers Guide — Trigger side effects after mutations