Skip to content

FraiseQL vs Hasura

Both FraiseQL and Hasura provide GraphQL APIs over databases. Here’s how they differ.

Hasura is a runtime GraphQL engine that interprets queries and generates SQL on-the-fly.

FraiseQL is a compiled Rust binary that pre-generates optimized SQL views at build time — no runtime SQL generation, no resolver code.

AspectFraiseQLHasura
ArchitectureCompiled Rust binaryInterpreted runtime engine
Query executionPre-built SQL viewsRuntime SQL generation
N+1 handlingEliminated by designRuntime batching
ConfigurationTOMLConsole + YAML
Schema sourceCode (Python, TS, Go…)Database introspection
Database supportPostgreSQL, MySQL, SQLite, SQL ServerPostgreSQL (primary), others via connectors
DeploymentSingle binaryDocker container + metadata
PerformancePredictable, sub-msVariable, depends on query
Custom logicObservers (reactive events)Actions (HTTP), Remote Schemas
PricingOpen source (Apache 2.0)Open source core, paid cloud

Pricing information accurate as of February 2026. Verify current pricing at hasura.io/pricing.

If you know Hasura, here is how its concepts map to FraiseQL equivalents:

HasuraFraiseQL Equivalent
Track tableCreate SQL view (v_*)
Permissions (YAML)PostgreSQL Row-Level Security
Event triggersObservers
ActionsSQL functions + mutations
Remote schemasFederation
Computed fieldsSQL view expressions
-- Composed views: tb_ tables, v_ views, child .data embedded in parent
CREATE VIEW v_user AS
SELECT id, jsonb_build_object('id', id, 'name', name, 'email', email) AS data
FROM tb_user;
CREATE VIEW v_post AS
SELECT p.id,
jsonb_build_object('id', p.id, 'title', p.title, 'author', vu.data) AS data
FROM tb_post p
JOIN v_user vu ON vu.id = p.fk_user;

Query execution: Single indexed view lookup

-- Generated at runtime per query
SELECT u.* FROM users u WHERE u.id = $1;
SELECT p.* FROM posts p WHERE p.author_id IN ($1, $2, ...);
-- Results assembled in memory

Query execution: Multiple queries + assembly

fraiseql.toml
[project]
name = "my-api"
[database]
url = "${DATABASE_URL}"
[server]
port = 8080
Terminal window
# JWT authentication via env var (no [auth] section in fraiseql.toml)
JWT_SECRET=your-256-bit-secret

Hasura: Console + YAML + Database Metadata

Section titled “Hasura: Console + YAML + Database Metadata”
config.yaml
version: 3
metadata_directory: metadata
actions:
handler_webhook_baseurl: http://localhost:3000

Plus separate files for:

  • tables.yaml
  • relationships.yaml
  • permissions.yaml
  • remote_schemas.yaml
  • etc.
@fraiseql.type
class User:
"""User with posts."""
id: str
name: str
email: str
posts: list['Post']
  • Full IDE support
  • Type checking
  • Refactoring tools
  • Version control friendly
  1. Create tables in database
  2. Hasura introspects schema
  3. Configure relationships in console
  4. Track tables/views
  • Quick to start
  • Limited to database capabilities
  • Configuration in metadata

FraiseQL uses observers for reactive business logic — you define conditions and actions that trigger on database changes:

from fraiseql import observer, webhook, slack, email
from fraiseql.observers import RetryConfig
@observer(
entity="Order",
event="INSERT",
condition="total > 1000",
actions=[
webhook("https://api.example.com/high-value-orders"),
slack("#sales", "High-value order {id}: ${total}"),
email(
to="sales@example.com",
subject="High-value order {id}",
body="Order {id} for ${total} was created",
),
],
retry=RetryConfig(max_attempts=5, backoff_strategy="exponential"),
)
def on_high_value_order():
"""Triggered when a high-value order is created."""
pass

