This part builds upon the previous parts by demonstrating how to perform GraphQL operations with proper database permissions. You’ll learn how to design database tables, configure user permissions, and implement complete CRUD operations through GraphQL queries and mutations in a real todos application. 
This is Part 4  in the Full-Stack React Development with Nhost series. This part focuses on GraphQL operations, database management, and permission-based data access control in a production application. 
 
Full-Stack React Development with Nhost  
Prerequisites  
What You’ll Build  
By the end of this part, you’ll have: 
GraphQL queries and mutations  for complete CRUD operations 
Database schema  with proper relationships and constraints 
User permissions  for secure data access control 
React components  that interact with GraphQL endpoint 
 
Step-by-Step Guide  
Create the To-Dos Table First, we’ll perform the database changes to set up the todos table with proper schema and relationships to users. In your Nhost project dashboard: 
Navigate to Database  
Click on the SQL Editor 
 Enter the following SQL: CREATE  TABLE  public .todos (  
  id uuid  DEFAULT  gen_random_uuid()  NOT NULL ,  
  created_at  timestamptz  DEFAULT  now ()  NOT NULL ,  
  updated_at  timestamptz  DEFAULT  now ()  NOT NULL ,  
  title  text  NOT NULL ,  
  details  text ,  
  completed bool  DEFAULT  false  NOT NULL ,  
  user_id uuid  NOT NULL ,  
  PRIMARY KEY  (id),  
  FOREIGN KEY  (user_id)  REFERENCES  auth . users  (id)  ON UPDATE CASCADE  ON DELETE CASCADE  
);  
 
 
CREATE OR REPLACE  FUNCTION  update_updated_at_column ()  
RETURNS  TRIGGER  AS  $$  
BEGIN  
  NEW . updated_at  =  now ();  
  RETURN  NEW;  
END ;  
$$  language  'plpgsql' ;  
 
 
CREATE  TRIGGER  update_todos_updated_at  
  BEFORE  UPDATE  ON  public . todos  
  FOR  EACH  ROW  
  EXECUTE  FUNCTION  update_updated_at_column();  
 
Please make sure to enable Track this  so that the new table todos is available through the auto-generated APIs 
Set Up Permissions It’s now time to set permission rules for the table you just created. With the table todos selected, click on … , followed by Edit Permissions .
You will set permissions for the user  role and actions insert , select , update , and delete .  Insert
 Select
 Update
 Delete
When inserting permissions we are only allowing users to set the title, details, and completed columns as the rest of the columns are set automatically by the backend. The user_id column is configured as a preset to the currently authenticated user’s ID using the X-Hasura-User-Id session variable. This ensures that each todo is associated with the user who created it. 
Create the Todos Page Component Now let’s implement the React component that uses the database we just configured. import  type  {  JSX  }  from  "react" ;  
import  {  useCallback ,  useEffect ,  useId ,  useState  }  from  "react" ;  
import  {  useAuth  }  from  "../lib/nhost/AuthProvider" ;  
 
// The interfaces below define the structure of our data  
// They are not strictly necessary but help with type safety  
 
// Represents a single todo item  
interface  Todo  {  
  id :  string ;  
  title :  string ;  
  details :  string  |  null ;  
  completed :  boolean ;  
  created_at :  string ;  
  updated_at :  string ;  
  user_id :  string ;  
}  
 
// This matches the GraphQL response structure for fetching todos  
// Can be used as a generic type on the request method  
interface  GetTodos  {  
  todos :  Todo [];  
}  
 
// This matches the GraphQL response structure for inserting a todo  
// Can be used as a generic type on the request method  
interface  InsertTodo  {  
  insert_todos_one :  Todo  |  null ;  
}  
 
// This matches the GraphQL response structure for updating a todo  
// Can be used as a generic type on the request method  
interface  UpdateTodo  {  
  update_todos_by_pk :  Todo  |  null ;  
}  
 
