NATS Integration
NATS Integration — Durable cross-service event streaming
FraiseQL supports two federation modes:
This page covers multi-database federation. For Apollo Federation, see Apollo Federation Support below.
FraiseQL Federation lets you expose data from multiple databases — PostgreSQL, MySQL, SQLite — through a single GraphQL endpoint. Each database is a named connection in fraiseql.toml. Queries that touch different databases are resolved in parallel and merged before the response leaves the server.
Multi-database federation is a good fit when:
Declare each database as a named entry under [databases] in fraiseql.toml:
[databases.primary]url = "${DATABASE_URL}"type = "postgresql"
[databases.analytics]url = "${ANALYTICS_DB_URL}"type = "postgresql"
[databases.legacy]url = "${LEGACY_DB_URL}"type = "mysql"FraiseQL reads type to choose the correct driver. Supported values: "postgresql", "mysql", "sqlite", "sqlserver".
Assign each query to its database with the database parameter:
import fraiseqlfrom dataclasses import dataclass
@fraiseql.typeclass User: """User record from the primary transactional database.""" id: str name: str email: str
@fraiseql.typeclass UserAnalytics: """Engagement metrics from the analytics database.""" user_id: str total_orders: int lifetime_value: float last_order_date: str
@fraiseql.query(sql_source="v_user", database="primary")def user(id: str) -> User: """Fetch a user from the primary database.""" pass
@fraiseql.query(sql_source="v_user_analytics", database="analytics")def user_analytics(user_id: str) -> UserAnalytics: """Fetch engagement metrics from the analytics database.""" passimport { type, query } from 'fraiseql';
@type()class User { id!: string; name!: string; email!: string;}
@type()class UserAnalytics { userId!: string; totalOrders!: number; lifetimeValue!: number; lastOrderDate!: string;}
@query({ sqlSource: 'v_user', database: 'primary' })async user(id: string): Promise<User> { return {} as User;}
@query({ sqlSource: 'v_user_analytics', database: 'analytics' })async userAnalytics(userId: string): Promise<UserAnalytics> { return {} as UserAnalytics;}A single GraphQL operation can reference resolvers backed by different databases. FraiseQL dispatches the sub-queries in parallel and assembles the result:
query UserDashboard($id: ID!) { user(id: $id) { id name email } userAnalytics(userId: $id) { totalOrders lifetimeValue lastOrderDate }}Expected response:
{ "data": { "user": { "id": "usr_123", "name": "Alice", "email": "alice@example.com" }, "userAnalytics": { "totalOrders": 47, "lifetimeValue": 3240.50, "lastOrderDate": "2024-01-10" } }}user is resolved against v_user on the primary PostgreSQL connection. userAnalytics is resolved against v_user_analytics on the analytics PostgreSQL connection. Both queries run concurrently. The client receives a single merged response.
FraiseQL does not execute SQL JOINs across database boundaries — that would require moving data between servers. Instead, FraiseQL resolves each database segment independently and joins results in application memory before sending the response.
What this means in practice:
| Scenario | Behaviour |
|---|---|
| Two fields from the same database | Single SQL query with a real JOIN |
| Two fields from different databases | Two parallel queries; joined in FraiseQL memory |
| Filtering by a field from another database | Not pushable to SQL; filter applied in memory |
| Ordering by a field from another database | Not pushable to SQL; sort applied in memory |
For list queries, FraiseQL batches lookups automatically. A query for 500 users followed by their analytics records produces two SQL queries — not 501:
query { users(limit: 500) { id name } # userAnalytics is fetched for all 500 users in a single batched query}When a write on one database needs to trigger an event consumed by another service, FraiseQL can integrate with NATS for durable cross-service messaging.
To enable NATS integration:
[observers]transport = "nats"
[observers.nats]url = "nats://localhost:4222"When a mutation executes, FraiseQL can publish events to NATS via the observer system. Downstream services subscribe to these events independently of their database connection.
When one database is unavailable, FraiseQL returns data from the healthy databases and null for fields backed by the unavailable one. The response includes a structured error:
{ "data": { "user": { "id": "usr_123", "name": "Alice", "email": "alice@example.com" }, "userAnalytics": null }, "errors": [ { "message": "Database 'analytics' unavailable", "path": ["userAnalytics"], "extensions": { "code": "DATABASE_UNAVAILABLE", "database": "analytics" } } ]}This allows read-heavy UIs to degrade gracefully. The client receives partial data rather than a complete failure.
A common use of federation is serving a legacy MySQL database alongside a new PostgreSQL schema during a migration. Both databases are exposed through the same API, and the client is unaware of the boundary:
[databases.primary]url = "${NEW_POSTGRES_URL}"type = "postgresql"
[databases.legacy]url = "${OLD_MYSQL_URL}"type = "mysql"@fraiseql.query(sql_source="v_order", database="primary")def order(id: str) -> Order: """New orders in PostgreSQL.""" pass
@fraiseql.query(sql_source="v_legacy_order", database="legacy")def legacy_order(id: str) -> LegacyOrder: """Orders not yet migrated, still in MySQL.""" pass@query({ sqlSource: 'v_order', database: 'primary' })async order(id: string): Promise<Order> { return {} as Order;}
@query({ sqlSource: 'v_legacy_order', database: 'legacy' })async legacyOrder(id: string): Promise<LegacyOrder> { return {} as LegacyOrder;}Once migration is complete, remove the legacy database from fraiseql.toml and deprecate the legacyOrder query.
In addition to multi-database federation, FraiseQL supports Apollo Federation v2. This allows FraiseQL to participate in a larger GraphQL architecture where multiple services expose their schemas through a gateway.
[federation]enabled = trueapollo_version = 2
[[federation.entities]]name = "User"key_fields = ["id"]
[[federation.entities]]name = "Order"key_fields = ["id"]When using Apollo Federation, define entity resolvers that the gateway can call to resolve references:
@fraiseql.entity_resolver(entity="User", key_fields=["id"])def resolve_user(id: str) -> User: """Resolve a User entity by ID for the Apollo gateway.""" pass@entityResolver({ entity: 'User', keyFields: ['id'] })async resolveUser(id: string): Promise<User> { return {} as User;}| Use Case | Recommended Approach |
|---|---|
| Single team, multiple databases | Multi-database federation |
| Multiple teams, service boundaries | Apollo Federation |
| Migrating from Apollo Gateway | Apollo Federation |
| Maximum query performance | Multi-database federation (no gateway hop) |
| Independent service deployment | Apollo Federation |
When FraiseQL acts as a subgraph in an Apollo Federation setup, a per-entity circuit breaker prevents cascading failures. After a configured number of consecutive errors the circuit opens: further calls to that entity are rejected immediately with 503 Service Unavailable and a Retry-After header rather than waiting for a timeout.
The circuit moves through three states:
| State | Behaviour |
|---|---|
| Closed | Normal operation — requests pass through |
| Open | Tripped after failure_threshold consecutive errors — requests rejected with 503 |
| HalfOpen | Recovery probe — success_threshold successes closes the circuit |
[federation.circuit_breaker]enabled = truefailure_threshold = 5 # open after N consecutive errors (default: 5)recovery_timeout_secs = 30 # probe after this many seconds (default: 30)success_threshold = 2 # successes in HalfOpen needed to close (default: 2)| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Enable the circuit breaker |
failure_threshold | integer | 5 | Consecutive errors before opening |
recovery_timeout_secs | integer | 30 | Seconds before attempting a probe |
success_threshold | integer | 2 | Successes in HalfOpen before closing |
Apply stricter thresholds to specific entities:
[[federation.circuit_breaker.per_database]]database = "Order" # name must match the federation entityfailure_threshold = 3 # stricter: open after 3 errorsrecovery_timeout_secs = 60Multiple [[federation.circuit_breaker.per_database]] entries are allowed — each with its own database name.
FraiseQL exposes a Prometheus gauge for the circuit state of each entity:
fraiseql_federation_circuit_breaker_state{entity="Order"} 1# 0 = Closed, 1 = Open, 2 = HalfOpenMulti-database federation has these limitations:
Minimize cross-database queries. If two fields are frequently requested together, consider:
Handle partial failures gracefully. When a database is unavailable, your client should be prepared to receive null for fields from that database with appropriate error handling.
Document your database boundaries. Make it clear in your schema which types come from which databases. This helps developers understand potential consistency and performance implications.
NATS Integration
NATS Integration — Durable cross-service event streaming
Multi-Database
Multi-Database — Configuration reference for multiple databases
Observers
Observers — React to cross-database changes