Built-in actions for webhooks, Slack, and email. No external services required.

actions.yaml
- name: processOrder
definition:
kind: synchronous
handler: http://business-logic-service:3000/process-order

Plus a separate HTTP service to handle the logic. Hasura also has event triggers, but they require external webhook handlers.

metadata/event_triggers/on_order_created.yaml
- name: on_order_created
definition:
enable_manual: false
insert:
columns: "*"
retry_conf:
num_retries: 3
interval_sec: 30
timeout_sec: 60
webhook: https://my-service.example.com/on-order-created
headers:
- name: X-Webhook-Secret
value_from_env: WEBHOOK_SECRET

Hasura calls your external HTTP webhook. You own and deploy a separate service to handle the event.

from fraiseql import observer, webhook, slack, email
from fraiseql.observers import RetryConfig
@observer(
entity="Order",
event="INSERT",
actions=[
webhook("https://my-service.example.com/on-order-created",
headers={"X-Webhook-Secret": "{env.WEBHOOK_SECRET}"}),
slack("#orders", "New order {id} received"),
],
retry=RetryConfig(max_attempts=3, interval_sec=30, timeout_sec=60),
)
def on_order_created():
"""Triggered when a new order is inserted."""
pass

The key difference: Hasura event triggers require you to host an external HTTP service. FraiseQL observers are declared inline in your schema code — actions like webhook, slack, and email are built-in, with no separate service required.

FraiseQL enforces validation during schema compilation, before any query executes:

  • 13 built-in validators across 4 categories
    • Standard: required, pattern, length, range, enum, checksum
    • Cross-field: comparison operators, conditionals
    • Mutual exclusivity: OneOf, AnyOf, ConditionalRequired, RequiredIfAbsent
  • No runtime overhead — validation rules are baked into the schema
  • Impossible to deploy invalid schemas — validation rule conflicts caught at build time
  • Zero database errors — invalid data never reaches the database

All validation is declarative — rules are defined via decorators in your schema code:

@fraiseql.type
class CreateUserInput:
email: Annotated[str, fraiseql.field(pattern=r"^[^@]+@[^@]+\.[^@]+$")]
age: Annotated[int, fraiseql.field(range={"min": 0, "max": 150})]
phone: Annotated[str, fraiseql.field(length=10)]

Hasura relies primarily on GraphQL’s built-in type system for validation:

  • Type checking — scalar types, required fields (from GraphQL spec)
  • Basic validation — Only what the type system provides
  • Runtime only — Validation happens during query execution
  • Custom validation via Actions — HTTP webhooks for complex logic

For anything beyond GraphQL type checking, Hasura users must implement custom Actions (external HTTP services).

AspectFraiseQLHasura
Built-in validators13 rules~3 (via GraphQL types)
Compile-time enforcement✅ Yes❌ No
Mutual exclusivityOneOf, AnyOf, ConditionalRequired, RequiredIfAbsent@oneOf only
Cross-field validation✅ Yes❌ Custom Actions required
Database protectionInvalid data impossiblePossible without Actions
ConfigurationDeclarative TOMLGraphQL directives + Actions

Hasura is a better choice when:

  • You need rapid prototyping — Point at a database, get instant API
  • Your team prefers GUI configuration — Console-based setup
  • You have an existing database — Introspection works great
  • You need event triggers — Hasura’s event system is mature
  • You want managed hosting — Hasura Cloud is polished