export  default  function  Todos () :  JSX . Element  {  
  const  {  nhost ,  session  }  =  useAuth ();  
  const  [ todos ,  setTodos ]  =  useState < Todo []>([]);  
  const  [ loading ,  setLoading ]  =  useState ( true );  
  const  [ error ,  setError ]  =  useState < string  |  null >( null );  
  const  [ newTodoTitle ,  setNewTodoTitle ]  =  useState ( "" );  
  const  [ newTodoDetails ,  setNewTodoDetails ]  =  useState ( "" );  
  const  [ editingTodo ,  setEditingTodo ]  =  useState < Todo  |  null >( null );  
  const  [ showAddForm ,  setShowAddForm ]  =  useState ( false );  
  const  [ expandedTodos ,  setExpandedTodos ]  =  useState < Set < string >>( new  Set ());  
 
  const  titleId  =  useId ();  
  const  detailsId  =  useId ();  
 
  const  fetchTodos  =  useCallback ( async  ()  =>  {  
    try  {  
      setLoading ( true );  
      // Make GraphQL request to fetch todos using Nhost client  
      // The query automatically filters by user_id due to Hasura permissions  
      const  response  =  await  nhost . graphql . request < GetTodos >({  
        query:  `  
          query GetTodos {  
            todos(order_by: { created_at: desc }) {  
              id  
              title  
              details  
              completed  
              created_at  
              updated_at  
              user_id  
            }  
          }  
        ` ,  
      });  
 
      // Check for GraphQL errors in the response body  
      if  ( response . body . errors ) {  
        throw  new  Error (  
          response . body . errors [ 0 ]?. message  ||  "Failed to fetch todos" ,  
        );  
      }  
 
      // Extract todos from the GraphQL response data  
      setTodos ( response . body ?. data ?. todos  ||  []);  
      setError ( null );  
    }  catch  ( err ) {  
      setError ( err  instanceof  Error  ?  err . message  :  "Failed to fetch todos" );  
    }  finally  {  
      setLoading ( false );  
    }  
  }, [ nhost . graphql ]);  
 
  const  addTodo  =  async  ( e :  React . FormEvent )  =>  {  
    e . preventDefault ();  
    if  ( ! newTodoTitle . trim ())  return ;  
 
    try  {  
      // Execute GraphQL mutation to insert a new todo  
      // user_id is automatically set by Hasura based on JWT token  
      const  response  =  await  nhost . graphql . request < InsertTodo >({  
        query:  `  
          mutation InsertTodo($title: String!, $details: String) {  
            insert_todos_one(object: { title: $title, details: $details }) {  
              id  
              title  
              details  
              completed  
              created_at  
              updated_at  
              user_id  
            }  
          }  
        ` ,  
        variables:  {  
          title:  newTodoTitle . trim (),  
          details:  newTodoDetails . trim ()  ||  null ,  
        },  
      });  
 
      if  ( response . body . errors ) {  
        throw  new  Error (  
          response . body . errors [ 0 ]?. message  ||  "Failed to add todo" ,  
        );  
      }  
 
      if  ( ! response . body ?. data ?. insert_todos_one ) {  
        throw  new  Error ( "Failed to add todo" );  
      }  
      setTodos ([ response . body ?. data ?. insert_todos_one ,  ... todos ]);  
      setNewTodoTitle ( "" );  
      setNewTodoDetails ( "" );  
      setShowAddForm ( false );  
      setError ( null );  
    }  catch  ( err ) {  
      setError ( err  instanceof  Error  ?  err . message  :  "Failed to add todo" );  
    }  
  };  
 
  const  updateTodo  =  async  (  
    id :  string ,  
    updates :  Partial < Pick < Todo ,  "title"  |  "details"  |  "completed" >>,  
  )  =>  {  
    try  {  
      // Execute GraphQL mutation to update an existing todo by primary key  
      // Hasura permissions ensure users can only update their own todos  
      const  response  =  await  nhost . graphql . request < UpdateTodo >({  
        query:  `  
          mutation UpdateTodo($id: uuid!, $updates: todos_set_input!) {  
            update_todos_by_pk(pk_columns: { id: $id }, _set: $updates) {  
              id  
              title  
              details  
              completed  
              created_at  
              updated_at  
              user_id  
            }  
          }  
        ` ,  
        variables:  {  
          id ,  
          updates ,  
        },  
      });  
 
      if  ( response . body . errors ) {  
        throw  new  Error (  
          response . body . errors [ 0 ]?. message  ||  "Failed to update todo" ,  
        );  
      }  
 
      if  ( ! response . body ?. data ?. update_todos_by_pk ) {  
        throw  new  Error ( "Failed to update todo" );  
      }  
 
      const  updatedTodo  =  response . body ?. data ?. update_todos_by_pk ;  
      if  ( updatedTodo ) {  
        setTodos ( todos . map (( todo )  =>  ( todo . id  ===  id  ?  updatedTodo  :  todo )));  
      }  
      setEditingTodo ( null );  
      setError ( null );  
    }  catch  ( err ) {  
      setError ( err  instanceof  Error  ?  err . message  :  "Failed to update todo" );  
    }  
  };  
 
  const  deleteTodo  =  async  ( id :  string )  =>  {  
    if  ( ! confirm ( "Are you sure you want to delete this todo?" ))  return ;  
 
    try  {  
      // Execute GraphQL mutation to delete a todo by primary key  
      // Hasura permissions ensure users can only delete their own todos  
      const  response  =  await  nhost . graphql . request ({  
        query:  `  
          mutation DeleteTodo($id: uuid!) {  
            delete_todos_by_pk(id: $id) {  
              id  
            }  
          }  
        ` ,  
        variables:  {  
          id ,  
        },  
      });  
 
      if  ( response . body . errors ) {  
        throw  new  Error (  
          response . body . errors [ 0 ]?. message  ||  "Failed to delete todo" ,  
        );  
      }  
 
      setTodos ( todos . filter (( todo )  =>  todo . id  !==  id ));  
      setError ( null );  
    }  catch  ( err ) {  
      setError ( err  instanceof  Error  ?  err . message  :  "Failed to delete todo" );  
    }  
  };  
 
  const  toggleComplete  =  async  ( todo :  Todo )  =>  {  
    await  updateTodo ( todo . id , {  completed:  ! todo . completed  });  
  };  
 
  const  saveEdit  =  async  ()  =>  {  
    if  ( ! editingTodo )  return ;  
    await  updateTodo ( editingTodo . id , {  
      title:  editingTodo . title ,  
      details:  editingTodo . details ,  
    });  
  };  
 
  const  toggleTodoExpansion  =  ( todoId :  string )  =>  {  
    const  newExpanded  =  new  Set ( expandedTodos );  
    if  ( newExpanded . has ( todoId )) {  
      newExpanded . delete ( todoId );  
    }  else  {  
      newExpanded . add ( todoId );  
    }  
    setExpandedTodos ( newExpanded );  
  };  
 
  // Fetch todos when user session is available  
  // The session contains the JWT token needed for GraphQL authentication  
  useEffect (()  =>  {  
    if  ( session ) {  
      fetchTodos ();  
    }  
  }, [ session ,  fetchTodos ]);  
 
  if  ( ! session ) {  
    return  (  
      < div  className = "auth-message" >  
        < p > Please sign in to view your todos. </ p >  
      </ div >  
    );  
  }  
 
  return  (  
    < div  className = "container" >  
      < header  className = "page-header" >  
        < h1  className = "page-title" >  
          My Todos  
          { ! showAddForm  &&  (  
            < button  
              type = "button"  
              onClick = { ()  =>  setShowAddForm ( true ) }  
              className = "add-todo-btn"  
              title = "Add a new todo"  
            >  
              +  
            </ button >  
          ) }  
        </ h1 >  
      </ header >  
 
      { error  &&  (  
        < div  className = "error-message" >  
          < strong > Error: </ strong >  { error }  
        </ div >  
      ) }  
 
      { showAddForm  &&  (  
        < div  className = "todo-form-card" >  
          < form  onSubmit = { addTodo }  className = "todo-form" >  
            < h2  className = "form-title" > Add New Todo </ h2 >  
            < div  className = "form-fields" >  
              < div  className = "field-group" >  
                < label  htmlFor = { titleId } > Title * </ label >  
                < input  
                  id = { titleId }  
                  type = "text"  
                  value = { newTodoTitle }  
                  onChange = { ( e )  =>  setNewTodoTitle ( e . target . value ) }  
                  placeholder = "What needs to be done?"  
                  required  
                />  
              </ div >  
              < div  className = "field-group" >  
                < label  htmlFor = { detailsId } > Details </ label >  
                < textarea  
                  id = { detailsId }  
                  value = { newTodoDetails }  
                  onChange = { ( e )  =>  setNewTodoDetails ( e . target . value ) }  
                  placeholder = "Add some details (optional)..."  
                  rows = { 3 }  
                />  
              </ div >  
              < div  className = "form-actions" >  
                < button  type = "submit"  className = "btn btn-primary" >  
                  Add Todo  
                </ button >  
                < button  
                  type = "button"  
                  onClick = { ()  =>  {  
                    setShowAddForm ( false );  
                    setNewTodoTitle ( "" );  
                    setNewTodoDetails ( "" );  
                  } }  
                  className = "btn btn-secondary"  
                >  
                  Cancel  
                </ button >  
              </ div >  
            </ div >  
          </ form >  
        </ div >  
      ) }  
 
      { ! showAddForm  &&  
        ( loading  ?  (  
          < div  className = "loading-container" >  
            < div  className = "loading-content" >  
              < div  className = "spinner" ></ div >  
              < span  className = "loading-text" > Loading todos... </ span >  
            </ div >  
          </ div >  
        )  :  (  
          < div  className = "todos-list" >  
            { todos . length  ===  0  ?  (  
              < div  className = "empty-state" >  
                < svg  
                  className = "empty-icon"  
                  fill = "none"  
                  stroke = "currentColor"  
                  viewBox = "0 0 24 24"  
                  aria-hidden = "true"  
                >  
                  < path  
                    strokeLinecap = "round"  
                    strokeLinejoin = "round"  
                    strokeWidth = { 1.5 }  
                    d = "M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"  
                  />  
                </ svg >  
                < h3  className = "empty-title" > No todos yet </ h3 >  
                < p  className = "empty-description" >  
                  Create your first todo to get started!  
                </ p >  
              </ div >  
            )  :  (  
              todos . map (( todo )  =>  (  
                < div  
                  key = { todo . id }  
                  className = { `todo-card  ${ todo . completed  ?  "completed"  :  "" } ` }  
                >  
                  { editingTodo ?. id  ===  todo . id  ?  (  
                    < div  className = "todo-edit" >  
                      < div  className = "edit-fields" >  
                        < div  className = "field-group" >  
                          < label  htmlFor = { ` ${ titleId } -edit` } > Title </ label >  
                          < input  
                            id = { ` ${ titleId } -edit` }  
                            type = "text"  
                            value = { editingTodo . title }  
                            onChange = { ( e )  =>  
                              setEditingTodo ({  
                                ... editingTodo ,  
                                title:  e . target . value ,  
                              })  
                            }  
                          />  
                        </ div >  
                        < div  className = "field-group" >  
                          < label  htmlFor = { ` ${ detailsId } -edit` } > Details </ label >  
                          < textarea  
                            id = { ` ${ detailsId } -edit` }  
                            value = { editingTodo . details  ||  "" }  
                            onChange = { ( e )  =>  
                              setEditingTodo ({  
                                ... editingTodo ,  
                                details:  e . target . value ,  
                              })  
                            }  
                            rows = { 3 }  
                          />  
                        </ div >  
                        < div  className = "edit-actions" >  
                          < button  
                            type = "button"  
                            onClick = { saveEdit }  
                            className = "btn btn-primary"  
                          >  
                            ✓ Save Changes  
                          </ button >  
                          < button  
                            type = "button"  
                            onClick = { ()  =>  setEditingTodo ( null ) }  
                            className = "btn btn-cancel"  
                          >  
                            ✕ Cancel  
                          </ button >  
                        </ div >  
                      </ div >  
                    </ div >  
                  )  :  (  
                    < div  className = "todo-content" >  
                      < div  className = "todo-header" >  
                        < button  
                          type = "button"  
                          className = { `todo-title-btn  ${ todo . completed  ?  "completed"  :  "" } ` }  
                          onClick = { ()  =>  toggleTodoExpansion ( todo . id ) }  
                        >  
                          { todo . title }  
                        </ button >  
                        < div  className = "todo-actions" >  
                          < button  
                            type = "button"  
                            onClick = { ()  =>  toggleComplete ( todo ) }  
                            className = "action-btn action-btn-complete"  
                            title = {  
                              todo . completed  
                                ?  "Mark as incomplete"  
                                :  "Mark as complete"  
                            }  
                          >  
                            { todo . completed  ?  "↶"  :  "✓" }  
                          </ button >  
                          < button  
                            type = "button"  
                            onClick = { ()  =>  setEditingTodo ( todo ) }  
                            className = "action-btn action-btn-edit"  
                            title = "Edit todo"  
                          >  
                            ✏️  
                          </ button >  
                          < button  
                            type = "button"  
                            onClick = { ()  =>  deleteTodo ( todo . id ) }  
                            className = "action-btn action-btn-delete"  
                            title = "Delete todo"  
                          >  
                            🗑️  
                          </ button >  
                        </ div >  
                      </ div >  
 
                      { expandedTodos . has ( todo . id )  &&  (  
                        < div  className = "todo-details" >  
                          { todo . details  &&  (  
                            < div  
                              className = { `todo-description  ${ todo . completed  ?  "completed"  :  "" } ` }  
                            >  
                              < p > { todo . details } </ p >  
                            </ div >  
                          ) }  
 
                          < div  className = "todo-meta" >  
                            < div  className = "meta-dates" >  
                              < span  className = "meta-item" >  
                                Created: { " " }  
                                {new  Date ( todo . created_at ). toLocaleString () }  
                              </ span >  
                              < span  className = "meta-item" >  
                                Updated: { " " }  
                                {new  Date ( todo . updated_at ). toLocaleString () }  
                              </ span >  
                            </ div >  
                            { todo . completed  &&  (  
                              < div  className = "completion-badge" >  
                                < svg  
                                  className = "completion-icon"  
                                  fill = "none"  
                                  stroke = "currentColor"  
                                  viewBox = "0 0 24 24"  
                                  aria-hidden = "true"  
                                >  
                                  < path  
                                    strokeLinecap = "round"  
                                    strokeLinejoin = "round"  
                                    strokeWidth = { 2 }  
                                    d = "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"  
                                  />  
                                </ svg >  
                                < span > Completed </ span >  
                              </ div >  
                            ) }  
                          </ div >  
                        </ div >  
                      ) }  
                    </ div >  
                  ) }  
                </ div >  
              ))  
            ) }  
          </ div >  
        )) }  
    </ div >  
  );  
}  
Update App Routes Add the todos page to your application routing. import  {  
  createBrowserRouter ,  
  createRoutesFromElements ,  
  Navigate ,  
  Outlet ,  
  Route ,  
  RouterProvider ,  
}  from  "react-router-dom" ;  
import  Navigation  from  "./components/Navigation" ;  
import  ProtectedRoute  from  "./components/ProtectedRoute" ;  
import  {  AuthProvider  }  from  "./lib/nhost/AuthProvider" ;  
import  Home  from  "./pages/Home" ;  
import  Profile  from  "./pages/Profile" ;  
import  SignIn  from  "./pages/SignIn" ;  
import  SignUp  from  "./pages/SignUp" ;  
import  Todos  from  "./pages/Todos" ;  
import  Verify  from  "./pages/Verify" ;  
 
