Skip to content

FraiseQL for Event-Driven Architectures

Teams building event-driven microservices typically need to instrument every write path — adding publish calls, handling failures, and managing retries in application code. FraiseQL’s observer system moves this to infrastructure: a TOML config tells the Rust runtime which database writes to publish as events and where to send them.

FraiseQL fires observers after a mutation commits to the database. No Python code is involved at runtime:

Client mutation
→ Rust runtime executes SQL function (transaction commits)
→ Rust runtime evaluates [[observers]] config
→ Matching events published to NATS / Redis / webhook
→ Subscriber services react

Your Python schema defines what mutations exist. The fraiseql.toml defines what events those mutations emit. No @observer decorator, no Python publish calls.

[observers]
backend = "nats"
nats_url = "${NATS_URL}"
[[observers]]
table = "tb_order"
event = "INSERT"
subject = "fraiseql.order.created"
[[observers]]
table = "tb_order"
event = "UPDATE"
subject = "fraiseql.order.updated"

[observers] (singleton) configures the backend. [[observers]] (array) declares individual rules. Multiple rules can share the same backend.

For teams without a message bus, publish events as outbound HTTP webhooks:

[observers]
backend = "webhook"
[[observers]]
table = "tb_user"
event = "INSERT"
[[observers.actions]]
type = "webhook"
url_env = "USER_CREATED_WEBHOOK_URL"

The url_env key names an environment variable — secrets stay out of config files.

In a federated deployment, each FraiseQL service publishes events to NATS independently. Downstream services subscribe:

inventory-service/fraiseql.toml
[observers]
backend = "nats"
nats_url = "${NATS_URL}"
# Subscribe to events from order-service
[[observers.subscribe]]
subject = "fraiseql.order.created"
handler = "fn_reserve_inventory"

fn_reserve_inventory is a SQL function in the inventory service’s own database. It receives the event payload as a single JSONB argument. No application-layer event bus wrapper required.

Observers move events between services. To deliver real-time updates to browser clients, use GraphQL subscriptions:

@fraiseql.subscription(
entity_type="Order",
operation="UPDATE"
)
def order_updated(customer_id: fraiseql.ID | None = None) -> Order:
"""Subscribe to order updates, optionally filtered by customer."""
pass

Clients connect via WebSocket. FraiseQL delivers events over graphql-ws protocol. NATS and subscriptions are complementary: NATS moves events between services; subscriptions push them to end users.

The Microservices Choreography example shows a complete event-driven architecture:

  • Order Service publishes fraiseql.order.created on every new order
  • Inventory Service subscribes and runs fn_reserve_inventory
  • Notification Service subscribes and sends confirmation emails
  • No direct service-to-service API calls — all coordination through NATS
  • Zero application code — observers run at the infrastructure layer, not in Python
  • Guaranteed ordering — events are published after the transaction commits, never before
  • Durable delivery — NATS JetStream provides at-least-once delivery with acknowledgements
  • Same security model — observer events carry the same mutation context (user ID, tenant ID) as the originating request
  • Transport-agnostic — observers fire on mutations from GraphQL, REST, or gRPC calls

Start with the Microservices Choreography example or the NATS Event Pipeline example for a working multi-service setup.

For a single-service starting point with webhook observers, use the fraiseql-starter-saas template and add [[observers]] config to fraiseql.toml.

Observers Guide

Configure post-mutation side effects, retries, and dead letter queues. Observers

Observer-Webhook Patterns

Event-driven architecture patterns with observers and outbound webhooks. Observer-Webhook Patterns

Subscriptions

Real-time push to clients via GraphQL WebSocket subscriptions. Subscriptions