FraiseQL is a better choice when:

  • You want predictable performance — Compiled queries, no surprises
  • You prefer code over configuration — Schema in your language
  • You need multi-database support — PostgreSQL, MySQL, SQLite, SQL Server
  • You want simple deployment — Single Rust binary, no orchestration
  • You value readability — TOML over YAML, one file over many
  1. Start Hasura locally:

    Terminal window
    docker run -d --name hasura \
    -p 8080:8080 \
    -e HASURA_GRAPHQL_DATABASE_URL=postgresql://user:pass@host/db \
    hasura/graphql-engine:latest

    Open the console at http://localhost:8080/console

    Manual steps required:

    • Click “Data” → “Manage”
    • Track each table manually
    • Configure relationships in UI
    • Set permissions via YAML
    • Export metadata for version control
  2. Create the same API with FraiseQL:

    # fraiseql.toml - single file
    [database]
    url = "${DATABASE_URL}"
    # schema.py - code-first
    import fraiseql
    @fraiseql.type
    class User:
    id: str
    name: str
    email: str
    posts: list['Post']
    @fraiseql.type
    class Post:
    id: str
    title: str
    author: User
    Terminal window
    fraiseql run

    No manual configuration — schema derived from code automatically.

  3. Compare query performance:

    Hasura generates SQL at runtime:

    -- Generated per request
    SELECT * FROM users WHERE ...;
    SELECT * FROM posts WHERE user_id IN (...);

    FraiseQL uses pre-built views:

    -- Single view query
    SELECT data FROM v_user WHERE id = $1;
  4. Test GraphQL introspection:

    Terminal window
    # Both expose same introspection
    curl -X POST http://localhost:8080/v1/graphql \
    -H "Content-Type: application/json" \
    -d '{"query": "{ __schema { types { name } } }"}'
    curl -X POST http://localhost:8080/graphql \
    -H "Content-Type: application/json" \
    -d '{"query": "{ __schema { types { name } } }"}'
Terminal window
# Get your Hasura table definitions
hasura metadata export

Hasura table:

tables.yaml
- table:
name: users
schema: public
object_relationships:
- name: posts
using:
foreign_key_constraint_on:
column: author_id
table:
name: posts

FraiseQL equivalent:

@fraiseql.type
class User:
id: str
name: str
email: str
posts: list['Post']
@fraiseql.type
class Post:
id: str
title: str
author: User

Hasura Action:

- name: processPayment
definition:
kind: synchronous
handler: http://payments:3000/process

FraiseQL observer:

@observer(
entity="Order",
event="UPDATE",
condition="status = 'pending_payment'",
actions=[
webhook("https://payments.internal/process",
body={"order_id": "{id}", "amount": "{total}"}),
],
)
def on_pending_payment():
"""Process payment when order status changes."""
pass

Already on Hasura and want to try FraiseQL? Here are the three core steps:

  1. Install the FraiseQL CLI

    Terminal window
    # Download the FraiseQL binary
    curl -fsSL https://install.fraiseql.dev | sh
    # Verify installation
    fraiseql --version
  2. Convert your Hasura YAML to a FraiseQL schema

    Export your Hasura metadata, then rewrite each table definition as a FraiseQL type and write the corresponding SQL views (v_*) to enforce permissions:

    Terminal window
    # Export existing Hasura configuration
    hasura metadata export
    # Inspect generated metadata
    ls metadata/databases/default/tables/

    For each Hasura table, create a FraiseQL type and a SQL view. See the examples above for the conversion pattern, or refer to the full migration guide.

  3. Run FraiseQL

    Terminal window
    # Start the FraiseQL server pointing at your existing database
    fraiseql run --config fraiseql.toml

    FraiseQL will read your schema types, connect to your database, and serve the GraphQL API on the configured port. You can keep Hasura running in parallel during the transition.

For a complete walkthrough including subscription migration and permission replication, see the Migrating from Hasura guide.

Migrating from Hasura? See the step-by-step migration guide.

ChooseWhen
HasuraRapid prototyping, existing databases, GUI preference
FraiseQLPredictable performance, code-first, multi-database

Both are excellent tools. Choose based on your team’s preferences and requirements.


Performance Benchmarks

See how FraiseQL performs with real numbers. View Benchmarks

Migrate from Hasura

Step-by-step guide to moving your existing Hasura setup. Migration Guide

How FraiseQL Works

Understand the compiled, database-first architecture. Core Concepts