// Root layout component to wrap all routes  
const  RootLayout  =  ()  =>  {  
  return  (  
    <>  
      < Navigation  />  
      < div  className = "app-content" >  
        < Outlet  />  
      </ div >  
    </>  
  );  
};  
 
// Create router with routes  
const  router  =  createBrowserRouter (  
  createRoutesFromElements (  
    < Route  element = { < RootLayout  /> } >  
      < Route  index  element = { < Home  /> }  />  
      < Route  path = "signin"  element = { < SignIn  /> }  />  
      < Route  path = "signup"  element = { < SignUp  /> }  />  
      < Route  path = "verify"  element = { < Verify  /> }  />  
      < Route  element = { < ProtectedRoute  /> } >  
        < Route  path = "profile"  element = { < Profile  /> }  />  
        < Route  path = "todos"  element = { < Todos  /> }  />  
      </ Route >  
      < Route  path = "*"  element = { < Navigate  to = "/"  /> }  />  
    </ Route > ,  
  ),  
);  
 
function  App () {  
  return  (  
    < AuthProvider >  
      < RouterProvider  router = { router }  />  
    </ AuthProvider >  
  );  
}  
 
export  default  App ;  
Update Navigation Links Add a link to the todos page in the navigation bar. src/components/Navigation.tsx
import  {  Link ,  useNavigate  }  from  "react-router-dom" ;  
import  {  useAuth  }  from  "../lib/nhost/AuthProvider" ;  
 
