The Complete Hasura GraphQL Handbook
Build a Mini Blog backend and explore every Hasura feature hands-on.
Introduction
30 Second Summary
Every tool has features you never discover until the exact moment you need them. That's fine for a text editor, but when your backend tool controls your database and API, those blind spots turn into hours of searching documentation.
In this project, you will build a "Mini Blog" backend using Hasura while systematically exploring every single feature the platform offers. By the end, you'll have a complete mental model of Hasura that you built with your own hands.
What You'll Build
A complete "Mini Blog" backend with authors, posts, comments, profiles, and tags, built on a local PostgreSQL database with a fully auto-generated GraphQL API, custom permissions, and event-driven automation.
By the end of this project, you'll have:
- A multi-table PostgreSQL database with every type of relationship, constraint, index, and computed field.
- A fully auto-generated GraphQL API with queries, mutations, subscriptions, and a complete permissions system controlling who can see and do what.
- Working examples of Actions, Event Triggers, and Cron Triggers that extend your API beyond basic CRUD with custom logic and scheduled tasks.
- Secret Mission: Build a many-to-many relationship between posts and tags using a junction table, and query it with nested GraphQL.
Are there any prerequisites?
You need Docker Desktop installed and running on your Mac. No prior database or GraphQL knowledge is needed.
This project runs entirely on your local machine using Docker containers. There are no cloud accounts, no costs, and no sign-ups required.
Before We Start
Before We Start...
Launch Hasura Locally with Docker
Before you can explore any of Hasura's features, you need a running instance to work with.
In this step, you'll download the official Hasura Docker Compose file, spin up the containers, and connect your PostgreSQL database to the Hasura Console.
In this step, get ready to:
- Download and start Hasura using Docker Compose.
- Connect your PostgreSQL database to the Hasura Console.
- Explore the Console layout and identify the main tabs.
Download and start Hasura with Docker Compose
Hasura provides an official Docker Compose file that bundles everything you need: the Hasura GraphQL Engine and a PostgreSQL database.
- Press Cmd+Space (macOS) to open your search bar.
- Type Terminal and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir hasura-handbook
cd hasura-handbook
- Download the official Hasura Docker Compose file by running this command:
curl https://raw.githubusercontent.com/hasura/graphql-engine/stable/install-manifests/docker-compose/docker-compose.yaml -o docker-compose.yml
What does this command do?
This curl command downloads the official Hasura Docker Compose configuration from GitHub and saves it as docker-compose.yml in your current folder.
The file defines three services: a postgres:15 database, the hasura/graphql-engine:v2.46.0 server, and a data connector agent.
- Confirm the file was downloaded by running:
ls
You should see docker-compose.yml in the output.
- Start the Hasura and PostgreSQL containers by running:
docker compose up -d
What does this command do?
The docker compose up -d command reads your docker-compose.yml file and starts all the services defined in it.
The -d flag runs them in "detached" mode, meaning the containers run in the background so you can keep using your terminal.
You should see output indicating that the containers are being created and started.
- Verify all containers are running by running:
docker compose ps
You should see three services listed with a status of running or Up.
Containers not starting?
- Make sure Docker Desktop is running. You should see the Docker whale icon in your menu bar.
- If you see a port conflict error mentioning port 8080, another application is using that port. Stop the other application and try again.
- If the download failed, check your internet connection and re-run the curl command.
Help me troubleshoot my Docker containers not starting for Hasura.
Open the Hasura Console and connect the database
Your Hasura server is now running locally. The Console is a web-based GUI where you manage your database and test your auto-generated GraphQL API.
- Open your web browser.
- Navigate to http://localhost:8080/console in the address bar.
You should see the Hasura Console load with a welcome screen.
What is the Hasura Console?
The Hasura Console is a GUI for managing your Hasura instance. You use it to create tables, set up relationships, configure permissions, and test your GraphQL API.
It runs entirely in your browser but talks to the Hasura server running in your Docker container on port 8080.
Now you need to tell Hasura about the PostgreSQL database running alongside it. The database is already running in a container, but Hasura doesn't automatically connect to it as a data source.
- Click the Data tab in the top navigation bar.
- Click Connect Database.
- Enter default in the Database Display Name field.
- Under Connect Database Via, select Environment Variable.
- Enter PG_DATABASE_URL as the environment variable name.
- Click Connect Database.
You should see a success message confirming the database is connected. The sidebar on the left will now show your default database with its public schema.
Why use an environment variable instead of a connection string?
The PG_DATABASE_URL environment variable is already defined in your docker-compose.yml file. It points to postgres://postgres:postgrespassword@postgres:5432/postgres.
Using an environment variable means you can change the database without modifying your Hasura configuration. In a real project, you would swap this value for your staging or production database URL.
Connection failed?
- Make sure you typed PG_DATABASE_URL exactly (all caps, with underscores). This must match the variable name in your docker-compose.yml file.
- Run docker compose ps in your terminal to confirm the postgres container is running. If it's not, run docker compose up -d again.
Help me fix my Hasura database connection error.
Explore the Console layout
Now that your database is connected, take a moment to orient yourself in the Console. There are four main tabs across the top navigation that you'll use throughout this project.
- Click through each of the four tabs in the top navigation bar to see what they contain:
- Data is where you manage your database. You create tables, set up relationships, configure permissions, and browse your data here.
- Actions is where you extend your GraphQL API with custom business logic. You'll use this later in the project.
- Events is where you configure Event Triggers (react to database changes) and Cron Triggers (run tasks on a schedule).
- API is the GraphiQL explorer where you test your auto-generated GraphQL API. This is where you'll run queries and mutations throughout the project.
What is GraphiQL?
GraphiQL is an interactive playground for writing and testing GraphQL queries. It gives you auto-complete, documentation, and instant results. Think of it as a command line specifically for your API.
- Click back to the Data tab when you're done exploring.
You'll spend most of your time in the Data tab for the next several steps, then branch out to Actions and Events later.
Your Hasura environment is fully set up and your database is connected. Next up, you'll create your first table and see Hasura automatically generate a GraphQL API from it.
Understand Schemas and Create Your First Table
Your Hasura instance is running and connected to PostgreSQL. Now it's time to create the database structure that will power your Mini Blog's GraphQL API.
Every database needs a way to organize its tables. In this step, you'll learn how schemas work, create your first table, and see Hasura instantly generate a full GraphQL API from it.
In this step, get ready to:
- Create a new schema to understand how databases organize tables.
- Build your first table with columns, data types, and a primary key.
- Run your first GraphQL query against real data.
Create a schema to organize your tables
A schema is like a folder that organizes your tables inside a database. PostgreSQL comes with a default schema called public, which is where most tables live.
You'll create a separate schema called blog to see how schemas work. Your actual tables will go in public for simplicity.
- In the Hasura Console, click the Data tab in the top navigation bar.
- Click SQL in the left-hand sidebar to open the SQL editor.
- Create the blog schema by pasting this SQL into the editor:
CREATE SCHEMA blog;
- Click the Run button to execute the statement.
What does this SQL do?
This creates a new empty schema called blog inside your PostgreSQL database. Think of it as creating a new folder to hold tables separately from the default public schema.
- Look at the left sidebar under your database. You should see blog appear as a new schema option alongside public.
Don't see the blog schema in the sidebar?
Make sure you clicked Run after pasting the SQL. If you see an error saying the schema already exists, that means it was created successfully on a previous attempt.
Try refreshing your browser page to see it appear in the sidebar.
Help me troubleshoot my schema creation in Hasura
Create your first table using the Console UI
Now you'll create the authors table in the public schema. This table stores information about each blog author.
- In the Data tab, make sure the public schema is selected in the sidebar.
- Click the Create Table button at the top of the page.
- Enter authors as the table name.
- Add the following columns one by one using the column fields:
- Column id: Type Integer (auto-increment).
- Column name: Type Text. Check the Not Null checkbox.
- Column email: Type Text. Check the Unique checkbox.
- Column bio: Type Text. Leave it nullable (no checkboxes).
- In the Primary Key section, open the dropdown and select id from the list of columns.
- Click Add table to create the table.
What is a primary key?
A primary key is a column that uniquely identifies each row in a table. No two rows can have the same primary key value. By making id auto-increment, the database assigns the next number automatically every time you add a row.
After clicking Add table, you should see the authors table appear in the left sidebar under the public schema.
Table not appearing in the sidebar?
Double-check that you selected id as the primary key. Hasura requires a primary key to track the table.
If you see an error about a missing primary key, scroll down to the Primary Key dropdown and select id before clicking Add table again.
Help me fix my table creation error in Hasura
Insert sample data and run your first GraphQL query
A table with no data isn't very interesting. Let's add some authors, then use the auto-generated GraphQL API to query them.
- Click authors in the left sidebar to open the table.
- Click the Insert Row tab at the top.
- Leave the id field empty (it auto-increments).
- Enter Sarah Chen for name.
- Enter sarah@example.com for email.
- Enter Tech writer and educator for bio.
- Click Save to insert the first row.
- Repeat the process to add a second author: name = Marcus Johnson, email = marcus@example.com, leave bio empty.
- Add a third author: name = Priya Patel, email = priya@example.com, bio = Full-stack developer.
Now for the moment of truth. Hasura has already generated a complete GraphQL API for your authors table.
- Click the API tab in the top navigation bar to open the GraphiQL explorer.
- Clear any existing content in the query editor, then paste this query:
query {
authors {
id
name
email
bio
}
}
- Click the play button (the triangle icon) to run the query.
What does this query do?
This is a GraphQL query asking for all rows in the authors table. You specify exactly which columns you want back (id, name, email, bio). Hasura generated this entire query API automatically just because you created and tracked the table.
You didn't write a single line of backend code. No routes, no controllers, no resolvers. Hasura did it all from your table definition.
You should see a JSON response on the right side containing all three authors with their details.
Seeing an empty result or an error?
If you see "authors": [] (empty array), go back to the Data tab and check Browse Rows on the authors table to confirm your rows were saved.
If you see a field-not-found error, make sure the column names in your query exactly match what you created: id, name, email, bio.
Help me debug my first GraphQL query in Hasura
You just went from zero to a working GraphQL API in minutes. Next up, you'll expand your database with more tables for posts and comments.
Create Tables, Columns, Rows, and Understand Data Types
In the previous step, you created your first table and watched Hasura auto-generate a full GraphQL API from it. One table is a great start, but a blog needs more than just authors.
A real blog has posts and comments too. In this step, you'll create two more tables and learn how different data types control what kind of information each column can hold.
In this step, get ready to:
- Create a posts table with multiple data types.
- Create a comments table and insert sample data into both tables.
- Explore your data using the Browse Rows tab with filtering and sorting.
Create the posts table
Every blog post needs a title, content, a reference to who wrote it, a published status, and a timestamp. Each of these requires a different data type to tell the database what kind of value the column can store.
- In the Hasura Console, click the Data tab in the top navigation bar.
- Click Create Table in the main content area.
- Enter posts in the Table Name field.
Now add the following columns one by one using the column fields below the table name:
- Column 1: Name id, Type Integer (auto-increment).
- Column 2: Name title, Type Text. Check the Not Null checkbox.
- Column 3: Name content, Type Text. Leave Nullable (no Not Null checkbox).
- Column 4: Name author_id, Type Integer. Check the Not Null checkbox.
- Column 5: Name is_published, Type Boolean. Set the Default Value to false.
- Column 6: Name created_at, Type Timestamp. Set the Default Value to now().
- In the Primary Key section at the bottom, select id.
- Click Add Table.
You should see a success message and the posts table appear in the left sidebar under the public schema.
What do these data types mean?
Integer stores whole numbers (1, 2, 3). With auto-increment, the database assigns the next number automatically so you never have to set it yourself.
Text stores any string of characters. Boolean stores only true or false. Timestamp stores a date and time value.
Choosing the right data type matters because it prevents bad data from entering your database. A Boolean column will never accidentally store a paragraph of text.
Table not appearing in the sidebar?
Make sure you clicked Add Table at the bottom of the form. If you see an error about a missing primary key, double-check that you selected id in the Primary Key dropdown.
Help me troubleshoot my posts table creation in Hasura
Create the comments table and insert sample data
Comments are the third piece of your blog. Each comment belongs to a specific post, so it needs a post_id column that references which post it's attached to.
- Click Create Table again from the Data tab.
- Enter comments in the Table Name field.
Add the following columns:
- Column 1: Name id, Type Integer (auto-increment).
- Column 2: Name post_id, Type Integer. Check Not Null.
- Column 3: Name author_name, Type Text. Check Not Null.
- Column 4: Name body, Type Text. Check Not Null.
- Column 5: Name created_at, Type Timestamp. Set the Default Value to now().
- Set the Primary Key to id.
- Click Add Table.
You should now see both posts and comments listed in the sidebar under the public schema alongside your authors table.
Now insert some sample data into both new tables so you have something to explore.
- Click posts in the left sidebar.
- Click the Insert Row tab.
- Leave id empty (it auto-increments).
- Enter My First Post for title.
- Enter This is the content of my first blog post. for content.
- Enter 1 for author_id (this should match an existing author's id).
- Leave is_published as its default (false).
- Leave created_at empty (it defaults to now()).
- Click Save.
- Insert a second post with title Learning Hasura, content Hasura makes GraphQL easy., author_id 1, and set is_published to true.
Now add sample data to the comments table.
- Click comments in the left sidebar.
- Click the Insert Row tab.
- Enter 1 for post_id.
- Enter Alice for author_name.
- Enter Great post, thanks for sharing! for body.
- Click Save.
- Insert a second comment with post_id of 1, author_name Bob, and body I learned a lot from this..
Why does author_id store a number and not a name?
The author_id column stores the id number of an author from the authors table. This is how databases link rows across tables. Instead of duplicating the author's full name in every post, you just store a reference number.
In a later step, you'll formalize this link with a foreign key constraint that enforces the rule: every author_id must point to a real author.
Browse your data with filters and sorting
Columns define the structure of your table (like headers in a spreadsheet). Rows are the actual data entries that fill in those columns. The Browse Rows tab lets you explore your rows visually and filter them down to exactly what you need.
- Click posts in the left sidebar.
- Click the Browse Rows tab.
You should see all your posts displayed in a table format with each column as a header and each row as an entry.
- In the filter section at the top, select is_published from the column dropdown.
- Set the operator to equals.
- Enter true as the value.
- Click Run query.
Only your published post should appear. This is the same kind of filtering that your GraphQL API uses behind the scenes.
- Clear the filter by removing the filter row.
- In the Sort section, select created_at and choose descending order.
- Click Run query.
Your posts now appear with the newest first. Try the same filter and sort options on the comments table to get comfortable navigating your data.
Columns vs rows
Think of it like a spreadsheet. Columns are the headers across the top (title, content, author_id). They define the structure of your data.
Rows are the entries going downward. Each row is one actual blog post, one actual comment, or one actual author. When you add a new post, you're adding a row. When you add a new field (like a word count), you're adding a column.
Your blog now has three tables with real data in them. Next up, you'll learn how to modify your table structure after creation and add powerful constraints that protect your data from invalid entries.
Use the Modify Tab - Primary Keys and Column Operations
You have three tables with sample data, and your GraphQL API is already auto-generating queries and mutations for all of them.
Now it is time to explore the most powerful tab in the Hasura Console: the Modify tab. This is where you change a table's structure after it has been created. You can add columns, remove them, configure constraints, and more.
In this step, get ready to:
- Explore the Modify tab and understand primary keys.
- Practice adding, renaming, and deleting columns.
- Use the Frequently Used Columns shortcut to add an updated_at column.
Navigate to the Modify tab and understand primary keys
Every table has a Primary Key column that uniquely identifies each row. No two rows can ever share the same primary key value. Your authors table uses id as its primary key.
- In the Hasura Console, click the Data tab in the top navigation bar.
- Click authors in the left sidebar under the public schema.
- Click the Modify tab at the top of the table view.
You should see a list of all columns in the authors table, showing each column's name, data type, whether it is nullable, and its default value.
What am I looking at?
The Modify tab is your table's control panel. Each column is listed with its properties.
The id column is marked as the Primary Key. This means every row in the table has a unique id value that never repeats. If you try to insert a row with a duplicate id, the database will reject it.
Why do primary keys matter?
Primary keys give you a guaranteed way to find exactly one row. When Hasura generates your GraphQL API, it creates special queries like authors_by_pk that let you fetch a single row by its primary key value.
Practice adding, renaming, and deleting a column
The Modify tab is where you change your table's structure after creation. You will practice the three most common column operations: adding, renaming, and deleting.
- On the authors Modify tab, scroll down to the Add a new column section.
- Enter website in the column name field.
- Set the data type to Text.
- Leave the Nullable checkbox checked (this means the column can be empty).
- Click Add column.
You should see website appear in the column list above.
What just happened?
You added a new column to an existing table without losing any data. All existing rows now have a website column with a null value (since it is nullable and has no default).
Now rename the column. The Hasura Console does not support renaming columns directly through its UI, so you will use a SQL command.
- Click SQL in the left sidebar (under the Data tab).
- Rename the column by running this SQL statement:
ALTER TABLE public.authors RENAME COLUMN website TO website_url;
- Click Run! to execute the statement.
Why use SQL for renaming?
Hasura's Console UI does not have a button for renaming columns directly. Using the SQL runner is the standard way to rename a column in Hasura.
- Navigate back to Data and click authors in the sidebar.
- Click the Modify tab.
You should see website_url in the column list where website used to be.
Finally, delete the column since you will not need it for the blog project.
- Find the website_url column in the Modify tab's column list.
- Click the Remove button (trash icon) next to it.
- Confirm the deletion when prompted.
The website_url column disappears from the list. The column and all its data are permanently gone.
Column deletion is destructive
Unlike adding a column (which is always safe), deleting a column removes all data stored in it permanently. In a production database, always back up your data before dropping columns.
Column still showing after rename?
If website_url still shows as website, try refreshing your browser tab. Hasura may need a moment to pick up schema changes made via raw SQL.
Why isn't my column rename showing up?
Add an updated_at column using Frequently Used Columns
Hasura provides a shortcut for columns that almost every table needs. The +Frequently used columns button pre-fills common column configurations like id, created_at, and updated_at so you do not have to remember the exact settings.
- Click posts in the left sidebar.
- Click the Modify tab.
- Click the +Frequently used columns button.
- Choose updated_at from the options.
- Click Add column.
You should see updated_at appear in the posts table's column list with a timestamp data type.
What are Frequently Used Columns?
These are pre-configured column templates that Hasura provides for patterns you will use in almost every table.
- id: an auto-incrementing integer primary key.
- created_at: a timestamp that defaults to the current time when a row is inserted.
- updated_at: a timestamp that automatically updates whenever a row is modified.
Don't see the Frequently used columns button?
Make sure you are on the Modify tab (not the Browse Rows or Insert Row tab). The button appears near the Add a new column section.
Where is the Frequently used columns button?
You now know how to modify any table's structure after creation. Next up, you will explore constraints that enforce rules on your data.
Use the Modify Tab - Constraints (Not Null, Unique, Check)
In the last step, you explored the Modify tab and practiced adding, renaming, and deleting columns. You now know how to change a table's structure after creation.
But structure alone does not protect your data. Without rules, someone could insert an author with no name or create two accounts with the same email address. Constraints are the database-level rules that prevent invalid data from ever being stored.
In this step, get ready to:
- Test the Not Null constraint on the authors table.
- Test the Unique constraint on the authors table.
- Add and test a Check constraint on the posts table.
Understand Not Null constraints
A Not Null constraint means a column must always have a value. It can never be left empty. This is useful for fields that are essential to the identity of a row.
- In the Hasura Console, click the Data tab in the top navigation.
- Click authors in the left sidebar.
- Click the Modify tab.
Look at the list of columns. Notice that name has a Not Null indicator (it cannot be empty), while bio is nullable (it can be left blank).
- Click the Insert Row tab.
- Leave the name field completely empty.
- Fill in any value for email (e.g. test@example.com).
- Click Save.
You should see an error message. The database rejected your insert because name violates the Not Null constraint.
What just happened?
PostgreSQL enforced the Not Null rule at the database level. No matter how the data arrives (Console, API, direct SQL), the constraint blocks any row that has a missing name value.
This is stronger than application-level validation because it cannot be bypassed. Even if your app has a bug, the database still protects the data.
Understand Unique constraints
A Unique constraint means no two rows can have the same value in a given column. This is perfect for fields like email addresses where duplicates would cause confusion.
- Return to the Modify tab of the authors table.
- Scroll down to the Unique Keys section.
- Observe that email already has a Unique constraint from when you created the table.
Now test what happens when you try to violate this constraint.
- Click the Insert Row tab.
- Enter a name (e.g. Duplicate Author).
- Enter an email address that already exists in your table (copy one from the Browse Rows tab).
- Click Save.
You should see an error indicating a unique constraint violation. The database rejected the duplicate email.
Why Unique constraints matter
Without a Unique constraint on email, two authors could sign up with the same address. Login systems, password resets, and notifications would all break because the system cannot tell the two accounts apart.
The Unique Keys section in the Modify tab lets you add new unique constraints or remove existing ones after table creation.
Add a Check constraint to the posts table
Check constraints are custom rules that your data must satisfy. Unlike Not Null (which only checks for emptiness) or Unique (which only checks for duplicates), a Check constraint can enforce any Boolean condition you define.
- Click posts in the left sidebar.
- Click the Modify tab.
- Scroll down to the Check Constraints section.
- In the constraint name field, enter title_min_length.
- In the check expression field, enter char_length(title) > 0.
- Click Add (or Save) to create the constraint.
What does this constraint do?
The expression char_length(title) > 0 checks that the title column has at least one character. This prevents posts with completely empty titles from being inserted or updated.
Check constraints are evaluated as a Boolean expression. If the expression returns true, the row is accepted. If it returns false, the operation is rejected.
Now test your new constraint.
- Click the Insert Row tab of the posts table.
- Leave the title field as an empty string (type nothing or just a blank space then delete it).
- Fill in author_id with a valid author ID (e.g. 1).
- Click Save.
You should see an error indicating the check constraint was violated. Your title_min_length rule rejected the empty title.
Constraint not showing up?
Make sure you scrolled far enough down in the Modify tab. The Check Constraints section is below the columns list and other constraint sections.
If your constraint was not saved, double check that you entered the expression exactly as char_length(title) > 0 with no extra spaces or missing parentheses.
Still stuck? Help me troubleshoot adding a check constraint in the Hasura Console Modify tab.
You now understand the three main types of column constraints. Next up, you will set up foreign keys and cascading rules to connect your tables together.
Add Foreign Keys and Configure Cascading
Right now your tables exist in isolation. The posts table has an author_id column, but nothing stops you from entering an author ID that doesn't exist. That means your database could end up full of orphaned data pointing to authors that were never created.
Foreign keys solve this problem by enforcing that a value in one table must actually exist in another table. In this step, you'll add foreign keys to link your tables together and configure cascading behavior so that deleting a record automatically cleans up related data.
In this step, get ready to:
- Add a foreign key from posts.author_id to authors.id with cascading delete.
- Add a foreign key from comments.post_id to posts.id with cascading delete.
- Test the full cascade chain and create a profiles table with a one-to-one foreign key.
Add a foreign key from posts to authors
A foreign key constraint enforces that a value in one column must match a value that already exists in another table. This prevents invalid data from sneaking in.
- In the Hasura Console, click the Data tab in the top navigation.
- Click posts in the left sidebar to select the posts table.
- Click the Modify tab.
- Scroll down to the Foreign Keys section and click Add.
- Set the Reference Table to authors.
- Under the column mapping, set From to author_id and To to id.
- Set the On Update Violation dropdown to restrict.
- Set the On Delete Violation dropdown to cascade.
- Click Save.
What is a foreign key doing here?
You just told PostgreSQL: every value in posts.author_id must match a real id in the authors table. If you try to insert a post with author_id = 999 and no author with that ID exists, the database will reject it.
This is how relational databases maintain data integrity across tables.
You should see the foreign key appear in the Foreign Keys section with the mapping author_id → authors.id displayed.
Don't see the Save button or getting an error?
Make sure you selected the correct reference table (authors) and that the column types match. Both posts.author_id and authors.id must be integers.
If you have existing rows in posts with an author_id value that doesn't exist in authors, the constraint will fail. Delete those orphaned rows first, then try again.
Help me fix my foreign key constraint error.
Understand cascading and configure the comments foreign key
When you set up the posts FK, you chose ON DELETE CASCADE. This tells the database what to do when the referenced row is deleted.
What do the cascade options mean?
- RESTRICT blocks the delete entirely if related rows exist.
- CASCADE automatically deletes all related rows too.
- SET NULL sets the foreign key column to NULL in related rows.
- SET DEFAULT resets the foreign key column to its default value.
- NO ACTION is similar to RESTRICT but checked at the end of the transaction.
CASCADE is the most aggressive option. If you delete an author, all their posts vanish automatically. Use it when child data has no meaning without its parent.
Now add the same kind of foreign key from comments to posts.
- Click comments in the left sidebar.
- Click the Modify tab.
- Scroll down to Foreign Keys and click Add.
- Set Reference Table to posts.
- Set From to post_id and To to id.
- Set On Update Violation to restrict.
- Set On Delete Violation to cascade.
- Click Save.
You should see the foreign key listed as post_id → posts.id in the Foreign Keys section.
Why CASCADE on both foreign keys?
You now have a chain: authors → posts → comments. If you delete an author, CASCADE deletes their posts. Because posts also have CASCADE to comments, those comments are deleted too. One delete at the top cleans up everything below.
Test the cascade chain and create a profiles table
Time to see cascading in action. You need at least one author with a linked post and comment to test this properly.
- Click authors in the left sidebar, then click the Insert Row tab.
- Insert an author with name set to Test Author and email set to test@example.com. Note the id value returned (likely 1 or the next available number).
- Click posts in the sidebar, then Insert Row. Set title to Cascade Test and author_id to your author's ID.
- Click comments in the sidebar, then Insert Row. Set post_id to the post's ID, author_name to Commenter, and body to Nice post.
- Now go back to authors and click the Browse Rows tab.
- Click the trash icon next to the test author to delete them.
- Switch to posts → Browse Rows. The post linked to that author is gone.
- Switch to comments → Browse Rows. The comment linked to that post is also gone.
One delete triggered a chain reaction across three tables. That is cascading in action.
Post or comment still showing up?
Double-check that you set On Delete Violation to cascade (not restrict) on both foreign keys. You can verify by clicking the table, going to Modify, and checking the Foreign Keys section.
If the cascade option is wrong, delete the foreign key and re-add it with the correct settings.
Help me figure out why my cascade delete isn't working.
Now create a profiles table that demonstrates a one-to-one foreign key pattern.
- Click Create Table in the left sidebar (or use the Add Table button in the Data tab).
- Set the table name to profiles.
- Add a column id with type Integer (auto-increment).
- Add a column author_id with type Integer. Check the Unique checkbox and uncheck Nullable.
- Add a column twitter_handle with type Text.
- Add a column avatar_url with type Text.
- Set id as the Primary Key.
- In the Foreign Keys section at the bottom, click Add.
- Set Reference Table to authors, From to author_id, and To to id.
- Click Add Table to create the table.
Why is author_id marked Unique?
A regular foreign key allows many rows to point to the same parent (many posts can share one author). But when the FK column is also UNIQUE, each parent can only be referenced once. This makes it a true one-to-one relationship: each author gets exactly one profile, and each profile belongs to exactly one author.
Confirm the profiles table appears in the left sidebar under your public schema.
Your tables are now properly linked with foreign keys and cascading rules. Next up, you'll learn how Hasura's tracking system controls which tables appear in your GraphQL API.
Track and Untrack Tables
So far, every table you created through the Hasura Console was automatically exposed in your GraphQL API. But what actually controls whether a table appears in your API or stays hidden?
The answer is tracking. In this step, you will learn what tracking means, see the difference between tracked and untracked tables, and practice toggling a table's visibility in your API.
In this step, get ready to:
- Understand what tracking means and find your list of tracked tables.
- Create a table via raw SQL and track it to expose it in GraphQL.
- Untrack and drop a table to see the difference between the two actions.
Understand what tracking means in Hasura
When a table is "tracked", Hasura knows about it and generates GraphQL queries, mutations, and subscriptions for it. When a table is "untracked", it still exists in the database but Hasura completely ignores it.
- In the Hasura Console, click the Data tab in the top navigation.
- In the left sidebar, click default to expand it, then click public.
- Look at the list of tables under the public schema.
You should see your four tables listed: authors, posts, comments, and profiles. These are all currently tracked, which is why they appear in your GraphQL API.
What does tracked mean?
Think of tracking as a light switch. When a table is tracked (switch ON), Hasura generates a full GraphQL API for it. Queries, mutations, subscriptions, filtering, pagination. All automatic.
When a table is untracked (switch OFF), the table still exists in PostgreSQL with all its data intact. Hasura just pretends it does not exist. Tables created through the Console UI are tracked by default. Tables created via raw SQL are not.
Create a table via SQL and track it
To see untracking in action, you need a table that starts out untracked. Creating a table through raw SQL (instead of the Console UI) gives you exactly that.
- Navigate to Data in the top navigation, then click SQL in the left sidebar.
- Create a temporary test table by pasting this SQL and clicking Run:
CREATE TABLE public.temp_test (id serial PRIMARY KEY, note text);
What does this SQL do?
This creates a simple table called temp_test in the public schema. It has an auto-incrementing id column as the primary key and a note text column. You are using this as a disposable table to experiment with tracking.
- After running the SQL, navigate back to Data and expand default → public in the left sidebar.
- Look for a section labeled Untracked Tables/Views. You should see temp_test listed there.
- Click the Track button next to temp_test.
The table now moves from the "Untracked" section into the tracked tables list in the sidebar.
- Click the API tab in the top navigation to open the GraphiQL explorer.
- Run this query to confirm temp_test is now part of your GraphQL API:
query {
temp_test {
id
note
}
}
What should I see?
You should see an empty array result like {"data": {"temp_test": []}}. The table is empty (you have not inserted any rows), but the important thing is that the query works at all. Hasura recognizes the table and generated a GraphQL query for it.
Seeing an error about temp_test not existing?
Make sure you clicked Track on the table after creating it via SQL. If you only ran the SQL but did not track it, the table exists in PostgreSQL but Hasura does not expose it in GraphQL.
Go back to Data → default → public and look for temp_test under Untracked Tables/Views.
help me track my temp_test table in Hasura
Untrack and drop the test table
Now you will reverse the process. First you will untrack the table (hide it from Hasura while keeping it in the database), then drop it entirely (delete it from the database).
- Click the Data tab in the top navigation.
- In the left sidebar, click temp_test to open it.
- At the top of the table page, look for and click the Untrack button.
- Confirm the action when prompted.
The table disappears from the sidebar and from your GraphQL API. It is now invisible to Hasura.
- Switch to the API tab and try running the same query again:
query {
temp_test {
id
note
}
}
You should see an error saying the field does not exist. Hasura no longer exposes this table in the API.
Why is this useful?
Untracking is useful when you have database tables that should not be exposed via your API. Internal logging tables, temporary migration tables, or sensitive data tables can all live in your database without ever appearing in your GraphQL schema.
The table still exists in PostgreSQL though. To actually delete it from the database, you need to drop it with SQL.
- Navigate to Data → SQL in the left sidebar.
- Drop the table by pasting this SQL and clicking Run:
DROP TABLE public.temp_test;
Untracking vs dropping
These are two completely different actions. Untracking hides a table from Hasura but keeps it intact in the database with all its data. Dropping permanently deletes the table and all its data from PostgreSQL.
Think of untracking as closing the blinds on a window. The room is still there. Dropping is demolishing the room entirely.
- Navigate back to Data → default → public in the sidebar.
- Confirm that temp_test no longer appears anywhere. Not in the tracked list, not in the untracked list. It is gone from the database entirely.
Still seeing temp_test in the sidebar?
Try refreshing the browser page. If it still appears under "Untracked Tables/Views", it means the DROP TABLE command did not run successfully. Go back to Data → SQL and run the drop statement again.
help me drop my temp_test table in Hasura
You now understand the full lifecycle of table visibility in Hasura. Next up, you will set up relationships between your tables so one GraphQL query can fetch data from multiple tables at once.
Set Up Object Relationships (One-to-One and Many-to-One)
Your tables have foreign keys connecting them, but Hasura doesn't automatically use those keys to link data in your GraphQL API. You need to tell Hasura about the connections by creating relationships.
An Object Relationship means "this row points to exactly ONE row in another table." Each post has one author. Each profile belongs to one author. In this step, you'll set up these Object Relationships and see how they unlock nested GraphQL queries.
In this step, get ready to:
- Track the suggested Object Relationship on the posts table.
- Test nested GraphQL queries that pull data from multiple tables.
- Add a one-to-one Object Relationship from authors to profiles.
Track the suggested Object Relationship on posts
Hasura automatically detects foreign keys and suggests relationships you can enable with one click. Since posts.author_id references authors.id, Hasura knows each post points to exactly one author.
- In the Hasura Console sidebar, click the posts table under public.
- Click the Relationships tab at the top of the table view.
- You should see a suggested relationship based on the foreign key from author_id to authors.id.
- Click Add next to the suggested Object Relationship.
- Name the relationship author and click Save.
What is an Object Relationship?
An Object Relationship returns a single object (one row) when you query it. It maps to situations where the current table has a foreign key pointing to another table.
In this case, each post has one author_id, so the relationship returns one author object. The relationship name (author) becomes the nested field name in your GraphQL queries.
You should now see author listed under Object Relationships on the posts table's Relationships tab.
Don't see the suggested relationship?
Make sure the foreign key from posts.author_id to authors.id exists. Check the Modify tab on the posts table and scroll to the Foreign Keys section. If it's missing, add it first.
Help me figure out why Hasura isn't suggesting a relationship on my posts table.
Test the Object Relationship in the API tab
The real payoff of relationships is that a single GraphQL query can now pull data from multiple tables at once. Instead of querying posts and then separately looking up each author, you get everything in one request.
- Click the API tab in the top navigation bar to open the GraphiQL explorer.
- Clear any existing query in the editor and paste the following query:
query {
posts {
id
title
author {
name
email
}
}
}
- Click the Play button (the triangle icon) to run the query.
What does this query do?
This query fetches all posts with their id and title. The nested author field uses the Object Relationship you just created to pull the author's name and email from the authors table in the same request.
Without relationships, you would need two separate queries and then manually match them in your application code. Hasura does this join automatically behind the scenes.
You should see your posts data with the nested author information included in each post object. Each post's author field shows the name and email of the author it belongs to.
Seeing null for the author field?
This means the author_id value in your post doesn't match any existing id in the authors table. Go to Data → posts → Browse Rows and check the author_id values match real author IDs.
Help me debug why my nested author query returns null.
Add a one-to-one Object Relationship from authors to profiles
Your profiles table has a UNIQUE constraint on author_id. This guarantees each author can have at most one profile, making it a true one-to-one relationship.
Before adding the relationship, insert some sample profile data so you can test the query afterwards.
- In the sidebar, click the profiles table.
- Click the Insert Row tab.
- Enter a value for author_id that matches an existing author's id (for example, 1).
- Enter a twitter_handle value (for example, @janedev).
- Optionally enter an avatar_url value.
- Click Save.
- Insert one more profile row for another author if you have multiple authors.
Now set up the Object Relationship from authors to profiles.
- In the sidebar, click the authors table.
- Click the Relationships tab.
- Hasura should suggest a relationship to the profiles table based on the foreign key from profiles.author_id to authors.id.
- Click Add next to the suggested relationship.
- Name it profile and click Save.
Why is this a one-to-one relationship?
Both Object Relationships and one-to-one relationships return a single object. The difference is the UNIQUE constraint. Because profiles.author_id is marked UNIQUE, the database guarantees that only one profile row can ever reference a given author.
Without the UNIQUE constraint, it would be a one-to-many relationship (one author could have many profiles). The constraint is what makes it strictly one-to-one.
Now test it with a nested query. Switch to the API tab and run this query:
query {
authors {
name
email
profile {
twitter_handle
avatar_url
}
}
}
- Click the Play button to run the query.
You should see each author with their nested profile data. Authors who don't have a profile row yet will show null for the profile field.
Not seeing the suggested relationship on authors?
The suggestion appears because the foreign key lives on the profiles table pointing back to authors. Make sure the foreign key from profiles.author_id to authors.id exists by checking the Modify tab on the profiles table.
Help me add the Object Relationship from authors to profiles.
You now have Object Relationships working in both directions. Next up, you'll set up Array Relationships so you can query in the other direction and fetch ALL posts for a given author.
Set Up Array Relationships (One-to-Many)
Your Object Relationships are connecting posts to their single author. Now it's time to go the other direction and fetch all posts belonging to an author.
Array Relationships capture this "one-to-many" direction. Without them, you'd need separate queries to get an author and then their posts.
In this step, get ready to:
- Add an Array Relationship from authors to posts.
- Add an Array Relationship from posts to comments.
- Test deeply nested GraphQL queries spanning three tables.
Track the suggested array relationship on authors
Since posts.author_id references authors.id, Hasura already knows that one author can have many posts. It suggests this as an Array Relationship on the authors table.
- In the Data tab, click the authors table in the sidebar.
- Click the Relationships tab.
- Find the suggested Array Relationship pointing to posts.
- Click Add to create it.
- Name the relationship posts.
- Click Save.
You should see posts listed as an Array Relationship on the authors table's Relationships tab.
Object vs Array relationships
An Object Relationship means "this row points to ONE row in another table." An Array Relationship means "many rows in another table point to THIS row."
The foreign key lives on the posts table (the "many" side), but the Array Relationship is defined on the authors table (the "one" side).
Don't see a suggested relationship?
Make sure the foreign key from posts.author_id to authors.id exists. Check it under the posts table's Modify tab in the Foreign Keys section.
If the relationship was already tracked from an earlier step, it will appear in the existing relationships list rather than the suggested list.
Why isn't Hasura suggesting my array relationship?
Test nested queries and add the comments relationship
With the Array Relationship in place, a single GraphQL query can now fetch authors along with all their posts. This is the power of relationships in Hasura.
- Switch to the API tab.
- Clear any existing query in the GraphiQL editor.
- Run this query to fetch authors with their posts:
query {
authors {
name
posts {
title
is_published
}
}
}
What does this query do?
The posts field nested inside authors fetches all posts belonging to each author. Each author returns an array (list) of their posts.
This is one GraphQL query hitting two tables behind the scenes. Without relationships, you'd need two separate queries and manual matching.
You should see each author with a nested array of their posts in the response panel on the right.
Now add an Array Relationship from posts to comments so each post can return its comments.
- Navigate back to the Data tab.
- Click the posts table in the sidebar.
- Click the Relationships tab.
- Find the suggested Array Relationship pointing to comments.
- Click Add to create it.
- Name the relationship comments.
- Click Save.
You should see comments listed as an Array Relationship under the posts table.
- Switch to the API tab and test a deeply nested query by running:
query {
authors {
name
posts {
title
comments {
body
}
}
}
}
What does this query do?
This query crosses three tables in one request: authors → posts → comments. Each level nests inside the previous one.
Hasura joins the data automatically using the relationships you defined. No custom resolver code needed.
You should see each author with their posts, and each post with its comments. Three levels of nesting in one query.
Seeing null for comments?
Make sure you have comment rows inserted in the comments table with valid post_id values. If the post_id doesn't match an existing post, the comments array will be empty.
Also check that you named the relationship exactly comments (lowercase). The field name in your query must match the relationship name.
Why is my nested GraphQL query returning empty arrays?
Understand the naming conventions
Hasura names its relationships by what they return in GraphQL. The direction depends on which table you're looking from.
Why 'Object' and 'Array'?
An Object Relationship returns a single JSON object (one author). An Array Relationship returns a JSON array (a list of posts).
In database terms: Object = many-to-one or one-to-one. Array = one-to-many.
From posts, looking at author is an Object Relationship because each post has one author. From authors, looking at posts is an Array Relationship because each author has many posts. Same foreign key, two directions.
Your Array Relationships are live, and you can now query across three tables in a single request. Next, you'll explore manual relationship configuration for cases where foreign keys aren't available.
Configure Manual Relationships
Every relationship you've built so far was suggested automatically by Hasura based on foreign key constraints.
But what happens when you need to connect tables that don't have a foreign key between them? Database views, SQL functions, and tables where adding a formal FK isn't possible all need a different approach. In this step, you'll learn how to manually configure relationships and discover the schema overview page for bulk tracking.
In this step, get ready to:
- Explore manual relationship configuration in the Hasura Console.
- Understand when and why you'd use manual relationships over FK-based ones.
- Discover the schema overview page for bulk relationship tracking.
Open the manual relationship form
When there's no foreign key constraint between two tables, Hasura can't automatically suggest a relationship. This happens frequently with database views and SQL functions, where FKs simply aren't possible.
- In the Hasura Console sidebar, click on the authors table.
- Navigate to the Relationships tab.
- Click the Add Relationship button.
You'll see the relationship configuration widget open. This is where you can choose between letting Hasura suggest a relationship from a FK or defining one manually.
When would I use manual relationships?
Manual relationships are needed when there's no foreign key constraint to guide Hasura. The most common scenarios are connecting a table to a database view, linking to a SQL function result, or relating tables across different databases.
You also use them when adding a FK isn't practical. For example, if two tables share a text column you want to join on, but enforcing referential integrity with a FK would be too restrictive for your data.
Configure a manual object relationship
To practice the manual configuration process, you'll create an object relationship on the authors table that maps to the profiles table. This duplicates the FK-based relationship you already have, but the goal here is to learn what each field in the form does.
- In the relationship widget, select Object Relationship as the type.
- Enter profile_manual as the relationship name.
- For the reference source, keep it set to your current database (default).
- Select profiles as the reference table.
- In the column mapping section, map the source column id to the reference column author_id.
- Click Create Relationship.
What do these fields mean?
The relationship type determines whether the result is a single object or an array. Object means one row comes back. Array means many rows come back.
The column mapping tells Hasura how to join the two tables. Source column is the column in the current table. Reference column is the column in the target table. Hasura matches rows where these values are equal.
You should see profile_manual appear in the Relationships tab alongside the existing profiles and posts relationships.
Don't see the new relationship?
Make sure you clicked Create Relationship at the bottom of the widget. If you see a validation error, double-check that the source column is id and the reference column is author_id.
help me set up a manual relationship in Hasura
Explore the schema overview for bulk tracking
So far you've been tracking relationships one table at a time. Hasura also has a schema-level overview page that shows all suggested relationships across your entire database at once.
- In the top navigation, click Data.
- In the left sidebar, click default (your database name) to expand it.
- Click public (your schema name).
This is the schema overview page. You'll see sections for Untracked Tables/Views and Untracked foreign-key relations.
- Look at the Untracked foreign-key relations section.
- Notice the Track All button at the top of this section.
What does Track All do?
Clicking Track All automatically creates relationships for every foreign key constraint in your database in one click. This is useful when you import an existing database with many tables and want Hasura to generate the full GraphQL API immediately.
You can also pick and choose individual relationships to track. Since you've already tracked all your relationships manually in previous steps, this section may be empty or show only the ones already configured.
The schema overview page is especially powerful when onboarding an existing database. Instead of clicking through each table individually, you can set up your entire relationship graph from one screen.
You now know how to connect tables with or without foreign keys. Next up, you'll learn about indexes and how they make your database queries faster.
Add Indexes to Optimize Queries
Your relationships are fully wired up, and nested GraphQL queries fetch data across multiple tables in a single request. But as your tables grow from dozens of rows to thousands or millions, those queries can slow down dramatically.
Indexes solve this problem. In this step, you'll learn what indexes are, create one on a frequently-queried column, and understand when to use different index types.
In this step, get ready to:
- Understand what indexes are and why they matter for performance.
- Create a btree index on the posts table's author_id column.
- Learn about different index types and their trade-offs.
Understand what an index is
Think of a book's index at the back. Instead of flipping through every page to find the word "PostgreSQL", you check the index and jump straight to page 42. A database index works the same way.
Without an index, the database must scan every single row to find what it needs. With an index on a column, it can jump directly to the matching rows.
Why does this matter for your blog?
Every time you run a query like "get all posts by author 1", the database searches the author_id column. With 10 rows, scanning everything is fine. With 10 million rows, it becomes painfully slow.
An index on author_id lets the database skip the full scan and jump to the right rows instantly.
Let's see where indexes live in the Hasura Console.
- In the Hasura Console, click the Data tab in the top navigation.
- In the left sidebar, click posts under the public schema.
- Click the Modify tab at the top of the table view.
- Scroll down until you see the Indexes section.
This section shows any existing indexes on the table. Right now you'll see an index for your primary key (id). PostgreSQL automatically creates an index on primary key columns.
Create an index on author_id
Since you'll frequently query posts by author (every time you fetch an author's posts through the relationship), the author_id column is a perfect candidate for an index.
- In the Indexes section of the posts table's Modify tab, click Add Index.
- In the index name field, type idx_posts_author_id.
- For the index type, select btree (this is the default and most common type).
- Select author_id as the column to index.
- Click Save.
You should see idx_posts_author_id now listed under the Indexes section with type btree and column author_id.
What did this actually do?
Behind the scenes, PostgreSQL built a sorted data structure (a B-tree) that maps author_id values to the physical location of matching rows. Now when you query posts by a specific author, the database uses this map instead of scanning the entire table.
Don't see the index appear?
Make sure you selected at least one column before clicking Save. The index cannot be created without a column.
If you see a naming conflict error, an index with that name may already exist. Try a slightly different name or check the existing indexes listed above the form.
Help me troubleshoot my index creation in Hasura.
Understand the different index types
You used btree for your index, but PostgreSQL offers several index types. Each is optimized for different kinds of queries.
- B-tree (default): Good for equality checks (WHERE author_id = 1) and range queries (WHERE created_at > '2024-01-01'). This covers most use cases.
- Hash: Good for equality checks only. Slightly faster than B-tree for exact matches, but cannot handle range queries or sorting.
- GIN: Designed for arrays and full-text search. Use this when a column contains multiple values (like a tags array) or when you're implementing search functionality.
- GiST: Designed for geometric and spatial data. Use this for location-based queries like "find all restaurants within 5km."
When in doubt, use B-tree
For the vast majority of columns you'll index in a typical web application, B-tree is the right choice. It handles both exact matches and range comparisons. You'll only need GIN or GiST for specialized data types.
What's the trade-off?
Indexes speed up reads (SELECT queries), but they slightly slow down writes (INSERT, UPDATE, DELETE). Every time you add or change a row, the database must also update the index.
For most applications, the read performance gain far outweighs the small write overhead. Only avoid indexes on columns that are written to constantly but rarely queried.
Your posts table now has a custom index that will keep queries fast as your data grows. Next up, you'll create computed fields that calculate values on-the-fly without storing them in the database.
Create Computed Fields
Your database now has indexes to speed up common queries. But what if you want to expose a calculated value in your GraphQL API without actually storing it in the database?
Computed fields solve this problem. They're virtual columns powered by SQL functions that calculate values on-the-fly every time you query them. In this step, you'll create a function that counts each author's posts, then expose it as a computed field in your GraphQL schema.
In this step, get ready to:
- Understand what computed fields are and when to use them.
- Create a SQL function that counts posts per author.
- Add the computed field to the authors table and test it via GraphQL.
Understand what computed fields are
Sometimes you need data that can be derived from existing columns but doesn't make sense to store permanently. For example, a "post count" for each author changes every time a post is added or deleted.
Storing it as a real column would mean updating it every time the posts table changes. Computed fields let you skip that entirely.
What is a computed field?
A computed field is a virtual column that doesn't exist in your database table. Instead, it's calculated at query time using a SQL function you define.
The function receives the current row as input, runs a calculation, and returns the result. In your GraphQL schema, the computed field appears alongside real columns as if it were stored data.
Create the SQL function
Before you can add a computed field, you need a SQL function for Hasura to call. This function must accept the table row as its first argument and return a value.
- In the Hasura Console, click the Data tab in the top navigation.
- Click SQL in the left sidebar to open the SQL editor.
- Paste the following function into the editor and click Run! to execute it:
CREATE FUNCTION author_post_count(author_row authors)
RETURNS integer AS $$
SELECT COUNT(*)::integer FROM posts WHERE posts.author_id = author_row.id
$$ LANGUAGE sql STABLE;
What does this function do?
- author_post_count(author_row authors) declares a function that takes a full authors row as input.
- SELECT COUNT(*)::integer FROM posts WHERE posts.author_id = author_row.id counts how many posts belong to that author.
- LANGUAGE sql STABLE tells PostgreSQL this is a plain SQL function that returns the same result for the same input within a single query. Hasura requires functions to be STABLE or IMMUTABLE to use them as computed fields.
- After clicking Run!, you should see a success message saying the command completed successfully.
Seeing an error about the function?
- Make sure the authors table exists and is tracked before running this function. The function references the authors type directly.
- Check that the posts table exists with an author_id column. The function queries this column to count posts.
- If you see "function already exists", the function was already created. You can proceed to the next substep.
Help me debug my computed field SQL function.
Add the computed field to the authors table
Now that the function exists in PostgreSQL, you need to tell Hasura to use it as a computed field on the authors table.
- In the left sidebar, click on the authors table under public.
- Click the Modify tab.
- Scroll down to the Computed fields section.
- Click Add.
- Enter post_count as the computed field name.
- Select author_post_count from the function dropdown.
- Click Add to save the computed field.
What just happened?
You linked the SQL function to the authors table as a virtual column called post_count. Hasura now includes post_count in the GraphQL schema for authors as if it were a real column.
Every time you query post_count, Hasura calls the author_post_count function with the current author row. The value is never stored in the database.
Now test the computed field in your GraphQL API.
- Click the API tab in the top navigation to open the GraphiQL explorer.
- Paste the following query into the editor and click the play button to run it:
query {
authors {
name
post_count
}
}
What does this query show?
Each author is returned with their name and a post_count value. The count is calculated live from the posts table at the moment you run the query.
If you add or delete posts for an author and re-run this query, the count updates automatically. No manual updates needed.
- Verify that each author in the results shows an integer post_count value matching the number of posts you inserted for them earlier.
Not seeing post_count in the results?
- Make sure you saved the computed field in the Modify tab. Go back to authors → Modify and confirm post_count appears under Computed fields.
- Check that the function name matches exactly. The dropdown should show author_post_count.
- If the field shows null for all authors, verify you have posts in the posts table with valid author_id values.
Help me troubleshoot my post_count computed field.
Your authors table now has a live post count that stays accurate without any manual maintenance. Next up, you'll customize the names of your auto-generated GraphQL fields and explore Apollo Federation.
Configure Custom GraphQL Root Fields and Apollo Federation
Your Hasura GraphQL API currently uses auto-generated names like authors, authors_by_pk, and insert_authors. These names come directly from your table name, which works fine for personal projects.
In team environments, you might want names that match your frontend conventions or feel more intuitive for other developers. In this step, you'll customize those root field names, rename a column in the GraphQL schema, and explore Apollo Federation for multi-service architectures.
In this step, get ready to:
- Rename the default GraphQL root fields on the authors table.
- Customize a column name in the GraphQL schema without changing the database.
- Enable Apollo Federation on the authors table.
Customize root field names on the authors table
By default, Hasura generates GraphQL root field names based on your table name. For the authors table, you get fields like authors (select all), authors_by_pk (select one), and insert_authors (insert).
You can rename all of these to whatever naming convention your team prefers.
- In the Hasura Console, click the Data tab in the top navigation bar.
- Click on the authors table in the left sidebar.
- Click the Modify tab.
- Scroll down to the Custom GraphQL Root Fields section.
- Click the Edit button in that section.
- Enter getAuthors in the Select field.
- Enter getAuthor in the Select By pk field.
- Enter addAuthors in the Insert field.
- Click Save.
Why customize root field names?
Teams often have naming conventions for their APIs. A frontend team using JavaScript might prefer getAuthors over authors because it reads more like a function call.
Custom root fields also help avoid naming conflicts when combining multiple GraphQL sources. Once you set a custom name, the old auto-generated name stops working and queries must use the new name.
Now test your renamed fields in the API Explorer.
- Click the API tab in the top navigation bar.
- Clear any existing query in the GraphiQL editor.
- Enter the following query and click the play button:
query {
getAuthors {
name
email
}
}
What does this query do?
Instead of using the default authors root field, you're now using your custom getAuthors name. Hasura recognizes the custom name and routes it to the same underlying table query.
You should see the same list of authors you inserted earlier, returned under the getAuthors field name.
Query returning an error?
Make sure you typed getAuthors exactly as you entered it in the Custom GraphQL Root Fields section. The name is case-sensitive.
If you still see the old authors field in the Explorer sidebar, refresh the page to reload the schema.
Help me debug my custom root field query.
Customize column names in the GraphQL schema
You can also rename individual columns in the GraphQL schema without changing the actual database column name. This is useful when your database uses snake_case (like created_at) but your frontend team prefers camelCase (like createdAt).
- Switch back to the Data tab.
- Click on the authors table in the left sidebar.
- Click the Modify tab.
- Find the email column in the list and click Edit next to it.
- Look for the GraphQL Field Name input and enter emailAddress.
- Click Save.
What does this change?
The column is still called email in PostgreSQL, but the GraphQL schema now shows emailAddress. This lets you present a clean, consistent API to frontend developers without restructuring your database.
Common use cases include converting snake_case database naming to camelCase for JavaScript or TypeScript frontends.
- Return to the API tab and run this query to confirm the column rename works:
query {
getAuthors {
name
emailAddress
}
}
You should see author data returned with the emailAddress field containing the same values that were previously under email.
Revert after exploring
Since the rest of this project uses the default column name email, go back and undo this change. Navigate to the Modify tab, edit the email column, clear the GraphQL Field Name input, and save.
Enable Apollo Federation on the authors table
Apollo Federation is a way to combine multiple GraphQL APIs into one unified supergraph. When you enable Federation on a Hasura table, other GraphQL services can reference and extend that table's type.
What is Apollo Federation?
Imagine you have multiple teams, each running their own GraphQL service. One team manages users, another manages orders, another manages inventory. Apollo Federation lets you stitch all these services into a single API that clients query as if it were one service.
Enabling Federation on a Hasura table exposes it as a Federation entity with a @key directive based on the primary key. Other subgraphs in your supergraph can then reference and extend the authors type without duplicating the data.
To enable Apollo Federation, you first need to add a server-level environment variable to your Docker setup. The Console toggle only appears after this flag is set.
- Open your docker-compose.yml file in a text editor.
- Find the environment section under the graphql-engine service.
- Add the following line alongside the other environment variables:
HASURA_GRAPHQL_ENABLE_APOLLO_FEDERATION: "true"
What does this environment variable do?
This flag tells Hasura to enable Apollo Federation v1 support at the server level. Without it, the Federation toggle won't appear in the Console's Modify tab.
Once set, each table gets its own toggle to opt in or out of Federation individually.
- Save the file.
- Restart your containers by running the following command in your terminal:
docker compose up -d
Docker recreates the graphql-engine container with the new environment variable. Your data and metadata are preserved because they live in Docker volumes.
- Return to the Hasura Console at http://localhost:8080/console.
- Click the Data tab in the top navigation bar.
- Click the authors table in the left sidebar.
- Click the Modify tab.
- Scroll down and find the Enable Apollo Federation section.
- Toggle the switch to enable it.
You should see the toggle change to an enabled state. The authors table type is now exposed as a Federation entity that other subgraphs can reference.
When would I use this?
You would use Apollo Federation when your organization has multiple GraphQL services that need to share types. For example, a payments service could extend the authors type to add billing information without modifying Hasura directly.
For this project, just know the feature exists. You don't need an Apollo gateway running to complete the rest of the steps.
Don't see the Apollo Federation section?
Make sure you added HASURA_GRAPHQL_ENABLE_APOLLO_FEDERATION: "true" to the correct service (graphql-engine), not to the postgres service.
Confirm you ran docker compose up -d after saving the file. The container needs to restart with the new variable.
Help me enable Apollo Federation in my Hasura Console.
Your API now has custom names that feel intentional and well-designed. Next up, you'll add human-readable comments to your tables and columns so your API becomes fully self-documenting.
Add Table and Column Comments
You have custom root fields, computed fields, and Apollo Federation configured on your tables. But a developer discovering your API for the first time still has to guess what each table and column does.
In this step, you'll add descriptions to your authors table and its columns. Hasura automatically turns these comments into GraphQL schema documentation, making your API self-describing.
In this step, get ready to:
- Add a table-level comment describing the purpose of the authors table.
- Add column-level comments to individual fields.
- Verify your comments appear in the GraphiQL documentation explorer.
Add a table comment to the authors table
Table comments describe the overall purpose of a table. Hasura picks up these comments and surfaces them as type descriptions in your GraphQL schema.
- In the Hasura Console sidebar, click the authors table under public.
- Click the Modify tab.
- At the top of the Modify page, find the Comment field for the table.
- Enter Stores blog author profiles and contact information in the Comment field.
- Click the Save button (or the save icon next to the comment field) to apply the comment.
What does a table comment do?
When you add a comment to a table, Hasura stores it as a PostgreSQL comment on that table object. It then exposes that comment as the description of the corresponding GraphQL type.
Anyone using your API can read this description without leaving their development tools. The documentation lives alongside the API itself.
Add comments to individual columns
Column comments work the same way as table comments, but they describe individual fields. Each column in the Modify tab has its own Comment option.
- On the same Modify tab for authors, find the name column in the columns list.
- Click Edit on the name column to expand its options.
- In the Comment field, enter The author's display name.
- Click Save for the name column.
- Find the email column and click Edit.
- In the Comment field, enter Must be unique across all authors.
- Click Save for the email column.
Why add column comments?
Column comments become field-level descriptions in your GraphQL schema. Frontend developers querying your API see these descriptions inside their IDE autocomplete and documentation tools.
Good comments eliminate guesswork about what a field contains or what constraints it has.
Don't see a Comment field on a column?
Make sure you clicked Edit on the specific column row to expand its options. The Comment field only appears after expanding the column's edit view.
help me find the Comment option for a column in the Hasura Modify tab
Verify your comments in the GraphiQL documentation explorer
The real payoff of adding comments is seeing them in your API documentation. The GraphiQL explorer built into Hasura makes these comments immediately visible.
- Click the API tab in the top navigation bar of the Hasura Console.
- In the GraphiQL explorer, click the Docs button on the right side to open the documentation explorer.
- Navigate to the authors type. You may need to click through query_root first, then find the authors query.
- Look at the type description. You should see your table comment: Stores blog author profiles and contact information.
- Click into the authors type fields. You should see your column comments next to name and email.
What makes this powerful?
Your API is now self-documenting. Any developer who connects to your GraphQL endpoint can explore the schema and understand each table and field without asking you or reading a separate wiki.
Comments not showing in Docs?
Try closing and reopening the Docs panel by clicking the Docs button twice. If comments still don't appear, go back to the Data tab, select authors, click Modify, and confirm your comments were saved.
help me see my table and column comments in the GraphiQL Docs explorer
Your API now describes itself. Next up, you'll create SQL functions and expose them as custom GraphQL queries for more powerful data retrieval.
Create and Track SQL Functions
You already used a SQL function in the previous step to power the post_count computed field. That function takes a single row as input and returns a scalar value. But PostgreSQL functions can do much more.
When a function returns a set of table rows, Hasura can track it and expose it directly as a GraphQL query. In this step, you'll create a search function that finds posts by keyword, then expose it in your API.
In this step, get ready to:
- Create a SQL function that searches posts by title and content.
- Track the function to expose it as a GraphQL query.
- Test the search function from the API tab.
Create the search_posts function
A tracked SQL function works differently from a computed field. Computed fields attach to a single row (like calculating one author's post count). A tracked function stands on its own as a top-level GraphQL query that can return multiple rows from any table.
What makes a function trackable?
For Hasura to track a function as a query, the function must return SETOF <table_name>. This tells Hasura that the function returns rows matching an existing table's structure. That way, Hasura can reuse the table's type, relationships, and permissions for the function's results.
- Navigate to Data → SQL in the top navigation bar.
- Clear any existing text in the SQL editor.
- Create the search function by pasting this SQL:
CREATE FUNCTION search_posts(search text) RETURNS SETOF posts AS $$
SELECT * FROM posts
WHERE title ILIKE ('%' || search || '%')
OR content ILIKE ('%' || search || '%')
$$ LANGUAGE sql STABLE;
What does this function do?
- search_posts(search text) defines a function that accepts one text argument called search.
- RETURNS SETOF posts means it returns zero or more rows that match the posts table structure.
- ILIKE is a case-insensitive pattern match. The % symbols are wildcards that match any text before or after the search term.
- STABLE tells PostgreSQL (and Hasura) that this function returns the same results for the same arguments within a single query. Hasura requires functions to be STABLE or IMMUTABLE to track them.
- Click Run! to execute the SQL.
- You should see a success message confirming the function was created.
Seeing an error about the function already existing?
If you see an error like "function search_posts already exists", it means you may have run this SQL before. Replace CREATE FUNCTION with CREATE OR REPLACE FUNCTION at the start and run it again.
help me fix the error I'm getting when creating my search_posts function
Track the function as a GraphQL query
Creating the function in the database is only half the work. Just like tables, functions start out "untracked". You need to tell Hasura to expose it in your GraphQL schema.
- Navigate to Data → default → public in the left sidebar.
- Scroll down past your tracked tables. You should see an Untracked Tables or Views section, followed by an Untracked Custom Functions section.
- Find search_posts in the untracked functions list.
- Click Track next to it.
- When prompted, choose to expose it as a Query. This means the function will appear as a query field (not a mutation) in your GraphQL schema.
You should now see search_posts listed in the sidebar under your tracked tables.
Why track as a Query instead of a Mutation?
Queries are for reading data. Mutations are for writing data. Since search_posts only reads from the database (it uses SELECT), it makes sense as a Query. If you had a function that inserted or updated data, you'd expose it as a Mutation instead.
Don't see search_posts in the untracked functions list?
Make sure the SQL ran successfully in the previous substep. Go back to Data → SQL and run the function creation SQL again. Also check that you're looking under the public schema, not the blog schema.
help me find and track my search_posts function in Hasura
Test the search function in the API tab
Now that your function is tracked, Hasura has added it to your GraphQL schema as a queryable field. You can call it just like any other query, passing arguments through the args parameter.
- Switch to the API tab in the top navigation.
- Clear the GraphiQL editor and run this query to search your posts:
query {
search_posts(args: { search: "hello" }) {
id
title
content
}
}
What does this query do?
- search_posts calls your tracked SQL function as a GraphQL query field.
- args: { search: "hello" } passes the search term to the function's search parameter.
- The function returns any posts where the title or content contains "hello" (case-insensitive).
- Click the play button to run the query.
You should see matching posts in the response panel. If none of your existing posts contain "hello", you'll get an empty array. That's correct. The function works but found no matches.
- Try replacing "hello" with a word you know exists in one of your posts (like part of a title you created earlier).
- Run the query again to confirm matching posts are returned.
Why tracked functions matter
Tracked functions let you add custom query logic that goes beyond basic CRUD. Search, complex filtering, calculations across multiple tables, reporting queries. Anything you can write in SQL, you can expose as a clean GraphQL query field.
Because the function returns SETOF posts, the results inherit all of the posts table's relationships. You could nest author or comments inside your search results just like a regular posts query.
Getting a "field not found" error?
If GraphiQL doesn't recognize search_posts, the function may not be tracked yet. Go back to Data → default → public and check the Untracked Custom Functions section. Make sure you tracked it as a Query.
help me debug why search_posts isn't showing up in my GraphQL schema
You've now added custom search logic to your GraphQL API without writing a single line of application code. Next up, you'll lock down who can see and do what by configuring permissions and roles.
Configure Permissions - Roles and Row-Level Security
Right now, anyone with the admin secret can see and do everything in your database.
In a real application, you'd want different users to see different data. For example, regular users should only see published blog posts, not unpublished drafts. In this step, you'll configure Hasura's permission system to control exactly who can see what.
In this step, get ready to:
- Understand the Hasura permission model (roles, operations, and the permission grid).
- Create a custom role and configure row-level SELECT permissions.
- Test your permissions in the API tab to confirm row-level security works.
Understand the Hasura permission model
Hasura's permission system works at the intersection of three things: a table, a role, and an operation (insert, select, update, or delete).
By default, only the admin role exists, which has full unrestricted access to everything. Every other role you create starts with zero permissions. You grant access one table and operation at a time.
- In the Hasura Console sidebar, click on the posts table.
- Click the Permissions tab at the top of the table view.
You'll see a permission grid. The rows represent roles and the columns represent operations (insert, select, update, delete). Right now, only the admin role is listed with full access to everything.
What is this grid?
Think of the permission grid like a spreadsheet. Each cell is an answer to: "Can [this role] perform [this operation] on [this table]?" You configure the answer for each cell individually.
Hasura uses a flat, non-hierarchical role model. Roles don't inherit from each other. Each role's permissions are completely independent.
Create a role and configure SELECT permissions
You'll create a user role that can only see published posts. Unpublished drafts will be invisible to this role.
- In the Permissions tab of the posts table, find the text field below the existing admin role row labeled Enter new role.
- Type user into that field.
- Click the select cell in the new user row to open the SELECT permission configuration.
A permission configuration panel opens. This is where you define row-level and column-level access.
- Under Row select permissions, select With custom check.
- Enter the following JSON filter:
{"is_published": {"_eq": true}}
What does this filter do?
This is a row-level permission filter. It means: "Only return rows where the is_published column equals true." Any post that hasn't been published yet will be completely invisible to the user role.
- Under Column select permissions, click Toggle All to allow the user role to see all columns.
- Click Save Permissions.
You should see the user role's select cell now shows a green checkmark or a custom-access indicator instead of being empty.
Don't see the custom check option?
Make sure you clicked the select cell (not insert, update, or delete) in the user row. The permission panel opens only after clicking a specific operation cell.
If you see a text editor for the JSON filter, make sure you enter valid JSON with double quotes around all keys and string values.
Help me configure SELECT permissions for the user role on the posts table.
Test the permission in the API tab
The permission is configured, but you need to see it in action. You'll run the same query as two different roles to see the difference.
- Click the API tab in the top navigation to open the GraphiQL explorer.
- Make sure your sample data includes at least one post with is_published set to true and at least one post with is_published set to false. If you don't have both, insert or update rows in the Browse Rows tab first.
- In the GraphiQL request headers section (the panel at the bottom of the query editor), add a new header with key x-hasura-role and value user.
- Remove or clear the x-hasura-admin-secret header if one is present.
- Enter the following query and click the play button:
query {
posts {
id
title
is_published
}
}
You should only see posts where is_published is true. Your unpublished drafts are completely hidden.
- Now switch back to admin: remove the x-hasura-role header and restore the x-hasura-admin-secret header.
- Run the same query again.
This time you should see all posts, both published and unpublished. The admin role bypasses all permission filters.
What is row-level security?
What you just built is row-level security. Different roles see different rows from the same table, based on conditions you define.
The user role's filter runs automatically on every query. The user never even knows unpublished posts exist. In a production app, you'd use session variables like X-Hasura-User-Id to filter data per individual user.
Seeing all posts even with the user role?
Double-check that the x-hasura-admin-secret header is removed. If both headers are present, the admin secret takes priority and bypasses all permissions.
Also confirm that the x-hasura-role header value is exactly user (lowercase, no spaces). Role names are case-sensitive.
Help me troubleshoot why my Hasura role permissions aren't filtering posts correctly.
You've just locked down your first table with role-based access control. Next up, you'll expand the user role's permissions to cover insert, update, and delete operations with column-level control.
Configure Permissions - Insert, Update, Delete and Column-Level Control
In the previous step, you configured SELECT permissions so the user role can only read published posts. Reading data is only half the story.
Now you need to control what users can create, change, and remove. This is where INSERT, UPDATE, and DELETE permissions come in. You will also use column presets to automatically fill in values behind the scenes.
In this step, get ready to:
- Configure INSERT permission with a column preset that auto-fills the author.
- Configure UPDATE and DELETE permissions with row-level ownership checks.
- Test all three operations in the API tab using session variable headers.
Configure INSERT permission for the user role
When a user creates a new post, you want to control which fields they can provide. You also want to prevent them from impersonating another author by manually setting author_id. Column presets solve this by automatically filling a column from the request's session variables.
- Navigate to Data in the top menu bar.
- Click the posts table in the left sidebar.
- Click the Permissions tab.
- Find the user role row (it already exists from your SELECT configuration).
- Click the insert cell for the user role.
The insert permission configuration panel opens. You will configure two things: which columns the user can provide values for, and which columns are filled in automatically.
- Under Column insert permissions, check only these three columns: title, content, and is_published.
- Scroll down to the Column presets section.
- For the author_id column, select from session variable.
- Enter X-Hasura-User-Id as the session variable name.
- Click Save Permissions.
What are column presets?
A column preset automatically fills a column's value from either a session variable or a static value. Once a preset is defined for a role, that column disappears from the role's GraphQL insert input. The user cannot override it.
In this case, author_id is always set to the value of X-Hasura-User-Id from the request headers. Users can only create posts as themselves.
This is different from column permissions, which control visibility. Column presets auto-fill values.
Why no row-level check on INSERT?
INSERT permissions do not need a row filter because the row does not exist yet. The column preset ensures author_id always matches the requesting user, so there is no way to insert a post belonging to someone else.
Configure UPDATE permission with an ownership check
Users should only be able to edit their own posts. You enforce this with a row-level permission check that compares the post's author_id to the requesting user's ID.
- On the same Permissions tab for the posts table, click the update cell for the user role.
- Under Row update permissions, select Custom check.
- Enter the following JSON rule:
{"author_id": {"_eq": "X-Hasura-User-Id"}}
- Under Column update permissions, check only these columns: title, content, and is_published.
- Leave author_id and created_at unchecked. Users cannot change who wrote the post or when it was created.
- Click Save Permissions.
What does this row check do?
The rule {"author_id": {"_eq": "X-Hasura-User-Id"}} means: only allow this operation on rows where author_id equals the value of the X-Hasura-User-Id session variable in the request.
If user 1 tries to update a post written by user 2, Hasura blocks the request. The user only sees and modifies their own content.
Configure DELETE permission and test all three operations
DELETE permission uses the same ownership check. After setting it up, you will test all three write operations in the API tab with custom headers.
- Click the delete cell for the user role.
- Under Row delete permissions, select Custom check.
- Enter the same JSON rule:
{"author_id": {"_eq": "X-Hasura-User-Id"}}
- Click Save Permissions.
Now test all three operations. Switch to the API tab and configure your request headers to simulate a regular user.
- Click the API tab in the top menu bar.
- In the headers section at the bottom of the GraphiQL editor, remove any existing x-hasura-admin-secret header.
- Add a header with key x-hasura-role and value user.
- Add a header with key x-hasura-user-id and value 1.
Test INSERT first by running this mutation:
mutation {
insert_posts_one(object: {
title: "My Permission Test Post",
content: "Testing insert as user 1",
is_published: true
}) {
id
title
author_id
}
}
What should I see?
The response returns the new post with author_id set to 1. You never passed author_id in the mutation input. The column preset filled it in automatically from your x-hasura-user-id header.
Now test UPDATE by running this mutation (replace the id with the ID returned from your insert):
mutation {
update_posts_by_pk(
pk_columns: { id: 1 },
_set: { title: "Updated Title" }
) {
id
title
}
}
Finally, test DELETE:
mutation {
delete_posts_by_pk(id: 1) {
id
title
}
}
Column presets vs column permissions
These two concepts sound similar but do different things. Column presets automatically fill a value from a session variable or static value. The column disappears from the GraphQL input entirely.
Column permissions control which columns a role can see or write to. The column still appears in the schema, but only the allowed ones are accessible. You used both in this step: a preset on author_id (auto-fill) and column permissions on UPDATE (restricting which fields can be edited).
Getting a permission denied error?
Make sure you removed the x-hasura-admin-secret header before adding the role headers. If both are present, the admin secret takes priority and your role-based permissions are bypassed.
If the UPDATE or DELETE returns null, the row check failed. Confirm that the post's author_id matches the value in your x-hasura-user-id header.
Help me debug my Hasura permission configuration for the user role.
- After testing, add the x-hasura-admin-secret header back for admin access in subsequent steps.
Your write permissions are locked down. Users can only create posts as themselves, edit their own posts, and delete their own posts. Next up, you will explore the full auto-generated GraphQL API to see every query, mutation, and subscription Hasura builds for you.
Explore the Auto-Generated GraphQL API
You've been building tables, setting up relationships, and configuring permissions for several steps now. Every time you made a change, Hasura was quietly generating a complete GraphQL API behind the scenes.
In this step, you'll finally explore that auto-generated API in full. You'll learn what operations Hasura created for you, then practice using queries, mutations, and subscriptions to interact with your data in real time.
In this step, get ready to:
- Explore the auto-generated queries, mutations, and subscriptions in the documentation explorer.
- Practice queries with filtering, sorting, pagination, and aggregation.
- Practice mutations and test real-time subscriptions.
Understand what Hasura generates automatically
For every tracked table in your database, Hasura auto-generates a full set of GraphQL operations. You never wrote a single resolver or endpoint. Let's see what's available.
- Switch to the API tab in the Hasura Console.
- On the right side of the GraphiQL editor, click the Docs button to open the documentation explorer.
- Click query_root to see all available queries.
You should see query fields for every tracked table. For the posts table alone, you'll find:
- posts (select all rows with filtering, sorting, pagination).
- posts_by_pk (select a single row by primary key).
- posts_aggregate (run aggregate functions like count, sum, avg, max).
- Now click back and explore mutation_root. You'll find:
- insert_posts_one and insert_posts (insert one or many rows).
- update_posts_by_pk and update_posts (update one or many rows).
- delete_posts_by_pk and delete_posts (delete one or many rows).
- Finally, click subscription_root. You'll see the same queries as under query_root, but they deliver real-time updates via WebSocket instead of a one-time response.
What did Hasura generate for me?
For every tracked table, Hasura creates three categories of operations automatically:
- Queries let you read data (select all, select by PK, aggregate).
- Mutations let you write data (insert one, insert many, update by PK, update many, delete by PK, delete many).
- Subscriptions are real-time versions of queries that push updates to your client whenever data changes.
Practice queries with filtering, sorting, pagination, and aggregation
The real power of Hasura's auto-generated queries is that they come with built-in arguments for filtering, sorting, and paginating results. Let's try them all in a single query.
- In the API tab, clear the GraphiQL editor and paste the following query:
query {
# Filter: only published posts
# Sort: newest first
# Paginate: first 5 results
posts(
where: {is_published: {_eq: true}}
order_by: {created_at: desc}
limit: 5
offset: 0
) {
id
title
is_published
created_at
author {
name
}
}
}
- Click the Play button (the triangular icon) to execute the query.
What does this query do?
- where: {is_published: {_eq: true}} filters results to only published posts.
- order_by: {created_at: desc} sorts results with the newest posts first.
- limit: 5, offset: 0 paginates the results, returning only the first 5 rows.
- The nested author { name } uses the object relationship to fetch each post's author in the same request.
You should see a JSON response on the right containing only published posts, sorted by date, with author names nested inside. If you have no published posts, you'll see an empty array.
Now let's try an aggregation query to count your posts.
- Clear the editor and paste this aggregation query:
query {
# Count all posts in the database
posts_aggregate {
aggregate {
count
}
}
}
- Click Play to execute.
You should see a response with a count value matching the total number of rows in your posts table.
What is an aggregate query?
Aggregation runs calculations across all matching rows instead of returning individual rows. Hasura supports count, sum, avg, and max out of the box.
Seeing an empty result?
If your filter query returns an empty array, make sure at least one post has is_published set to true. You can update a post via the Browse Rows tab in the Data section.
Help me debug my query
Practice mutations and test a real-time subscription
Queries let you read. Mutations let you write. Let's insert a new post, update it, and delete it, all through GraphQL.
- Insert a new post by pasting and running this mutation:
mutation {
# Insert a single post and return its generated id
insert_posts_one(object: {
title: "My GraphQL Post"
content: "Testing mutations in Hasura"
author_id: 1
is_published: true
}) {
id
title
created_at
}
}
What does this mutation do?
- insert_posts_one inserts a single row into the posts table.
- The object argument contains the column values for the new row.
- The return fields (id, title, created_at) let you confirm what was created.
You should see a response with the new post's id, title, and created_at timestamp. Note the id value for the next step.
- Update the post you just created by pasting and running this mutation (replace 1 with the actual id from the response above if different):
mutation {
# Update a post's title by its primary key
update_posts_by_pk(
pk_columns: {id: 1}
_set: {title: "New Title"}
) {
id
title
}
}
You should see the post returned with its updated title.
- Delete the post by pasting and running this mutation:
mutation {
# Delete a post by its primary key
delete_posts_by_pk(id: 1) {
id
title
}
}
The response confirms which post was deleted by returning its id and title one last time.
Now let's test the most exciting part: real-time subscriptions.
- Clear the editor and paste this subscription:
subscription {
# Watch for real-time changes to posts
posts {
id
title
}
}
- Click Play to start the subscription. The response panel now shows a live connection that updates whenever data changes.
- Open a new browser tab and navigate to http://localhost:8080/console.
- In the new tab, go to Data → posts → Insert Row and add a new post.
- Switch back to your original tab with the subscription running.
The subscription result updates automatically to include the new post. No page refresh needed. This is Hasura delivering real-time data over a WebSocket connection.
When would I use subscriptions?
Subscriptions are perfect for live dashboards, chat applications, notification feeds, or any UI that should reflect database changes instantly without polling.
Subscription not updating?
Make sure the subscription is still running (the Play button should be active). If you stopped it, click Play again to reconnect. Also confirm you inserted the row in the same posts table in the public schema.
Help me troubleshoot my subscription
You've now explored the full auto-generated GraphQL API. Every query, mutation, and subscription was created automatically from your table definitions and relationships. Next up, you'll learn how to extend this API beyond basic CRUD with custom Actions.
Create an Action to Extend Your GraphQL API
So far, everything you've built in Hasura has been powered by your database tables. Queries, mutations, and subscriptions all map directly to the data you've stored.
But what about operations that don't live in your database? Maybe you need to call an external API, run custom business logic, or combine data from multiple sources. That's where Actions come in.
In this step, get ready to:
- Understand what Actions are and when to use them.
- Create a Query Action with input and output types.
- Understand the Action execution flow.
Understand what Actions are and when to use them
When you need GraphQL operations that don't map to database tables, you create an Action. Actions let you connect REST endpoints, serverless functions, or custom servers to your GraphQL API.
What is an Action?
An Action is a custom Query or Mutation you define in Hasura that calls an external HTTP webhook (your handler) instead of your database. You define the input arguments and output type using GraphQL SDL, point Hasura at a URL, and it handles the rest.
Use cases include calling external APIs, running validation logic, combining data from multiple sources, or implementing authentication flows.
- In the Hasura Console, click the Actions tab in the top navigation bar.
- Click Create to start defining a new Action.
You should see the Action creation form with fields for the Action Definition, Type Configuration, and Webhook Handler.
Define the Action Definition and types
You'll now create a simple Query Action called hello that takes a name as input and returns a greeting message. This demonstrates the core pattern without needing a real backend.
- Clear any placeholder text in the Action Definition field.
- Paste the following into the Action Definition field:
type Query {
hello(name: String!): HelloResponse
}
What does this definition do?
This tells Hasura you're creating a new Query (not a Mutation) called hello. It accepts one required argument name of type String! and returns a HelloResponse object.
The ! means the argument is required. Anyone calling this query must provide a name.
- Clear any placeholder text in the Type Configuration field.
- Paste the following into the Type Configuration field:
type HelloResponse {
message: String!
}
What does this type do?
This defines the shape of the response. When someone calls the hello query, the handler must return a JSON object with a message field containing a string.
- In the Webhook Handler field, paste the following URL:
https://httpbin.org/post
Why httpbin.org?
This is a public demo endpoint that accepts any POST request and returns a 200 response. It won't return data matching your HelloResponse type, but it lets you create and test the Action flow without building a real handler.
In production, you'd replace this with your own server (Node.js, Python, etc.) that processes the input and returns a proper { "message": "Hello, <name>!" } response.
- Scroll to the bottom of the page and click Create Action.
You should see a success message and the Action now appears in the Actions sidebar.
Action not appearing?
Make sure you didn't leave the default placeholder text in the Action Definition or Type Configuration fields. The fields must contain only your pasted definitions.
Check that your Action Definition uses type Query (not type Mutation) since this is a Query Action.
help me create my Hasura Action
Understand the Action execution flow
Now that you've created the Action, let's understand what happens when someone calls it.
- Navigate to the API tab to open GraphiQL.
- Run the following query to test your Action:
query {
hello(name: "Alice") {
message
}
}
You'll see an error response because httpbin.org doesn't return data in the shape Hasura expects. This is expected and proves the Action is wired up correctly.
How does the Action flow work?
- A client sends the hello query to your Hasura GraphQL endpoint.
- Hasura sees it's an Action and sends a POST request to your handler URL (https://httpbin.org/post) with the input arguments and session variables.
- Your handler processes the request and returns a JSON response matching the output type.
- Hasura forwards that response back to the client as the GraphQL result.
What does Hasura send to the handler?
Hasura POSTs a JSON body containing the action name, input arguments, and session variables. The payload looks like this:
{
"action": {
"name": "hello"
},
"input": {
"name": "Alice"
},
"session_variables": {
"x-hasura-role": "admin"
}
}
In production, your handler reads the input, runs your custom logic, and returns a response matching the output type (e.g., { "message": "Hello, Alice!" }).
Actions make any REST API queryable via GraphQL
The handler can be anything that accepts HTTP POST requests. A Node.js Express server, a Python Flask app, an AWS Lambda function, or even a public API with request transforms. You just point Hasura at the URL.
Your first Action is set up. Next, you'll create an Event Trigger that fires automatically when data changes in your database.
Set Up Event Triggers to React to Database Changes
Your Action from the previous step lets you extend your GraphQL API with custom logic that runs when someone explicitly calls it.
But what about things that should happen automatically when data changes? Sending a welcome email when a new author signs up, or notifying an admin when a post is published. These reactions shouldn't require a manual API call. They should fire on their own the moment something happens in the database.
In this step, get ready to:
- Understand how Event Triggers automate reactions to database changes.
- Create an Event Trigger that fires when a new post is inserted.
- Test the trigger and inspect the payload it sends to a webhook.
Understand what Event Triggers do
An Event Trigger fires a webhook whenever an INSERT, UPDATE, or DELETE happens on a table.
The webhook is just an HTTP endpoint that receives the event data. In production, this endpoint would be your own server that sends emails, pushes notifications, or updates other systems.
What is an Event Trigger?
Think of an Event Trigger as a watcher sitting on your table. Every time a row is inserted, updated, or deleted, the watcher sends a POST request to a URL you specify.
Real-world examples: "Send a welcome email when a new author signs up." "Notify admin when a post is published." "Update a search index when content changes."
- In the Hasura Console, click the Events tab in the top navigation bar.
- Click Event Triggers in the left sidebar (it should already be selected).
- Click Create.
You should see the Event Trigger creation form with fields for the trigger name, database, schema, table, and trigger operations.
Create an Event Trigger on the posts table
You will create a trigger that fires every time a new post is inserted. For the webhook, you will use a public demo endpoint that accepts any request and returns a 200 response.
- In the Trigger Name field, enter on_new_post.
- For Database, select default.
- For Schema/Table, select the posts table from the public schema.
- Under Trigger Operations, check only Insert.
- In the Webhook URL field, enter https://httpbin.org/post.
- Click Create Event Trigger.
Why httpbin.org/post?
This is a public demo endpoint that accepts any POST request and returns a 200 response. It is the same endpoint used in Hasura's official Event Trigger quickstart.
In a real project, you would replace this with your own server URL. That server would handle the event by sending emails, updating a search index, or triggering notifications.
You should see the on_new_post trigger listed under Event Triggers in the sidebar.
Don't see your trigger?
Make sure you selected the posts table from the public schema, not the blog schema. The webhook URL must start with https://.
Help me troubleshoot my Event Trigger creation in Hasura.
Test the Event Trigger by inserting a new post
The trigger is now watching the posts table. Every time a new row is inserted, Hasura will automatically send a POST request to your webhook URL with the new row's data.
- Navigate to Data → posts → Insert Row.
- Enter a title like Event Trigger Test and some content.
- Set author_id to the ID of an existing author (e.g. 1).
- Click Save.
This insert should have fired your Event Trigger in the background. Now verify that it worked.
- Navigate to Events → on_new_post.
- Click the Processed Events tab.
You should see at least one event listed with a 200 status, confirming it was delivered successfully to the webhook.
- Click on the event entry to expand its details.
You should see the full payload that Hasura sent to the webhook. This includes the new row's data (the post you just inserted), the table name, and the operation type.
What is in the event payload?
The payload includes the event data (the new row with all its column values), the table schema and name, and session variables. Your webhook handler uses this data to decide what action to take.
In production, this payload is what your email service would read to know which author to welcome, or what your notification system would use to alert an admin about new content.
Seeing 0 processed events?
Wait a few seconds and refresh the page. Event delivery is asynchronous, so there may be a brief delay. If the event still does not appear, check that your Docker container has internet access (the webhook URL is external).
Help me troubleshoot why my Event Trigger is not delivering events.
Your Event Trigger is now reacting to database changes in real time. Next up, you will create a Cron Trigger that runs on a schedule instead of waiting for data changes.
Create Cron Triggers for Scheduled Tasks
In the previous step, you created an Event Trigger that fires whenever data changes in your database. That covers reactive automation. But what about tasks that need to happen on a recurring schedule, regardless of whether data changed?
Think of tasks like generating a daily report, cleaning up old drafts every night, or sending a weekly digest email. These need a time-based trigger, not a data-based one. That is exactly what Cron Triggers do in Hasura.
In this step, get ready to:
- Understand what Cron Triggers are and when to use them.
- Create a Cron Trigger that runs daily at midnight UTC.
- Explore the Cron Trigger dashboard and retry configuration.
Understand what Cron Triggers do
A Cron Trigger calls a webhook on a recurring schedule, like a cron job. Unlike Event Triggers that react to database changes, Cron Triggers fire purely based on time.
Common use cases include generating a daily report, cleaning up old data every week, or sending digest emails every Monday.
- In the Hasura Console, click the Events tab in the top navigation bar.
- Click Cron Triggers in the left sidebar.
You should see an empty Cron Triggers page with a button to create a new trigger. This is where you define triggers that run on a schedule.
How is this different from Event Triggers?
Event Triggers react to database events (INSERT, UPDATE, DELETE). Cron Triggers fire on a time schedule regardless of what happens in your data.
Both call HTTP webhooks, and both live under the Events tab in the Console.
Create the daily_cleanup Cron Trigger
You will now create a Cron Trigger that simulates a nightly cleanup job. It will call a demo webhook every day at midnight UTC.
- Click Create on the Cron Triggers page.
- In the Name field, enter daily_cleanup.
- In the Webhook field, enter https://httpbin.org/post.
- In the Cron Schedule field, enter 0 0 * * *.
What does 0 0 * * * mean?
This is a cron expression. The five positions represent: minute, hour, day-of-month, month, day-of-week. So 0 0 * * * means "at minute 0, hour 0 (midnight), every day, every month, every day of the week."
All cron events in Hasura are scheduled based on UTC timezone.
- Notice the Frequently used crons dropdown next to the schedule field. Click it to see preset options like "every minute" or "every midnight." These are shortcuts so you do not have to memorize cron syntax.
- In the Payload field, enter the following JSON:
{"task": "cleanup_old_drafts"}
What is the payload for?
The payload is a JSON object sent to the webhook whenever the trigger fires. Your webhook handler reads this payload to know what task to perform.
In a real system, this might tell your cleanup service which table to clean or what age threshold to use for deleting old drafts.
- Click Add Cron Trigger to save and create the trigger.
You should see your daily_cleanup trigger appear in the Cron Triggers list. Hasura immediately starts scheduling events based on your cron expression.
Don't see the trigger in the list?
Make sure you entered the cron schedule in the correct format with spaces between each position. The expression 0 0 * * * must have exactly five space-separated values.
Also check that the webhook URL starts with https:// and is spelled exactly as https://httpbin.org/post.
Help me troubleshoot my Cron Trigger creation
Explore the Cron Trigger dashboard
Now that your trigger exists, Hasura provides a dashboard for monitoring its scheduled events and invocation history.
- Click on daily_cleanup in the Cron Triggers list to open its detail page.
You should see three tabs for monitoring this trigger.
- Click the Pending Events tab. This shows events that are scheduled but have not yet fired. You should see upcoming midnight events listed here.
- Click the Processed Events tab. This shows events that have already fired and been delivered to the webhook. Since midnight has not arrived yet, this tab is likely empty.
- Click the Invocation Logs tab. This shows the detailed request and response data for each invocation. When an event fires, you can expand it here to see exactly what Hasura sent to the webhook and what the webhook responded.
What about retry configuration?
Cron Triggers support retry logic for failed webhook calls. You can configure this in the Advanced Settings when creating or editing a trigger.
The retry settings control how many times Hasura retries a failed invocation, how many seconds to wait between retries, and how long to wait for a response before timing out. This ensures your scheduled tasks are resilient to temporary network failures.
Your Cron Trigger is live and scheduling events automatically. Next up, you will learn how to export and manage all of this configuration as metadata for version control and team collaboration.
Manage Hasura Metadata
You now have a complete Mini Blog backend with scheduled tasks running via Cron Triggers. Every table, relationship, permission, trigger, and action you configured throughout this project is stored somewhere.
That "somewhere" is called metadata. In this step, you will learn what metadata is, export it as a file, and understand the operations that make it useful for teams and deployment.
In this step, get ready to:
- Understand what Hasura metadata is and how it differs from your actual data.
- Export your metadata as a JSON file.
- Explore the metadata operations available in the Console.
Understand what Hasura metadata is
Everything you have configured in this project lives in two separate layers. Understanding the difference between them is important.
- Your data is the actual rows in your tables: the authors, posts, and comments you inserted.
- Your metadata is everything else: which tables are tracked, what relationships exist, what permissions are configured, which event triggers and actions are set up.
Metadata is Hasura's memory of your configuration. It tells Hasura how to generate your GraphQL API from the underlying database.
Where is metadata stored?
Internally, Hasura saves metadata as a single JSON blob in a table called hdb_metadata in a schema called hdb_catalog. This lives in the same PostgreSQL database you connected at the start of the project.
The key insight is that your blog data and Hasura's configuration data are stored separately, even though they share the same database server.
- In the Hasura Console, navigate to the Data tab.
- Look at the sidebar listing your tracked tables (authors, posts, comments, profiles).
- Notice the other tabs across the top: Actions, Events, API.
All of this configuration, across every tab, is metadata. If you deleted the metadata, your database tables would still exist. But Hasura would not know about them.
Export your metadata
Exporting metadata saves your entire Hasura configuration to a file. This file is what you would commit to version control so your team can reproduce the same setup.
- Click the gear icon in the top-right corner of the Console to open Settings.
- Find the Metadata Actions section on the page.
- Click Export Metadata.
- A .json file downloads to your computer.
- Open the downloaded JSON file in any text editor.
- Scroll through the file to see your configuration represented as structured data.
What is inside the metadata file?
The exported JSON contains every piece of configuration you set up throughout this project:
- All tracked tables (authors, posts, comments, profiles) with their column configurations.
- All relationships (object and array) between tables.
- All permissions for the user role on posts.
- The hello Action, the on_new_post Event Trigger, and the daily_cleanup Cron Trigger.
- Computed fields, custom root fields, and table comments.
This single file is a complete snapshot of your Hasura configuration. You could take this file to a brand new Hasura instance (with the same database tables) and restore your entire API setup.
Why is this useful for teams?
In a real project, you would commit this metadata file (or its YAML equivalent from the Hasura CLI) to Git. When a teammate clones the repository, they can apply the metadata to their own local Hasura instance and get an identical API configuration without manual setup.
This is how teams deploy to staging and production environments. The metadata file becomes the single source of truth for your API's structure.
Explore metadata operations
Exporting is just one of the metadata operations available. The Settings page also offers three other operations that are essential for managing your Hasura instance.
- Return to Settings (gear icon, top right) and look at the Metadata Actions section.
- Identify the three operations listed alongside Export: Reload Metadata, Clear Metadata, and Apply Metadata (import).
What does each operation do?
- Reload Metadata refreshes Hasura's knowledge of the database. Use this when you have made changes to the database outside of Hasura (for example, adding a column via psql) and need Hasura to pick them up.
- Clear Metadata removes all configuration but keeps the actual database tables intact. After clearing, Hasura forgets about your tracked tables, relationships, permissions, and triggers. This is irreversible, so always export first.
- Apply Metadata (import) restores configuration from a file. Use this to set up a new Hasura instance with an existing configuration, or to restore a previous state after clearing.
Do not click Clear Metadata
Clearing metadata is irreversible and will remove all the configuration you built throughout this project. Only use it if you intentionally want to start fresh. If you do clear it accidentally, you can re-apply the JSON file you just exported.
Together, these operations form a complete workflow: export your metadata before making risky changes, apply it to restore or replicate, reload when the underlying database changes externally, and clear only when you want a blank slate.
When would I use these in a real project?
Imagine you have a development, staging, and production environment. You build your API locally, export the metadata, commit it to Git, and then apply it to staging for testing. Once verified, you apply the same metadata to production.
This ensures every environment has the exact same API configuration without manual clicking in each Console.
Secret mission
Many-to-Many Relationships (Posts and Tags)
You've mastered one-to-one and one-to-many relationships. But what about the relationship type that trips up most developers? In this secret mission, you'll build a many-to-many relationship between posts and tags using a junction table, then secure it with permissions.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on local Docker containers, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Docker containers (Hasura GraphQL Engine and PostgreSQL).
- Docker volume (PostgreSQL data persistence).
- Local files (docker-compose.yml and hasura_metadata.json).
Keep everything running
No action needed. Choose this if you plan to keep exploring Hasura or want to pick up where you left off in your next session.
- Your Docker containers will keep running in the background until you restart your computer or stop Docker Desktop.
- To access the Hasura Console again at any time, open http://localhost:8080/console in your browser.
- Docker Desktop must be running for the containers to stay active.
Pause - I'll come back to this later
Stop running containers to free up memory and CPU. Your data is preserved in Docker volumes so everything will be exactly as you left it.
- Open Terminal and navigate to the folder where your docker-compose.yml file lives.
- Stop the containers by running:
docker compose stop
- When you want to resume, navigate to the same folder and run:
docker compose start
- Then open http://localhost:8080/console to pick up where you left off.
Delete - I don't want to use this again
Remove all project containers, volumes, and local files for a complete reset.
- Open Terminal and navigate to the folder where your docker-compose.yml file lives.
- Remove all containers, networks, and volumes by running:
docker compose down -v
What does this command do?
The -v flag removes the Docker volumes where PostgreSQL stored your database. Without it, your data would persist even after removing the containers.
- Delete the docker-compose.yml file by running:
rm docker-compose.yml
- Delete the exported metadata file by running:
rm hasura_metadata.json
- Confirm the files are removed by running:
ls
- You should no longer see docker-compose.yml or hasura_metadata.json in the output.
That's a Wrap!
That's a Wrap!
Nice work! You just built a complete Mini Blog backend while exploring every single feature Hasura has to offer.
Next time any database or API scenario comes up in development, you'll know exactly which Hasura feature to reach for.
You've learned how to:
- Set up a multi-table PostgreSQL database with every type of relationship, constraint, and permission, all exposed through Hasura's auto-generated GraphQL API.
- Extended your API beyond basic CRUD using Actions, Event Triggers, and Cron Triggers to handle custom logic, database reactions, and scheduled tasks.
- Managed your entire configuration as exportable metadata ready for version control and team deployment.
- Built a many-to-many relationship between posts and tags using a junction table and queried it with nested GraphQL.
Ready to quiz yourself?