export  default  function  Navigation ()  {  
  const  {  isAuthenticated ,  session ,  nhost  }  =  useAuth ();  
  const  navigate  =  useNavigate ();  
 
  const  handleSignOut  =  async  ()  =>  {  
    try  {  
      if  ( session ) {  
        await  nhost . auth . signOut ({  
          refreshToken:  session . refreshToken ,  
        });  
      }  
      navigate ( "/" );  
    }  catch  ( err :  unknown ) {  
      const  message  =  err  instanceof  Error  ?  err . message  :  String ( err );  
      console . error ( "Error signing out:" ,  message );  
    }  
  };  
 
  return  (  
    < nav  className = "navigation" >  
      < div  className = "nav-container" >  
        < Link  to = "/"  className = "nav-logo" >  
          Nhost React Demo  
        </ Link >  
 
        < div  className = "nav-links" >  
          < Link  to = "/"  className = "nav-link" >  
            Home  
          </ Link >  
 
          { isAuthenticated  ?  (  
            <>  
              < Link  to = "/todos"  className = "nav-link" >  
                Todos  
              </ Link >  
              < Link  to = "/profile"  className = "nav-link" >  
                Profile  
              </ Link >  
              < button  
                type = "button"  
                onClick = { handleSignOut }  
                className = "nav-link nav-button"  
              >  
                Sign Out  
              </ button >  
            </>  
          )  :  (  
            <>  
              < Link  to = "/signin"  className = "nav-link" >  
                Sign In  
              </ Link >  
              < Link  to = "/signup"  className = "nav-link" >  
                Sign Up  
              </ Link >  
            </>  
          ) }  
        </ div >  
      </ div >  
    </ nav >  
  );  
}  
Test Your Complete Application Run your application and test all the functionality: Things to try out: 
Try signing in and out and see how the Todos page is only available when authenticated 
Create, view, edit, complete, and delete todos. See how the UI updates accordingly 
Open the application in another browser or incognito window, sign in with a different account and verify that you cannot see or modify todos from the first account 
  
Key Features Implemented  
Properly designed todos table with constraints, indexes, and automatic timestamp updates for optimal performance. 
Auto-generated GraphQL API with queries and mutations for full CRUD operations on todos. 
Comprehensive permissions ensuring users can only access their own todos through all GraphQL operations. 
Complete Create, Read, Update, Delete functionality with proper error handling and user feedback. 
Expandable todo items, inline editing, completion status, and detailed timestamps.