This is the full developer documentation for Nhost # Build. Deploy. Scale > Learn how to get started with Nhost # Get started with our guides or explore our community ### [Getting Started](/getting-started) [Learn the fundamentals of Nhost and how to set up your first project](/getting-started) ### [React](/getting-started/quickstart/react) [Integrate Nhost with your React application](/getting-started/quickstart/react) ### [API Reference](/reference) [Explore the API reference for client SDKs and data APIs](/reference) ### [Platform](/platform) [Learn about the Nhost Platform and deployment targets](/platform) ### [Products](/products) [Explore our integrated products like database, auth, GraphQL & storage.](/products) ### [Example Applications](https://github.com/nhost/nhost/tree/main/examples) [Deployable applications built with Nhost](https://github.com/nhost/nhost/tree/main/examples) # What is Nhost? > Nhost is a backend platform for building production apps with PostgreSQL, GraphQL, authentication, file storage, and serverless functions. Nhost is a backend development platform designed to help you build and scale production-ready applications fast, with powerful, fully managed infrastructure. It eliminates backend complexity by providing a performant and reliable software stack ready-to-go, while still offering the flexibility to customize and grow as your app grows. ![Hero Light](/images/nhost-overview-light.svg)![Hero Light](/images/nhost-overview-dark.svg) ## Next steps [Section titled “Next steps”](#next-steps) Follow one of our quick start guides for learning how to quickly setup Nhost with your framework of choice, in just a few easy steps. ### [Nhost CLI](/getting-started/local-development/cli) [Learn how to setup a local Nhost environment](/getting-started/local-development/cli) ### [Next.js](/getting-started/quickstart/nextjs) [Learn how to connect Nhost with Next.js](/getting-started/quickstart/nextjs) ### [React](/getting-started/quickstart/react) [Learn how to connect Nhost with React](/getting-started/quickstart/react) ### [Vue](/getting-started/quickstart/vue) [Learn how to connect Nhost with Vue](/getting-started/quickstart/vue) ### [React Native](/getting-started/quickstart/reactnative) [Learn how to connect Nhost with React Native](/getting-started/quickstart/reactnative) ## Follow a tutorial to build a Todo Manager [Section titled “Follow a tutorial to build a Todo Manager”](#follow-a-tutorial-to-build-a-todo-manager) Follow one of your tutorials where we walk you through building a Todo Manager application using features from Nhost. ### [Next.js](/getting-started/tutorials/nextjs/1-introduction) [Todo Manager with Nhost and NextJS](/getting-started/tutorials/nextjs/1-introduction) ### [React](/getting-started/tutorials/react/1-introduction) [Todo Manager with Nhost and React](/getting-started/tutorials/react/1-introduction) ### [Vue](/getting-started/tutorials/vue/1-introduction) [Todo Manager with Nhost and Vue](/getting-started/tutorials/vue/1-introduction) ### [Svelte](/getting-started/tutorials/svelte/1-introduction) [Todo Manager with Nhost and SvelteKit](/getting-started/tutorials/svelte/1-introduction) ### [React Native](/getting-started/tutorials/reactnative/1-introduction) [Todo Manager with Nhost and React Native](/getting-started/tutorials/reactnative/1-introduction) # Get up and running with the Nhost CLI > Start developing locally with Nhost. Install the CLI, initialize your project, and spin up a local PostgreSQL database, GraphQL engine, Auth, and Storage. The Nhost CLI is the recommended way to start building your application. It sets up a full local development environment including PostgreSQL, GraphQL, Authentication, and Storage, tracking all migrations and metadata. [Docker](https://docs.docker.com/get-docker/) is required to run the local environment. ### 1. Install the CLI [Section titled “1. Install the CLI”](#1-install-the-cli) **Supported platforms** * macOS * Linux * Windows (via WSL2)1 Caution 1 Windows isn’t supported natively. WSL2 is required to install and use the Nhost CLI. We recommend using docker-ce inside WSL2 instead of Docker Desktop. Choose your preferred installation method: * npm ```sh npm install -g @nhost/cli ``` * pnpm ```sh pnpm add -g @nhost/cli ``` * Bun ```sh bun add -g @nhost/cli ``` * Yarn ```sh yarn global add @nhost/cli # Yarn Classic ``` * Homebrew ```sh brew install nhost/tap/nhost ``` * cURL script ```sh curl -sSL https://raw.githubusercontent.com/nhost/nhost/main/cli/get.sh | bash ``` * Nix ```sh nix profile install github:nhost/nhost#cli ``` Or run the CLI without installation: * npm ```sh npx @nhost/cli@latest --version ``` * pnpm ```sh pnpm dlx @nhost/cli@latest --version ``` * Bun ```sh bunx @nhost/cli@latest --version ``` * Yarn ```sh yarn dlx @nhost/cli@latest --version ``` ### 2. Authenticate [Section titled “2. Authenticate”](#2-authenticate) Login to your Nhost account. This is required if you want to pull configuration from an existing Nhost Cloud project (step 3) or deploy later. You can skip it if you only want to develop locally. ```sh nhost login ``` Note If you registered with GitHub, set a password on your [account page](https://app.nhost.io/account) first. ### 3. Initialize a Project [Section titled “3. Initialize a Project”](#3-initialize-a-project) Initialize a new Nhost project in your current directory. ```sh nhost init ``` This scaffolds your project — a `nhost/` directory holding your backend configuration (all version-controlled in Git) plus a `functions/` directory for serverless functions: ```sh . ├─ 📁 functions # Serverless functions (Node.js / TypeScript) └─ 📁 nhost # Project & backend configuration ├─ 📁 emails # Email templates (verification, password reset, …) ├─ 📁 metadata # Hasura metadata — tables, permissions, relationships ├─ 📁 migrations # Database schema migrations └─ 📁 seeds # Seed data for local development ``` If you already have a project on Nhost Cloud and want to pull down its configuration: ```sh nhost init --remote ``` ### 4. Start Local Environment [Section titled “4. Start Local Environment”](#4-start-local-environment) Start the full Nhost stack locally. This uses Docker to spin up all the necessary services. ```sh nhost up ``` Once running, the CLI prints the URLs for your local services: *** | Service | URLs | | -------------- | --------------------------------------------------- | | **Postgres:** | | | **Hasura:** | | | **GraphQL:** | | | **Auth:** | | | **Storage:** | | | **Functions:** | | | **Dashboard:** | | | **Mailhog:** | | The `local.nhost.run` domain resolves to your machine, so these URLs work over HTTPS out of the box. To stop the environment run `nhost down`, and to follow the logs run `nhost logs`. ### 5. Open the Dashboard [Section titled “5. Open the Dashboard”](#5-open-the-dashboard) Open the local dashboard by going to to manage your database, auth settings, storage, and more. ![Nhost local dashboard](/images/cli/local-dashboard.png) ### 6. Connect an AI Assistant (optional) [Section titled “6. Connect an AI Assistant (optional)”](#6-connect-an-ai-assistant-optional) The CLI ships an MCP server that lets an AI assistant read your GraphQL schema, run queries and mutations, and manage metadata and migrations against your local project. The fastest way to set it up is to hand the onboarding file to your assistant: * Claude Code ```sh curl -fsSL https://docs.nhost.io/install-mcp | claude ``` * Codex CLI ```sh curl -fsSL https://docs.nhost.io/install-mcp | codex exec - ``` * Gemini CLI ```sh curl -fsSL https://docs.nhost.io/install-mcp | gemini ``` * Other ```sh curl -fsSL https://docs.nhost.io/install-mcp | ``` See [MCP Development Setup](/platform/cli/mcp/development-setup) for the manual steps and client configuration. ### Next Steps [Section titled “Next Steps”](#next-steps) Now that your local backend is running, you can connect your frontend application to it or dive deeper into the CLI. ### [Next.js](/getting-started/quickstart/nextjs) [Connect your local Nhost backend to a Next.js app](/getting-started/quickstart/nextjs) ### [React](/getting-started/quickstart/react) [Connect your local Nhost backend to a React app](/getting-started/quickstart/react) ### [CLI Reference](/reference/cli/commands) [Full reference for all CLI commands and flags](/reference/cli/commands) ### [Local Development](/platform/cli/local-development) [Go deeper: database migrations, metadata, and deploying to Nhost](/platform/cli/local-development) ### [MCP Server](/platform/cli/mcp/development-setup) [Connect an AI assistant to your local project](/platform/cli/mcp/development-setup) # Get up and running with Nhost and Next.js > Build a Next.js app with Nhost in minutes: connect the SDK, run GraphQL queries, and add authentication with server-side rendering support. ### Create Project [Section titled “Create Project”](#create-project) If you haven’t, please create a project through the [Nhost Dashboard](https://app.nhost.io). ### Setup Database [Section titled “Setup Database”](#setup-database) Navigate to **Database → SQL Console** and run the following SQL to create a new table `movies` with some great movies. ```sql CREATE TABLE movies ( id SERIAL PRIMARY KEY, title VARCHAR(255) NOT NULL, director VARCHAR(255), release_year INTEGER, genre VARCHAR(100), rating FLOAT ); INSERT INTO movies (title, director, release_year, genre, rating) VALUES ('Inception', 'Christopher Nolan', 2010, 'Sci-Fi', 8.8), ('The Godfather', 'Francis Ford Coppola', 1972, 'Crime', 9.2), ('Forrest Gump', 'Robert Zemeckis', 1994, 'Drama', 8.8), ('The Matrix', 'Lana Wachowski, Lilly Wachowski', 1999, 'Action', 8.7); ``` Caution Make sure the option `Track this` is enabled ![SQL Editor](/images/quickstarts/react/sql-editor.png) ### Permissions [Section titled “Permissions”](#permissions) Select the new table `movies` just created, and click in **Edit Permissions** to set the following permissions for the `public` role and `select` action. ![Permission Rules](/images/quickstarts/react/permissions.png) ### Setup the Application [Section titled “Setup the Application”](#setup-the-application) Create the application using standard tooling: ```bash npx create-next-app@latest my-app --yes --js --src-dir cd my-app ``` ### Install the Nhost package [Section titled “Install the Nhost package”](#install-the-nhost-package) Navigate to the application and install `@nhost/nhost-js`. ```bash npm install @nhost/nhost-js ``` ### Configure the Nhost client and fetch the list of movies [Section titled “Configure the Nhost client and fetch the list of movies”](#configure-the-nhost-client-and-fetch-the-list-of-movies) * ./src/lib/nhost.js ```js import { createClient } from "@nhost/nhost-js"; // Replace and with your project's values export const nhost = createClient({ subdomain: "", region: "", }) ``` * ./src/app/page.js ```js import { nhost } from '../lib/nhost' async function getMovies() { try { const resp = await nhost.graphql.request({ query: `query GetMovies { movies { title director release_year genre rating } }`, }) return resp.body?.data?.movies || [] } catch (error) { console.error('Failed to fetch movies:', error) return [] } } export default async function Home() { const movies = await getMovies() return (

Movies

{movies.map((movie, index) => ( ))}
Title Director Release Year Genre Rating
{movie.title} {movie.director} {movie.release_year} {movie.genre} {movie.rating}
) } ``` ### The end [Section titled “The end”](#the-end) Run your project with `npm run dev` and navigate to `http://localhost:3000` in your browser. ## Related resources [Section titled “Related resources”](#related-resources) * [Full source code](https://github.com/nhost/nhost/tree/main/examples/quickstarts/nextjs) on GitHub — a complete example configured for a local Nhost project # Get up and running with Nhost and React > Build a React app with Nhost in minutes: install the SDK with Vite, run GraphQL queries, and add authentication to your frontend. ### Create Project [Section titled “Create Project”](#create-project) If you haven’t, please create a project through the [Nhost Dashboard](https://app.nhost.io). ### Setup Database [Section titled “Setup Database”](#setup-database) Navigate to **Database → SQL Console** and run the following SQL to create a new table `movies` with some great movies. ```sql CREATE TABLE movies ( id SERIAL PRIMARY KEY, title VARCHAR(255) NOT NULL, director VARCHAR(255), release_year INTEGER, genre VARCHAR(100), rating FLOAT ); INSERT INTO movies (title, director, release_year, genre, rating) VALUES ('Inception', 'Christopher Nolan', 2010, 'Sci-Fi', 8.8), ('The Godfather', 'Francis Ford Coppola', 1972, 'Crime', 9.2), ('Forrest Gump', 'Robert Zemeckis', 1994, 'Drama', 8.8), ('The Matrix', 'Lana Wachowski, Lilly Wachowski', 1999, 'Action', 8.7); ``` Caution Make sure the option `Track this` is enabled ![SQL Editor](/images/quickstarts/react/sql-editor.png) ### Permissions [Section titled “Permissions”](#permissions) Select the new table `movies` just created, and click in **Edit Permissions** to set the following permissions for the `public` role and `select` action. ![Permission Rules](/images/quickstarts/react/permissions.png) ### Setup the Application [Section titled “Setup the Application”](#setup-the-application) Create the application using standard tooling: ```bash npm create vite@latest my-react-app -- --template react cd my-react-app ``` ### Install the Nhost package [Section titled “Install the Nhost package”](#install-the-nhost-package) Navigate to the application and install `@nhost/nhost-js`. ```bash npm install @nhost/nhost-js ``` ### Configure the Nhost client and fetch the list of movies [Section titled “Configure the Nhost client and fetch the list of movies”](#configure-the-nhost-client-and-fetch-the-list-of-movies) * ./src/lib/nhost.js ```js import { createClient } from "@nhost/nhost-js"; // Replace and with your project's values export const nhost = createClient({ subdomain: "", region: "", }) ``` * ./src/App.jsx ```js import { useEffect, useState } from 'react' import { nhost } from './lib/nhost' import './App.css' function App() { const [movies, setMovies] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) useEffect(() => { const fetchMovies = async () => { try { const resp = await nhost.graphql.request({ query: `query GetMovies { movies { title director release_year genre rating } }`, }) setMovies(resp.body.data.movies || []) } catch (err) { setError('Failed to fetch movies') } finally { setLoading(false) } } fetchMovies() }, []) if (loading) return
Loading...
if (error) return
Error: {error}
return (

Movies

{movies.map((movie, index) => ( ))}
Title Director Release Year Genre Rating
{movie.title} {movie.director} {movie.release_year} {movie.genre} {movie.rating}
) } export default App ``` ### The end [Section titled “The end”](#the-end) Run your project with `npm run dev` and navigate to `http://localhost:5173` in your browser. ## Related resources [Section titled “Related resources”](#related-resources) * [Full source code](https://github.com/nhost/nhost/tree/main/examples/quickstarts/react) on GitHub — a complete example configured for a local Nhost project # Get up and running with Nhost and React Native > Build a React Native app with Nhost and Expo in minutes: set up the SDK, run GraphQL queries, and add mobile authentication. ### Create Project [Section titled “Create Project”](#create-project) If you haven’t, please create a project through the [Nhost Dashboard](https://app.nhost.io). ### Setup Database [Section titled “Setup Database”](#setup-database) Navigate to **Database → SQL Console** and run the following SQL to create a new table `movies` with some great movies. ```sql CREATE TABLE movies ( id SERIAL PRIMARY KEY, title VARCHAR(255) NOT NULL, director VARCHAR(255), release_year INTEGER, genre VARCHAR(100), rating FLOAT ); INSERT INTO movies (title, director, release_year, genre, rating) VALUES ('Inception', 'Christopher Nolan', 2010, 'Sci-Fi', 8.8), ('The Godfather', 'Francis Ford Coppola', 1972, 'Crime', 9.2), ('Forrest Gump', 'Robert Zemeckis', 1994, 'Drama', 8.8), ('The Matrix', 'Lana Wachowski, Lilly Wachowski', 1999, 'Action', 8.7); ``` Caution Make sure the option `Track this` is enabled ![SQL Editor](/images/quickstarts/react/sql-editor.png) ### Permissions [Section titled “Permissions”](#permissions) Select the new table `movies` just created, and click in **Edit Permissions** to set the following permissions for the `public` role and `select` action. ![Permission Rules](/images/quickstarts/react/permissions.png) ### Setup the Application [Section titled “Setup the Application”](#setup-the-application) Create the application using standard tooling: ```bash npx create-expo-app@latest --template default -y my-reactnative-app cd my-reactnative-app ``` ### Install the Nhost package [Section titled “Install the Nhost package”](#install-the-nhost-package) Navigate to the application and install `@nhost/nhost-js`. ```bash npm install @nhost/nhost-js ``` ### Configure the Nhost client and fetch the list of movies [Section titled “Configure the Nhost client and fetch the list of movies”](#configure-the-nhost-client-and-fetch-the-list-of-movies) * ./lib/nhost.js ```js import { createClient } from "@nhost/nhost-js"; // Replace and with your project's values export const nhost = createClient({ subdomain: "", region: "", }) ``` * ./app/(tabs)/index.tsx ```js import { useEffect, useState } from 'react'; import { ScrollView, StyleSheet, ActivityIndicator } from 'react-native'; import { ThemedText } from '@/components/themed-text'; import { ThemedView } from '@/components/themed-view'; import { nhost } from '@/lib/nhost'; export default function HomeScreen() { const [movies, setMovies] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchMovies = async () => { try { const resp = await nhost.graphql.request({ query: `query GetMovies { movies { title director release_year genre rating } }`, }); setMovies(resp.body.data.movies || []); } catch (err) { setError('Failed to fetch movies'); } finally { setLoading(false); } }; fetchMovies(); }, []); if (loading) { return ( Loading... ); } if (error) { return ( Error: {error} ); } return ( Movies {movies.map((movie, index) => ( {movie.title} Director: {movie.director} Year: {movie.release_year} Genre: {movie.genre} Rating: {movie.rating} ))} ); } const styles = StyleSheet.create({ container: { flex: 1, padding: 16, }, header: { marginBottom: 20, alignItems: 'center', }, movieCard: { backgroundColor: 'rgba(0, 0, 0, 0.1)', padding: 16, marginBottom: 12, borderRadius: 8, }, }); ``` ### The end [Section titled “The end”](#the-end) Run your project with `npm run web` and navigate to `http://localhost:8081` in your browser. ## Related resources [Section titled “Related resources”](#related-resources) * [Full source code](https://github.com/nhost/nhost/tree/main/examples/quickstarts/reactnative) on GitHub — a complete example configured for a local Nhost project # Get up and running with Nhost and SvelteKit > Build a SvelteKit app with Nhost in minutes: install the SDK with Vite, run GraphQL queries, and add authentication to your frontend. ### Create Project [Section titled “Create Project”](#create-project) If you haven’t, please create a project through the [Nhost Dashboard](https://app.nhost.io). ### Setup Database [Section titled “Setup Database”](#setup-database) Navigate to **Database → SQL Console** and run the following SQL to create a new table `movies` with some great movies. ```sql CREATE TABLE movies ( id SERIAL PRIMARY KEY, title VARCHAR(255) NOT NULL, director VARCHAR(255), release_year INTEGER, genre VARCHAR(100), rating FLOAT ); INSERT INTO movies (title, director, release_year, genre, rating) VALUES ('Inception', 'Christopher Nolan', 2010, 'Sci-Fi', 8.8), ('The Godfather', 'Francis Ford Coppola', 1972, 'Crime', 9.2), ('Forrest Gump', 'Robert Zemeckis', 1994, 'Drama', 8.8), ('The Matrix', 'Lana Wachowski, Lilly Wachowski', 1999, 'Action', 8.7); ``` Caution Make sure the option `Track this` is enabled ![SQL Editor](/images/quickstarts/react/sql-editor.png) ### Permissions [Section titled “Permissions”](#permissions) Select the new table `movies` just created, and click in **Edit Permissions** to set the following permissions for the `public` role and `select` action. ![Permission Rules](/images/quickstarts/react/permissions.png) ### Setup the Application [Section titled “Setup the Application”](#setup-the-application) Create the application using standard tooling: ```bash npm create vite@latest my-svelte-app -- --template svelte cd my-svelte-app ``` ### Install the Nhost package [Section titled “Install the Nhost package”](#install-the-nhost-package) Navigate to the application and install `@nhost/nhost-js`. ```bash npm install @nhost/nhost-js ``` ### Configure the Nhost client and fetch the list of movies [Section titled “Configure the Nhost client and fetch the list of movies”](#configure-the-nhost-client-and-fetch-the-list-of-movies) * ./src/lib/nhost.js ```js import { createClient } from "@nhost/nhost-js"; // Replace and with your project's values export const nhost = createClient({ subdomain: "", region: "", }) ``` * ./src/App.svelte ```svelte {#if loading}
Loading...
{:else if error}
Error: {error}
{:else}

Movies

{#each movies as movie, index (index)} {/each}
Title Director Release Year Genre Rating
{movie.title} {movie.director} {movie.release_year} {movie.genre} {movie.rating}
{/if} ``` ### The end [Section titled “The end”](#the-end) Run your project with `npm run dev` and navigate to `http://localhost:5173` in your browser. ## Related resources [Section titled “Related resources”](#related-resources) * [Full source code](https://github.com/nhost/nhost/tree/main/examples/quickstarts/svelte) on GitHub — a complete example configured for a local Nhost project # Get up and running with Nhost and Vue > Build a Vue app with Nhost in minutes: install the SDK with Vite, run GraphQL queries, and add authentication to your frontend. ### Create Project [Section titled “Create Project”](#create-project) If you haven’t, please create a project through the [Nhost Dashboard](https://app.nhost.io). ### Setup Database [Section titled “Setup Database”](#setup-database) Navigate to **Database → SQL Console** and run the following SQL to create a new table `movies` with some great movies. ```sql CREATE TABLE movies ( id SERIAL PRIMARY KEY, title VARCHAR(255) NOT NULL, director VARCHAR(255), release_year INTEGER, genre VARCHAR(100), rating FLOAT ); INSERT INTO movies (title, director, release_year, genre, rating) VALUES ('Inception', 'Christopher Nolan', 2010, 'Sci-Fi', 8.8), ('The Godfather', 'Francis Ford Coppola', 1972, 'Crime', 9.2), ('Forrest Gump', 'Robert Zemeckis', 1994, 'Drama', 8.8), ('The Matrix', 'Lana Wachowski, Lilly Wachowski', 1999, 'Action', 8.7); ``` Caution Make sure the option `Track this` is enabled ![SQL Editor](/images/quickstarts/react/sql-editor.png) ### Permissions [Section titled “Permissions”](#permissions) Select the new table `movies` just created, and click in **Edit Permissions** to set the following permissions for the `public` role and `select` action. ![Permission Rules](/images/quickstarts/react/permissions.png) ### Setup the Application [Section titled “Setup the Application”](#setup-the-application) Create the application using standard tooling: ```bash npm create vite@latest my-vue-app -- --template vue cd my-vue-app ``` ### Install the Nhost package [Section titled “Install the Nhost package”](#install-the-nhost-package) Navigate to the application and install `@nhost/nhost-js`. ```bash npm install @nhost/nhost-js ``` ### Configure the Nhost client and fetch the list of movies [Section titled “Configure the Nhost client and fetch the list of movies”](#configure-the-nhost-client-and-fetch-the-list-of-movies) * ./src/lib/nhost.js ```js import { createClient } from "@nhost/nhost-js"; // Replace and with your project's values export const nhost = createClient({ subdomain: "", region: "", }) ``` * ./src/App.vue ```vue ``` ### The end [Section titled “The end”](#the-end) Run your project with `npm run dev` and navigate to `http://localhost:5173` in your browser. ## Related resources [Section titled “Related resources”](#related-resources) * [Full source code](https://github.com/nhost/nhost/tree/main/examples/quickstarts/vue) on GitHub — a complete example configured for a local Nhost project # Create Your Nhost Project for Next.js > Create and configure a new Nhost project as the backend for your Next.js app, with PostgreSQL, GraphQL, auth, and storage ready to use. Welcome to the **Full-Stack Next.js Development with Nhost** series! In this comprehensive tutorial series, you’ll build a complete React application with Nhost that demonstrates authentication, database operations, and file management. ## About This Tutorial Series [Section titled “About This Tutorial Series”](#about-this-tutorial-series) This tutorial series is divided into **5 parts**, each focusing on a specific aspect of building modern web applications with Nhost and Next.js. By the end of the series, you’ll have built a fully functional application featuring: * **User Authentication** - Complete sign up, sign in, and email verification flow * **Todo Management** - Users can create, update, delete, and mark todos as complete * **File Uploads** - Users can upload and manage files with proper permissions * **Protected Routes** - Secure areas that only authenticated users can access Note This is **Part 1** in the Full-Stack Next.js Development with Nhost series. This tutorial sets up the foundation by creating your Nhost project and understanding the series structure. ## Full-Stack Next.js Development with Nhost [Section titled “Full-Stack Next.js Development with Nhost”](#full-stack-nextjs-development-with-nhost) ### [1. Create Project](/getting-started/tutorials/nextjs/1-introduction) [**Current** - Set up your Nhost project](/getting-started/tutorials/nextjs/1-introduction) ### [2. Protected Routes](/getting-started/tutorials/nextjs/2-protected-routes) [Route protection basics](/getting-started/tutorials/nextjs/2-protected-routes) ### [3. User Authentication](/getting-started/tutorials/nextjs/3-user-authentication) [Complete auth flow](/getting-started/tutorials/nextjs/3-user-authentication) ### [4. GraphQL Operations](/getting-started/tutorials/nextjs/4-graphql-operations) [CRUD operations with GraphQL](/getting-started/tutorials/nextjs/4-graphql-operations) ### [5. File Uploads](/getting-started/tutorials/nextjs/5-file-uploads) [File upload and management](/getting-started/tutorials/nextjs/5-file-uploads) ## What You’ll Learn [Section titled “What You’ll Learn”](#what-youll-learn) Throughout this series, you’ll master: * Setting up and configuring Nhost projects * Implementing secure authentication flows * Building protected routes with Next.js Router * Performing GraphQL queries and mutations * Managing file uploads and storage * Configuring database permissions and security * Building responsive Next.js interfaces ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * Node.js 22+ installed on your machine * Basic knowledge of Next.js and JavaScript * Understanding of modern web development concepts Creating an Nhost project is the first step to building your application with Nhost. Let’s get started by setting up your backend infrastructure. ## Step-by-Step Guide [Section titled “Step-by-Step Guide”](#step-by-step-guide) ### Sign Up or Log in [Section titled “Sign Up or Log in”](#sign-up-or-log-in) If you don’t have an Nhost account, sign up at [Nhost](https://app.nhost.io/). If you already have an account, log in. ![sign up/sign in](/images/tutorials/create-nhost-project/1.png) ### Create a New Project [Section titled “Create a New Project”](#create-a-new-project) Click on the “Create Project” button on your dashboard or follow the onboarding prompts if you’re a new user. ![2](/images/tutorials/create-nhost-project/2.png) ### Take note of your project subdomain and region [Section titled “Take note of your project subdomain and region”](#take-note-of-your-project-subdomain-and-region) Take note of your project subdomain and region. You will need this information to connect your application to the Nhost backend in upcoming tutorials. ![3](/images/tutorials/create-nhost-project/3.png) ## What’s Next? [Section titled “What’s Next?”](#whats-next) With your Nhost project created, you now have access to: * [**PostgreSQL Database**](/products/database) - For storing your application data * [**Authentication Service**](/products/auth) - For managing users and sessions * [**GraphQL API**](/products/graphql) - For querying and mutating data * [**File Storage**](/products/storage) - For uploading and managing files * [**Functions**](/products/functions) - For running serverless functions In the [next tutorial](/getting-started/tutorials/nextjs/2-protected-routes), you’ll start building your Next.js application and learn how to protect routes based on user authentication status. Tip Keep your project subdomain and region handy - you’ll need them throughout the series to connect your Next.js application to the Nhost backend. # Protecting Routes and Content in Next.js > Learn how to protect routes and content in a Next.js application using Nhost authentication. This tutorial part demonstrates how to implement robust route protection in a Next.js application using [Nhost authentication](/products/auth). You’ll build a complete authentication system with a protected `/profile` page that includes server-side rendering, client components, server actions and API routes. It also features cross-tab session synchronization, and automatic redirects. In addition, we will see how to show conditional navigation and content based on authentication status. Note This is **Part 2** in the Full-Stack Next.js Development with Nhost series. This part builds a foundation for authentication-protected routes that you can extend to secure any part of your application. ## Full-Stack Next.js Development with Nhost [Section titled “Full-Stack Next.js Development with Nhost”](#full-stack-nextjs-development-with-nhost) ### [1. Create Project](/getting-started/tutorials/nextjs/1-introduction) [Set up your Nhost project](/getting-started/tutorials/nextjs/1-introduction) ### [2. Protected Routes](/getting-started/tutorials/nextjs/2-protected-routes) [**Current** - Route protection basics](/getting-started/tutorials/nextjs/2-protected-routes) ### [3. User Authentication](/getting-started/tutorials/nextjs/3-user-authentication) [Complete auth flow](/getting-started/tutorials/nextjs/3-user-authentication) ### [4. GraphQL Operations](/getting-started/tutorials/nextjs/4-graphql-operations) [CRUD operations with GraphQL](/getting-started/tutorials/nextjs/4-graphql-operations) ### [5. File Uploads](/getting-started/tutorials/nextjs/5-file-uploads) [File upload and management](/getting-started/tutorials/nextjs/5-file-uploads) ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * An [Nhost project](/getting-started/tutorials/nextjs/1-introduction) set up * Node.js 22+ installed * Basic knowledge of React and Next.js ## Step-by-Step Guide [Section titled “Step-by-Step Guide”](#step-by-step-guide) ### Create a New Next.js App [Section titled “Create a New Next.js App”](#create-a-new-nextjs-app) We’ll start by creating a fresh Next.js application with TypeScript support. Next.js provides server-side rendering, file-based routing, and optimized builds for modern React applications. ```bash npx create-next-app@16 nhost-nextjs-tutorial --typescript --eslint --app --no-tailwind --src-dir --yes cd nhost-nextjs-tutorial npm install ``` ### Install Required Dependencies [Section titled “Install Required Dependencies”](#install-required-dependencies) Install the Nhost JavaScript SDK for authentication and session management. The Nhost SDK handles authentication with built-in Next.js support for server-side rendering. ```bash npm install @nhost/nhost-js ``` ### Environment Configuration [Section titled “Environment Configuration”](#environment-configuration) Configure your Nhost project connection by creating environment variables. This allows the app to connect to your specific Nhost backend from both server and client sides. Create a `.env.local` file in your project root: ```bash NHOST_REGION= NHOST_SUBDOMAIN= ``` Caution Replace `` and `` with the actual values from your Nhost project dashboard. ### Create Server-Side Nhost Helper [Section titled “Create Server-Side Nhost Helper”](#create-server-side-nhost-helper) Create server-side utilities for handling authentication in Next.js server components and proxy. This enables server-side session access and token refresh functionality. src/lib/nhost/server.tsx ```tsx 1 import { createServerClient, type NhostClient } from "@nhost/nhost-js"; 2 import { DEFAULT_SESSION_KEY, type StoredSession } from "@nhost/nhost-js/session"; 3 import { cookies } from "next/headers"; 4 import type { NextRequest, NextResponse } from "next/server"; 5 6 const key = DEFAULT_SESSION_KEY; 7 8 /** 9 * Creates an Nhost client for use in server components. 10 * 11 * We rely on the vanilla createClient method from the Nhost JS SDK and a SessionStorage 12 * customized to be able to retrieve the session from cookies in Next.js server components. 13 */ 14 export async function createNhostClient(): Promise { 15 const cookieStore = await cookies(); 16 17 const nhost = createServerClient({ 18 region: process.env["NHOST_REGION"] || "local", 19 subdomain: process.env["NHOST_SUBDOMAIN"] || "local", 20 storage: { 21 // storage compatible with Next.js server components 22 get: (): StoredSession | null => { 23 const s = cookieStore.get(key)?.value || null; 24 if (!s) { 25 return null; 26 } 27 const session = JSON.parse(s) as StoredSession; 28 return session; 29 }, 30 set: (value: StoredSession) => { 31 cookieStore.set(key, JSON.stringify(value)); 32 }, 33 remove: () => { 34 cookieStore.delete(key); 35 }, 36 }, 37 }); 38 39 return nhost; 40 } 41 42 /** 43 * Proxy function to handle Nhost authentication and session management. 44 * 45 * This function is designed to be used in Next.js proxy to manage user sessions 46 * and refresh tokens. Refreshing the session needs to be done in the proxy 47 * to ensure that the session is always up-to-date and accessible by both server and client components. 48 * 49 * @param {NextRequest} request - The incoming Next.js request object 50 * @param {NextResponse} response - The outgoing Next.js response object 51 */ 52 export async function handleNhostProxy( 53 request: NextRequest, 54 response: NextResponse, 55 ): Promise { 56 const nhost = createServerClient({ 57 region: process.env["NHOST_REGION"] || "local", 58 subdomain: process.env["NHOST_SUBDOMAIN"] || "local", 59 storage: { 60 // storage compatible with Next.js proxy 61 get: (): StoredSession | null => { 62 const raw = request.cookies.get(key)?.value || null; 63 if (!raw) { 64 return null; 65 } 66 const session = JSON.parse(raw) as StoredSession; 67 return session; 68 }, 69 set: (value: StoredSession) => { 70 response.cookies.set({ 71 name: key, 72 value: JSON.stringify(value), 73 path: "/", 74 httpOnly: false, //if set to true we can't access it in the client 75 secure: process.env.NODE_ENV === "production", 76 sameSite: "lax", 77 maxAge: 60 * 60 * 24 * 30, // 30 days in seconds 78 }); 79 }, 80 remove: () => { 81 response.cookies.delete(key); 82 }, 83 }, 84 }); 85 86 // we only want to refresh the session if the token will 87 // expire in the next 60 seconds 88 return await nhost.refreshSession(60); 89 } ``` ### Create Proxy for Route Protection [Section titled “Create Proxy for Route Protection”](#create-proxy-for-route-protection) Create a Next.js proxy to handle route protection at the server level. This proxy runs before any page renders and automatically redirects unauthenticated users from protected routes. src/proxy.ts ```tsx 1 import type { NextRequest } from "next/server"; 2 import { NextResponse } from "next/server"; 3 import { handleNhostProxy } from "./lib/nhost/server"; 4 5 // Define public routes that don't require authentication 6 const publicRoutes = ["/"]; 7 8 export async function proxy(request: NextRequest) { 9 // Create a response that we'll modify as needed 10 const response = NextResponse.next(); 11 12 // Get the current path 13 const path = request.nextUrl.pathname; 14 15 // Check if this is a public route or a public asset 16 const isPublicRoute = publicRoutes.some( 17 (route) => path === route || path.startsWith(`${route}/`), 18 ); 19 20 // Handle Nhost authentication and token refresh 21 // Always call this to ensure session is up-to-date 22 // even for public routes, so that session changes are detected 23 const session = await handleNhostProxy(request, response); 24 25 // If it's a public route, allow access without checking auth 26 if (isPublicRoute) { 27 return response; 28 } 29 30 // If no session and not a public route, redirect to signin 31 if (!session) { 32 const homeUrl = new URL("/", request.url); 33 return NextResponse.redirect(homeUrl); 34 } 35 36 // Session exists, allow access to protected route 37 return response; 38 } 39 40 // Define which routes this proxy should run on 41 export const config = { 42 matcher: [ 43 /* 44 * Match all request paths except: 45 * - _next/static (static files) 46 * - _next/image (image optimization files) 47 * - favicon.ico (favicon file) 48 * - public files (public directory) 49 */ 50 "/((?!_next/static|_next/image|favicon.ico|public).*)", 51 ], 52 }; ``` ### Create the Protected Profile Page [Section titled “Create the Protected Profile Page”](#create-the-protected-profile-page) Create a page that displays user information. Note that the page itself doesn’t need any special authentication logic - the route protection is handled entirely by the proxy from the previous step. src/app/profile/page.tsx ```tsx 1 import { createNhostClient } from "../../lib/nhost/server"; 2 3 export default async function Profile() { 4 // Create the client with async cookie access 5 const nhost = await createNhostClient(); 6 const session = nhost.getUserSession(); 7 8 return ( 9
10
11

Your Profile

12
13 14
15

User Information

16
17
18 Display Name:{" "} 19 {session?.user?.displayName || "Not set"} 20
21
22 Email: {session?.user?.email || "Not available"} 23
24
25 User ID: {session?.user?.id || "Not available"} 26
27
28 Roles: {session?.user?.roles?.join(", ") || "None"} 29
30
31 Email Verified: 32 39 {session?.user?.emailVerified ? "✓ Yes" : "✗ No"} 40 41
42
43
44 45
46

Session Information

47
48
49
            {JSON.stringify(session, null, 2)}
50
          
51
52
53
54 ); 55 } ``` ### Create a Simple Home Page [Section titled “Create a Simple Home Page”](#create-a-simple-home-page) Build a public homepage that adapts its content based on authentication status. This shows users different options depending on whether they’re signed in. src/app/page.tsx ```tsx 1 import { createNhostClient } from "../lib/nhost/server"; 2 3 export default async function Home() { 4 const nhost = await createNhostClient(); 5 const session = nhost.getUserSession(); 6 7 return ( 8
9
10

Welcome to Nhost Next.js Demo

11
12 13 {session ? ( 14
15

Hello, {session.user?.displayName || session.user?.email}!

16
17 ) : ( 18
19

You are not signed in.

20
21 )} 22
23 ); 24 } ``` ### Create the Navigation Component [Section titled “Create the Navigation Component”](#create-the-navigation-component) Create a server-side navigation component that provides consistent navigation across all pages. This component adapts its links based on authentication status - showing different options for signed-in and signed-out users with server-side rendering. src/components/Navigation.tsx ```tsx 1 import Link from "next/link"; 2 import { createNhostClient } from "../lib/nhost/server"; 3 4 export default async function Navigation() { 5 const nhost = await createNhostClient(); 6 const session = nhost.getUserSession(); 7 8 return ( 9 34 ); 35 } ``` ### Update the Root Layout [Section titled “Update the Root Layout”](#update-the-root-layout) Configure the Next.js root layout to include the navigation and page content. src/app/layout.tsx ```tsx 1 import type { Metadata } from "next"; 2 import "./globals.css"; 3 import Navigation from "../components/Navigation"; 4 5 export const metadata: Metadata = { 6 title: "Nhost Next.js Tutorial", 7 description: "Next.js tutorial with Nhost authentication", 8 }; 9 10 export default function RootLayout({ 11 children, 12 }: { 13 children: React.ReactNode; 14 }) { 15 return ( 16 17 18 19
{children}
20 21 22 ); 23 } ``` ### Add Application Styles [Section titled “Add Application Styles”](#add-application-styles) Replace the contents of the file `src/app/global.css` with the following styles to provide a clean and modern look for the application. This file will be used across the rest of series. src/app/globals.css ```css 1 :root { 2 font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; 3 line-height: 1.5; 4 font-weight: 400; 5 6 color-scheme: dark; 7 color: rgba(255, 255, 255, 0.87); 8 background-color: #242424; 9 10 font-synthesis: none; 11 text-rendering: optimizeLegibility; 12 -webkit-font-smoothing: antialiased; 13 -moz-osx-font-smoothing: grayscale; 14 } 15 16 body { 17 margin: 0; 18 min-width: 320px; 19 min-height: 100vh; 20 } 21 22 #root { 23 width: 100%; 24 min-height: 100vh; 25 display: block; 26 margin: 0; 27 padding: 0; 28 } 29 30 a { 31 font-weight: 500; 32 color: #646cff; 33 text-decoration: inherit; 34 } 35 36 a:hover { 37 color: #535bf2; 38 } 39 40 h1 { 41 font-size: 3.2em; 42 line-height: 1.1; 43 } 44 45 button { 46 border-radius: 8px; 47 border: 1px solid transparent; 48 padding: 0.6em 1.2em; 49 font-size: 1em; 50 font-weight: 500; 51 font-family: inherit; 52 background-color: #1a1a1a; 53 cursor: pointer; 54 transition: border-color 0.25s; 55 } 56 57 button:hover { 58 border-color: #646cff; 59 } 60 61 button:focus, 62 button:focus-visible { 63 outline: 4px auto -webkit-focus-ring-color; 64 } 65 66 input, 67 textarea { 68 width: 100%; 69 padding: 0.875rem 1rem; 70 border: 1px solid rgba(255, 255, 255, 0.2); 71 border-radius: 8px; 72 font-size: 0.875rem; 73 transition: all 0.2s ease; 74 background: rgba(255, 255, 255, 0.05); 75 color: white; 76 box-sizing: border-box; 77 font-family: inherit; 78 } 79 80 input:focus, 81 textarea:focus { 82 outline: none; 83 border-color: #3b82f6; 84 box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); 85 background: rgba(255, 255, 255, 0.08); 86 } 87 88 input::placeholder, 89 textarea::placeholder { 90 color: rgba(255, 255, 255, 0.5); 91 } 92 93 textarea { 94 resize: vertical; 95 min-height: 4rem; 96 } 97 98 label { 99 display: block; 100 margin: 0 0 0.5rem 0; 101 font-weight: 500; 102 color: rgba(255, 255, 255, 0.9); 103 font-size: 0.875rem; 104 } 105 106 @keyframes spin { 107 0% { 108 transform: rotate(0deg); 109 } 110 100% { 111 transform: rotate(360deg); 112 } 113 } 114 115 /* Global Layout */ 116 .app-content { 117 padding: 0 2rem 2rem; 118 max-width: 800px; 119 margin: 0 auto; 120 } 121 122 .page-center { 123 text-align: center; 124 padding: 2rem; 125 } 126 127 .page-header { 128 margin-bottom: 2rem; 129 } 130 131 .page-title { 132 font-weight: 700; 133 margin: 0; 134 display: flex; 135 align-items: center; 136 gap: 1rem; 137 } 138 139 .margin-bottom { 140 margin-bottom: 1rem; 141 } 142 143 .margin-top { 144 margin-top: 1rem; 145 } 146 147 .container { 148 width: 800px; 149 max-width: calc(100vw - 4rem); 150 min-width: 320px; 151 margin: 0 auto; 152 padding: 2rem; 153 box-sizing: border-box; 154 position: relative; 155 } 156 157 /* Status Messages */ 158 .success-message { 159 padding: 1rem; 160 background-color: #d4edda; 161 color: #155724; 162 border-radius: 8px; 163 margin-bottom: 1rem; 164 } 165 166 .error-message { 167 background: rgba(239, 68, 68, 0.1); 168 color: #ef4444; 169 border: 1px solid rgba(239, 68, 68, 0.3); 170 border-radius: 12px; 171 padding: 1rem 1.5rem; 172 margin: 1rem 0; 173 } 174 175 .help-text { 176 color: #666; 177 } 178 179 .verification-status { 180 color: #28a745; 181 font-size: 1.2rem; 182 font-weight: bold; 183 margin-bottom: 1rem; 184 } 185 186 .verification-status.error { 187 color: #dc3545; 188 font-size: 1.1rem; 189 } 190 191 /* Email Verification Status */ 192 .email-verified { 193 color: #10b981; 194 font-weight: bold; 195 margin-left: 0.5rem; 196 } 197 198 .email-unverified { 199 color: #ef4444; 200 font-weight: bold; 201 margin-left: 0.5rem; 202 } 203 204 /* Debug Info */ 205 .debug-panel { 206 margin-bottom: 1rem; 207 padding: 1rem; 208 background-color: #f8f9fa; 209 border-radius: 8px; 210 text-align: left; 211 max-height: 200px; 212 overflow: auto; 213 } 214 215 .debug-title { 216 font-weight: bold; 217 margin-bottom: 0.5rem; 218 } 219 220 .debug-item { 221 margin-bottom: 0.25rem; 222 } 223 224 .debug-key { 225 font-family: monospace; 226 color: #007bff; 227 } 228 229 .debug-value { 230 font-family: monospace; 231 } 232 233 /* Session Display */ 234 .session-display { 235 font-size: 0.75rem; 236 overflow: auto; 237 margin: 0; 238 line-height: 1.4; 239 white-space: pre-wrap; 240 word-break: break-word; 241 } 242 243 /* Loading Spinner */ 244 .spinner-verify { 245 width: 32px; 246 height: 32px; 247 border: 3px solid #f3f3f3; 248 border-top: 3px solid #007bff; 249 border-radius: 50%; 250 animation: spin 1s linear infinite; 251 margin: 0 auto; 252 } 253 254 /* Navigation */ 255 .navigation { 256 background: rgba(255, 255, 255, 0.05); 257 backdrop-filter: blur(10px); 258 border-bottom: 1px solid rgba(255, 255, 255, 0.1); 259 position: sticky; 260 top: 0; 261 z-index: 100; 262 margin-bottom: 2rem; 263 } 264 265 .nav-container { 266 max-width: 800px; 267 margin: 0 auto; 268 padding: 1rem 2rem; 269 display: flex; 270 justify-content: space-between; 271 align-items: center; 272 } 273 274 .nav-logo { 275 font-size: 1.25rem; 276 font-weight: 700; 277 color: white; 278 text-decoration: none; 279 background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); 280 -webkit-background-clip: text; 281 -webkit-text-fill-color: transparent; 282 background-clip: text; 283 } 284 285 .nav-logo:hover { 286 opacity: 0.8; 287 } 288 289 .nav-links { 290 display: flex; 291 align-items: center; 292 gap: 1.5rem; 293 } 294 295 .nav-link { 296 color: rgba(255, 255, 255, 0.8); 297 text-decoration: none; 298 font-weight: 500; 299 font-size: 0.875rem; 300 padding: 0.5rem 0.75rem; 301 border-radius: 6px; 302 transition: all 0.2s ease; 303 border: none; 304 background: none; 305 cursor: pointer; 306 font-family: inherit; 307 } 308 309 .nav-link:hover { 310 color: white; 311 background: rgba(255, 255, 255, 0.1); 312 } 313 314 .nav-button { 315 color: #ef4444; 316 } 317 318 .nav-button:hover { 319 background: rgba(239, 68, 68, 0.2); 320 } 321 322 /* Buttons */ 323 .btn { 324 padding: 0.75rem 1.5rem; 325 border: none; 326 border-radius: 8px; 327 font-weight: 500; 328 cursor: pointer; 329 transition: all 0.2s ease; 330 font-size: 0.875rem; 331 display: inline-flex; 332 align-items: center; 333 justify-content: center; 334 gap: 0.5rem; 335 min-width: 120px; 336 } 337 338 .btn-primary { 339 background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); 340 color: white; 341 } 342 343 .btn-primary:hover { 344 transform: translateY(-1px); 345 box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4); 346 } 347 348 .btn-secondary { 349 background: rgba(255, 255, 255, 0.1); 350 color: white; 351 border: 1px solid rgba(255, 255, 255, 0.2); 352 } 353 354 .btn-secondary:hover { 355 background: rgba(255, 255, 255, 0.2); 356 } 357 358 .btn-cancel { 359 background: rgba(239, 68, 68, 0.1); 360 color: #ef4444; 361 border: 1px solid rgba(239, 68, 68, 0.3); 362 } 363 364 .btn-cancel:hover { 365 background: rgba(239, 68, 68, 0.2); 366 } 367 368 /* Loading State */ 369 .loading-container { 370 display: flex; 371 justify-content: center; 372 align-items: center; 373 padding: 4rem 2rem; 374 } 375 376 .loading-content { 377 display: flex; 378 align-items: center; 379 gap: 1rem; 380 } 381 382 .spinner { 383 width: 2rem; 384 height: 2rem; 385 border: 3px solid rgba(59, 130, 246, 0.3); 386 border-top: 3px solid #3b82f6; 387 border-radius: 50%; 388 animation: spin 1s linear infinite; 389 } 390 391 .loading-text { 392 color: rgba(255, 255, 255, 0.7); 393 font-size: 0.875rem; 394 } 395 396 /* Empty State */ 397 .empty-state { 398 text-align: center; 399 padding: 4rem 2rem; 400 background: rgba(255, 255, 255, 0.02); 401 border: 1px solid rgba(255, 255, 255, 0.1); 402 border-radius: 16px; 403 } 404 405 .empty-icon { 406 width: 4rem; 407 height: 4rem; 408 color: rgba(255, 255, 255, 0.4); 409 margin: 0 auto 1rem; 410 } 411 412 .empty-title { 413 font-size: 1.25rem; 414 font-weight: 600; 415 color: white; 416 margin: 0 0 0.5rem 0; 417 } 418 419 .empty-description { 420 color: rgba(255, 255, 255, 0.6); 421 margin: 0; 422 } 423 424 /* Forms */ 425 .form-card { 426 background: rgba(255, 255, 255, 0.05); 427 backdrop-filter: blur(10px); 428 border: 1px solid rgba(255, 255, 255, 0.1); 429 border-radius: 16px; 430 padding: 2rem; 431 margin-bottom: 2rem; 432 width: 100%; 433 box-sizing: border-box; 434 } 435 436 .form-title { 437 font-size: 1.5rem; 438 font-weight: 600; 439 color: white; 440 margin: 0 0 1.5rem 0; 441 } 442 443 .form-fields { 444 display: flex; 445 flex-direction: column; 446 gap: 1.5rem; 447 } 448 449 .field-group { 450 display: flex; 451 flex-direction: column; 452 } 453 454 .form-actions { 455 display: flex; 456 gap: 1rem; 457 margin-top: 1rem; 458 } 459 460 /* Auth Pages */ 461 .auth-form { 462 max-width: 400px; 463 } 464 465 .auth-form-field { 466 margin-bottom: 1rem; 467 } 468 469 .auth-input { 470 width: 100%; 471 padding: 0.5rem; 472 margin-top: 0.25rem; 473 } 474 475 .auth-error { 476 color: red; 477 margin-bottom: 1rem; 478 padding: 0.5rem; 479 background-color: #fee; 480 border-radius: 4px; 481 } 482 483 .auth-button { 484 width: 100%; 485 padding: 0.75rem; 486 color: white; 487 border: none; 488 border-radius: 4px; 489 cursor: pointer; 490 } 491 492 .auth-button:disabled { 493 cursor: not-allowed; 494 } 495 496 .auth-button.primary { 497 background-color: #28a745; 498 } 499 500 .auth-button.secondary { 501 background-color: #007bff; 502 } 503 504 .auth-links { 505 margin-top: 1rem; 506 } 507 508 /* Todos */ 509 510 .todo-form { 511 width: 100%; 512 } 513 514 /* Todo List */ 515 .todos-list { 516 display: flex; 517 flex-direction: column; 518 gap: 1rem; 519 } 520 521 .todo-card { 522 background: rgba(255, 255, 255, 0.03); 523 backdrop-filter: blur(10px); 524 border: 1px solid rgba(255, 255, 255, 0.1); 525 border-radius: 12px; 526 transition: all 0.2s ease; 527 overflow: hidden; 528 width: 100%; 529 box-sizing: border-box; 530 } 531 532 .todo-card:hover { 533 border-color: rgba(255, 255, 255, 0.2); 534 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); 535 } 536 537 .todo-card.completed { 538 opacity: 0.7; 539 } 540 541 /* Todo Content */ 542 .todo-content { 543 padding: 1rem 1.5rem; 544 } 545 546 .todo-edit { 547 padding: 1.5rem; 548 min-height: 200px; 549 } 550 551 .edit-fields { 552 display: flex; 553 flex-direction: column; 554 gap: 1rem; 555 } 556 557 .edit-actions { 558 display: flex; 559 gap: 1rem; 560 margin-top: 1.5rem; 561 } 562 563 .todo-header { 564 display: flex; 565 justify-content: space-between; 566 align-items: center; 567 gap: 1rem; 568 } 569 570 .todo-title-btn { 571 background: none; 572 border: none; 573 padding: 0; 574 text-align: left; 575 font-size: 1.25rem; 576 font-weight: 600; 577 color: white; 578 cursor: pointer; 579 transition: color 0.2s ease; 580 flex: 1; 581 line-height: 1.4; 582 word-wrap: break-word; 583 overflow-wrap: break-word; 584 max-width: calc(100% - 140px); 585 } 586 587 .todo-title-btn:hover { 588 color: #3b82f6; 589 } 590 591 .todo-title-btn.completed { 592 text-decoration: line-through; 593 color: rgba(255, 255, 255, 0.5); 594 } 595 596 .todo-actions { 597 display: flex; 598 gap: 0.5rem; 599 flex-shrink: 0; 600 min-width: 132px; 601 justify-content: flex-end; 602 } 603 604 /* Action Buttons */ 605 .action-btn { 606 width: 40px; 607 height: 40px; 608 border: none; 609 border-radius: 6px; 610 cursor: pointer; 611 background: rgba(255, 255, 255, 0.05); 612 display: flex; 613 align-items: center; 614 justify-content: center; 615 font-size: 18px; 616 transition: all 0.2s ease; 617 -webkit-text-fill-color: currentColor; 618 } 619 620 .action-btn-complete { 621 color: #10b981; 622 font-size: 20px; 623 } 624 625 .action-btn-complete:hover { 626 background: rgba(16, 185, 129, 0.2); 627 color: #34d399; 628 } 629 630 .action-btn-edit { 631 color: #3b82f6; 632 } 633 634 .action-btn-edit:hover { 635 background: rgba(59, 130, 246, 0.2); 636 color: #60a5fa; 637 } 638 639 .action-btn-delete { 640 color: #ef4444; 641 } 642 643 .action-btn-delete:hover { 644 background: rgba(239, 68, 68, 0.2); 645 color: #f87171; 646 } 647 648 /* Add Todo Button */ 649 .add-todo-btn { 650 width: 36px; 651 height: 36px; 652 border: none; 653 border-radius: 6px; 654 cursor: pointer; 655 background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); 656 display: flex; 657 align-items: center; 658 justify-content: center; 659 color: white; 660 font-size: 18px; 661 font-weight: normal; 662 -webkit-text-fill-color: white; 663 transition: all 0.2s ease; 664 } 665 666 .add-todo-btn:hover { 667 transform: scale(1.1); 668 box-shadow: 0 4px 20px rgba(59, 130, 246, 0.4); 669 } 670 671 /* Todo Details */ 672 .todo-details { 673 margin-top: 1rem; 674 padding-top: 1rem; 675 border-top: 1px solid rgba(255, 255, 255, 0.1); 676 } 677 678 .description { 679 background: rgba(255, 255, 255, 0.02); 680 border: 1px solid rgba(255, 255, 255, 0.1); 681 border-radius: 8px; 682 padding: 1rem; 683 margin-bottom: 1rem; 684 } 685 686 .description p { 687 margin: 0; 688 color: rgba(255, 255, 255, 0.8); 689 line-height: 1.6; 690 } 691 692 .description.completed p { 693 text-decoration: line-through; 694 color: rgba(255, 255, 255, 0.5); 695 } 696 697 .todo-meta { 698 display: flex; 699 justify-content: space-between; 700 align-items: center; 701 gap: 1rem; 702 } 703 704 .meta-dates { 705 display: flex; 706 gap: 1rem; 707 flex-wrap: wrap; 708 } 709 710 .meta-item { 711 font-size: 0.75rem; 712 color: rgba(255, 255, 255, 0.5); 713 } 714 715 .completion-badge { 716 display: flex; 717 align-items: center; 718 gap: 0.25rem; 719 font-size: 0.75rem; 720 color: #10b981; 721 font-weight: 500; 722 } 723 724 .completion-icon { 725 width: 0.875rem; 726 height: 0.875rem; 727 } 728 729 /* Responsive Design */ 730 @media (max-width: 768px) { 731 .nav-container { 732 padding: 1rem; 733 flex-direction: column; 734 gap: 1rem; 735 } 736 737 .nav-links { 738 gap: 1rem; 739 flex-wrap: wrap; 740 justify-content: center; 741 } 742 743 .container { 744 padding: 1rem; 745 } 746 747 .form-actions { 748 flex-direction: column; 749 } 750 751 .edit-actions { 752 flex-direction: column; 753 } 754 755 .todo-header { 756 flex-direction: column; 757 align-items: flex-start; 758 gap: 1rem; 759 } 760 761 .todo-actions { 762 align-self: stretch; 763 justify-content: center; 764 } 765 766 .meta-dates { 767 flex-direction: column; 768 gap: 0.25rem; 769 } 770 771 .todo-meta { 772 flex-direction: column; 773 align-items: flex-start; 774 gap: 0.5rem; 775 } 776 } 777 778 /* File Upload */ 779 .file-upload-btn { 780 min-height: 120px; 781 flex-direction: column; 782 gap: 0.5rem; 783 width: 100%; 784 border: 2px dashed rgba(255, 255, 255, 0.3); 785 display: flex; 786 align-items: center; 787 justify-content: center; 788 cursor: pointer; 789 transition: all 0.2s ease; 790 } 791 792 .file-upload-btn:hover { 793 border-color: rgba(255, 255, 255, 0.5); 794 background-color: rgba(255, 255, 255, 0.05); 795 } 796 797 .file-upload-info { 798 margin-top: 0.5rem; 799 font-size: 0.875rem; 800 color: rgba(255, 255, 255, 0.8); 801 } 802 803 /* File Table */ 804 .file-table { 805 width: 100%; 806 border-collapse: collapse; 807 } 808 809 .file-table th { 810 padding: 0.75rem; 811 text-align: left; 812 border-bottom: 1px solid rgba(255, 255, 255, 0.1); 813 color: rgba(255, 255, 255, 0.7); 814 font-weight: 500; 815 font-size: 0.875rem; 816 } 817 818 .file-table th:last-child { 819 text-align: center; 820 } 821 822 .file-table td { 823 padding: 0.75rem; 824 border-bottom: 1px solid rgba(255, 255, 255, 0.05); 825 } 826 827 .file-table tr:hover { 828 background-color: rgba(255, 255, 255, 0.02); 829 } 830 831 .file-name { 832 color: white; 833 font-weight: 500; 834 } 835 836 .file-meta { 837 color: rgba(255, 255, 255, 0.6); 838 font-size: 0.875rem; 839 } 840 841 .file-actions { 842 display: flex; 843 gap: 0.5rem; 844 justify-content: center; 845 } 846 847 /* Responsive File Table */ 848 @media (max-width: 768px) { 849 .file-table { 850 font-size: 0.875rem; 851 } 852 853 .file-table th, 854 .file-table td { 855 padding: 0.5rem; 856 } 857 858 .file-actions { 859 flex-direction: column; 860 gap: 0.25rem; 861 } 862 } ``` ### Run and test the Application [Section titled “Run and test the Application”](#run-and-test-the-application) Start the development server to test your route protection implementation: ```bash npm run dev ``` Things to try out: 1. Try navigating to `/profile` - you should be redirected to the homepage `/` since you’re not authenticated. 2. Because you are not signed in, the navigation bar should only show the “Home” link and the placeholder for signin/signup links. 3. On the homepage, you should see a message indicating that you are not signed in. After we complete the next tutorial on user authentication, you will be able to sign in and access the protected `/profile` page and see how the navigation bar and homepage updates accordingly. ## How It Works [Section titled “How It Works”](#how-it-works) 1. **Server-Side Helpers**: Utilities for handling authentication in Next.js server components and proxy 2. **Proxy Route Protection**: Next.js proxy runs before any page renders, automatically redirecting unauthenticated users from protected routes and refreshing tokens 3. **Protected Pages**: Server components can assume authentication since the proxy handles protection, focusing purely on rendering authenticated content 4. **Navigation**: Server-side navigation component that adapts its links based on authentication status 5. **Automatic Redirects**: All route protection and redirects are handled at the proxy level for optimal performance and security ## Key Features Demonstrated [Section titled “Key Features Demonstrated”](#key-features-demonstrated) Proxy Route Protection Routes are protected at the proxy level, running before any page renders to provide the highest level of security and performance by preventing unauthorized access before components load. No Loading States Server-side authentication checks eliminate the need for loading spinners during authentication verification, providing instant feedback to users. Automatic Server Redirects Users are automatically redirected based on their authentication status using Next.js proxy redirects, ensuring proper navigation flow before any client-side rendering. Cross-tab Synchronization Authentication state is synchronized across multiple browser tabs using Nhost’s session storage events with Next.js router refresh for server-side state updates. Cookie-Based Session Management Complete user session and profile information is shared between server and client using cookie-based storage, enabling seamless SSR and client-side functionality. Server-Side Rendering Authentication-dependent content is rendered server-side for improved SEO, performance, and security compared to client-only approaches. Automatic Token Refresh The proxy automatically refreshes authentication tokens before they expire, ensuring seamless user experience without manual token management. # User Authentication in Next.js > Add sign up, sign in, and email verification to your Next.js App Router app using Nhost Auth and session-aware server components. This tutorial part builds upon the [Protected Routes part](/getting-started/tutorials/nextjs/2-protected-routes) by adding complete email/password authentication with email verification functionality. You’ll implement sign up, sign in, email verification, and sign out features to create a full authentication flow. Note This is **Part 3** in the Full-Stack Next.js Development with Nhost series. This part creates a production-ready authentication system with secure email verification and proper error handling using Next.js App Router. ## Full-Stack Next.js Development with Nhost [Section titled “Full-Stack Next.js Development with Nhost”](#full-stack-nextjs-development-with-nhost) ### [1. Create Project](/getting-started/tutorials/nextjs/1-introduction) [Set up your Nhost project](/getting-started/tutorials/nextjs/1-introduction) ### [2. Protected Routes](/getting-started/tutorials/nextjs/2-protected-routes) [Route protection basics](/getting-started/tutorials/nextjs/2-protected-routes) ### [3. User Authentication](/getting-started/tutorials/nextjs/3-user-authentication) [**Current** - Complete auth flow](/getting-started/tutorials/nextjs/3-user-authentication) ### [4. GraphQL Operations](/getting-started/tutorials/nextjs/4-graphql-operations) [CRUD operations with GraphQL](/getting-started/tutorials/nextjs/4-graphql-operations) ### [5. File Uploads](/getting-started/tutorials/nextjs/5-file-uploads) [File upload and management](/getting-started/tutorials/nextjs/5-file-uploads) ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * Complete the [Protected Routes part](/getting-started/tutorials/nextjs/2-protected-routes) first * The project from the previous part set up and running ## Step-by-Step Guide [Section titled “Step-by-Step Guide”](#step-by-step-guide) ### Create the Sign In Flow [Section titled “Create the Sign In Flow”](#create-the-sign-in-flow) In this step, we’ll create a complete sign-in flow using Next.js App Router patterns. We’ll build three key files: a server component for the main page that handles URL parameters, a client component for the interactive form, and server actions for secure authentication processing. * Page Component The main sign-in page is a **server component** that handles URL parameters (like error messages) and renders the sign-in form. This component runs on the server and can access search parameters directly. src/app/signin/page.tsx ```tsx 1 import Link from "next/link"; 2 import SignInForm from "./SignInForm"; 3 4 export default async function SignIn({ 5 searchParams, 6 }: { 7 searchParams: Promise<{ error?: string }>; 8 }) { 9 // Extract error from URL parameters 10 const params = await searchParams; 11 const error = params?.error; 12 13 return ( 14
15

Sign In

16 17 18
19

20 Don't have an account? Sign Up 21

22
23
24 ); 25 } ``` * Form Component The sign-in form is a **client component** that handles user interactions, loading states, and form submissions. It communicates with server actions and provides real-time feedback to users. src/app/signin/SignInForm.tsx ```tsx 1 "use client"; 2 3 import { useRouter } from "next/navigation"; 4 import { useId, useState } from "react"; 5 import { signIn } from "./actions"; 6 7 interface SignInFormProps { 8 initialError?: string; 9 } 10 11 export default function SignInForm({ initialError }: SignInFormProps) { 12 const [error, setError] = useState(initialError); 13 const [isLoading, setIsLoading] = useState(false); 14 const router = useRouter(); 15 16 const emailId = useId(); 17 const passwordId = useId(); 18 19 const handleSubmit = async (formData: FormData) => { 20 setIsLoading(true); 21 setError(undefined); 22 23 try { 24 const result = await signIn(formData); 25 26 if (result.redirect) { 27 router.push(result.redirect); 28 } else if (result.error) { 29 setError(result.error); 30 } 31 } catch (err: unknown) { 32 setError( 33 err instanceof Error ? err.message : "An error occurred during sign in", 34 ); 35 } finally { 36 setIsLoading(false); 37 } 38 }; 39 40 return ( 41
42
43 44 51
52 53
54 55 62
63 64 {error &&
{error}
} 65 66 73
74 ); 75 } ``` * Server Actions Server actions handle the authentication logic securely on the server side. They validate form data, communicate with Nhost, and return appropriate responses for success or error states. src/app/signin/actions.ts ```tsx 1 "use server"; 2 3 import type { ErrorResponse } from "@nhost/nhost-js/auth"; 4 import type { FetchError } from "@nhost/nhost-js/fetch"; 5 import { createNhostClient } from "../../lib/nhost/server"; 6 7 export async function signIn(formData: FormData) { 8 const email = formData.get("email") as string; 9 const password = formData.get("password") as string; 10 11 if (!email || !password) { 12 return { 13 error: "Email and password are required", 14 }; 15 } 16 17 try { 18 const nhost = await createNhostClient(); 19 20 const response = await nhost.auth.signInEmailPassword({ 21 email, 22 password, 23 }); 24 25 if (response.body?.session) { 26 return { redirect: "/profile" }; 27 } else { 28 return { 29 error: "Failed to sign in. Please check your credentials.", 30 }; 31 } 32 } catch (err) { 33 const error = err as FetchError; 34 return { 35 error: `An error occurred during sign in: ${error.message}`, 36 }; 37 } 38 } ``` ### Create the Sign Up Flow [Section titled “Create the Sign Up Flow”](#create-the-sign-up-flow) In this step, we’ll build the user registration system with email verification support. The sign-up flow includes handling both the registration form and the email verification success state, all using Next.js server and client components. * Page Component The sign-up page is a **server component** that manages different states: showing the registration form or displaying the email verification success message. It handles URL parameters to determine which state to render. src/app/signup/page.tsx ```tsx 1 import Link from "next/link"; 2 import SignUpForm from "./SignUpForm"; 3 4 export default async function SignUp({ 5 searchParams, 6 }: { 7 searchParams: Promise<{ 8 error?: string; 9 verify?: string; 10 email?: string; 11 }>; 12 }) { 13 // Extract parameters from URL 14 const params = await searchParams; 15 const error = params?.error; 16 const verificationSent = params?.verify === "success"; 17 const email = params?.email; 18 19 if (verificationSent) { 20 return ( 21
22

Check Your Email

23
24

25 We've sent a verification link to {email} 26

27

28 Please check your email and click the verification link to activate 29 your account. 30

31
32

33 Back to Sign In 34

35
36 ); 37 } 38 39 return ( 40
41

Sign Up

42 43 44
45

46 Already have an account? Sign In 47

48
49
50 ); 51 } ``` * Form Component The registration form is a **client component** that collects user information (display name, email, password) and handles form validation, loading states, and error feedback during the sign-up process. src/app/signup/SignUpForm.tsx ```tsx 1 "use client"; 2 3 import { generatePKCEPair } from "@nhost/nhost-js/auth"; 4 import { useRouter } from "next/navigation"; 5 import { useId, useState } from "react"; 6 import { signUp } from "./actions"; 7 8 interface SignUpFormProps { 9 initialError?: string; 10 } 11 12 export default function SignUpForm({ initialError }: SignUpFormProps) { 13 const [error, setError] = useState(initialError); 14 const [isLoading, setIsLoading] = useState(false); 15 const router = useRouter(); 16 17 const displayNameId = useId(); 18 const emailId = useId(); 19 const passwordId = useId(); 20 21 const handleSubmit = async (formData: FormData) => { 22 setIsLoading(true); 23 setError(undefined); 24 25 try { 26 // Generate PKCE pair and store verifier for email verification 27 const { verifier, challenge } = await generatePKCEPair(); 28 document.cookie = `nhost_pkce_verifier=${verifier}; path=/; SameSite=Lax; max-age=3600`; 29 30 // Pass the challenge to the server action 31 formData.set("codeChallenge", challenge); 32 33 const result = await signUp(formData); 34 35 if (result.redirect) { 36 router.push(result.redirect); 37 } else if (result.error) { 38 setError(result.error); 39 } 40 } catch (err: unknown) { 41 setError( 42 err instanceof Error ? err.message : "An error occurred during sign up", 43 ); 44 } finally { 45 setIsLoading(false); 46 } 47 }; 48 49 return ( 50
51
52 53 60
61 62
63 64 71
72 73
74 75 83 Minimum 8 characters 84
85 86 {error &&
{error}
} 87 88 95
96 ); 97 } ``` * Server Actions Server actions handle user registration with Nhost, including setting up email verification. They process form data, create user accounts, and coordinate the email verification flow by setting the appropriate redirect URLs. src/app/signup/actions.ts ```tsx 1 "use server"; 2 3 import type { ErrorResponse } from "@nhost/nhost-js/auth"; 4 import type { FetchError } from "@nhost/nhost-js/fetch"; 5 import { createNhostClient } from "../../lib/nhost/server"; 6 7 export async function signUp(formData: FormData) { 8 const email = formData.get("email") as string; 9 const password = formData.get("password") as string; 10 const displayName = formData.get("displayName") as string; 11 const codeChallenge = formData.get("codeChallenge") as string; 12 13 if (!email || !password || !displayName) { 14 return { 15 error: "All fields are required", 16 }; 17 } 18 19 try { 20 const nhost = await createNhostClient(); 21 22 const response = await nhost.auth.signUpEmailPassword({ 23 email, 24 password, 25 options: { 26 displayName, 27 redirectTo: `${process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"}/verify`, 28 }, 29 codeChallenge, 30 }); 31 32 if (response.body?.session) { 33 // Successfully signed up and automatically signed in 34 return { redirect: "/profile" }; 35 } else { 36 // Verification email sent 37 return { 38 redirect: `/signup?verify=success&email=${encodeURIComponent(email)}`, 39 }; 40 } 41 } catch (err) { 42 const error = err as FetchError; 43 return { 44 error: `An error occurred during sign up: ${error.message}`, 45 }; 46 } 47 } ``` ### Create the Email Verification System [Section titled “Create the Email Verification System”](#create-the-email-verification-system) In this step, we’ll implement email verification using PKCE. When users click the verification link in their email, a Route Handler extracts the authorization code and exchanges it for a session using the PKCE verifier stored in a cookie during sign-up. On success it redirects to the profile page; on failure it redirects to an error page. * Verify Route Handler The verification endpoint is a **Route Handler** that exchanges the authorization code for a session using PKCE. It reads the code verifier from a cookie (set during sign-up), performs the token exchange server-side, and redirects to the profile page on success. We use a Route Handler instead of a Server Component because cookies can only be modified in a Server Action or Route Handler. src/app/verify/route.ts ```ts 1 import { cookies } from "next/headers"; 2 import { type NextRequest, NextResponse } from "next/server"; 3 import { createNhostClient } from "../../lib/nhost/server"; 4 5 const PKCE_VERIFIER_KEY = "nhost_pkce_verifier"; 6 7 export async function GET(request: NextRequest) { 8 const code = request.nextUrl.searchParams.get("code"); 9 const errorUrl = new URL("/verify/error", request.url); 10 11 if (!code) { 12 errorUrl.searchParams.set("message", "No authorization code found in URL"); 13 return NextResponse.redirect(errorUrl); 14 } 15 16 const cookieStore = await cookies(); 17 const codeVerifier = cookieStore.get(PKCE_VERIFIER_KEY)?.value; 18 19 if (!codeVerifier) { 20 errorUrl.searchParams.set( 21 "message", 22 "No PKCE verifier found. The sign-in must be initiated from the same browser.", 23 ); 24 return NextResponse.redirect(errorUrl); 25 } 26 27 // Remove the PKCE verifier cookie 28 cookieStore.delete(PKCE_VERIFIER_KEY); 29 30 try { 31 const nhost = await createNhostClient(); 32 await nhost.auth.tokenExchange({ code, codeVerifier }); 33 } catch (err) { 34 errorUrl.searchParams.set( 35 "message", 36 `Verification failed: ${(err as Error).message}`, 37 ); 38 return NextResponse.redirect(errorUrl); 39 } 40 41 return NextResponse.redirect(new URL("/profile", request.url)); 42 } ``` * Error Page The verification error page is a **server component** that displays helpful error messages and debugging information when email verification fails. It shows the specific error message and any URL parameters that might help diagnose the issue. src/app/verify/error/page.tsx ```tsx 1 import Link from "next/link"; 2 3 export default async function VerifyError({ 4 searchParams, 5 }: { 6 searchParams: Promise>; 7 }) { 8 const params = await searchParams; 9 const message = params?.message || "Unknown verification error"; 10 11 // Filter out the message to show other URL parameters 12 const urlParams = Object.entries(params).filter(([key]) => key !== "message"); 13 14 return ( 15
16

Email Verification

17 18
19

Verification failed

20

{message}

21 22 {urlParams.length > 0 && ( 23
24

URL Parameters:

25 {urlParams.map(([key, value]) => ( 26
27 {key}:{" "} 28 {value} 29
30 ))} 31
32 )} 33 34 35 Back to Sign In 36 37
38
39 ); 40 } ``` Caution **Important Configuration Required:** Before testing email verification, you must configure your Nhost project’s authentication settings: 1. Go to your Nhost project dashboard 2. Navigate to **Auth → Settings → Authentication** 3. Add your local development URL (e.g., `http://localhost:3000`) to the **Allowed Redirect URLs** field 4. Ensure your production domain is also added when deploying Without this configuration, you’ll receive a `redirectTo not allowed` error when users attempt to sign up or verify their email addresses. ### Create the Sign Out System [Section titled “Create the Sign Out System”](#create-the-sign-out-system) In this step, we’ll implement user sign-out functionality using Next.js patterns. We’ll create a client component for the sign-out button and a server action to handle the actual sign-out process securely on the server side. * Sign Out Button The sign-out button is a **client component** that provides an interactive button for users to sign out. It handles the user interaction and calls the server action, then manages navigation and component refresh after sign-out. src/components/SignOutButton.tsx ```tsx 1 "use client"; 2 3 import { useRouter } from "next/navigation"; 4 import { signOut } from "../lib/nhost/actions"; 5 6 export default function SignOutButton() { 7 const router = useRouter(); 8 9 const handleSignOut = async () => { 10 try { 11 await signOut(); 12 router.push("/"); 13 router.refresh(); // Refresh to update server components 14 } catch (err) { 15 console.error("Error signing out:", err); 16 } 17 }; 18 19 return ( 20 27 ); 28 } ``` * Server Action The sign-out server action handles the authentication logic securely on the server side. It retrieves the current session, calls Nhost’s sign-out method with the refresh token, and redirects the user to the home page after successful sign-out. src/lib/nhost/actions.ts ```tsx 1 "use server"; 2 3 import { redirect } from "next/navigation"; 4 import { createNhostClient } from "./server"; 5 6 export async function signOut() { 7 try { 8 const nhost = await createNhostClient(); 9 const session = nhost.getUserSession(); 10 11 if (session) { 12 await nhost.auth.signOut({ 13 refreshToken: session.refreshToken, 14 }); 15 } 16 } catch (err) { 17 console.error("Error signing out:", err); 18 throw err; 19 } 20 21 redirect("/"); 22 } ``` ### Update Navigation Component [Section titled “Update Navigation Component”](#update-navigation-component) In this step, we’ll update the server-side navigation component that shows different links based on the user’s authentication state. The navigation will display “Sign In” and “Sign Up” links for unauthenticated users, and “Profile” and “Sign Out” for authenticated users. src/components/Navigation.tsx ```tsx 1 import Link from "next/link"; 2 import { createNhostClient } from "../lib/nhost/server"; 3 import SignOutButton from "./SignOutButton"; 4 5 export default async function Navigation() { 6 const nhost = await createNhostClient(); 7 const session = nhost.getUserSession(); 8 9 return ( 10 41 ); 42 } ``` ### Update Public Routes in Proxy [Section titled “Update Public Routes in Proxy”](#update-public-routes-in-proxy) In this step, we’ll configure the proxy to allow access to authentication-related routes without requiring authentication. This ensures that users can access sign-in, sign-up, and email verification pages even when not logged in. src/proxy.ts ```tsx 1 import type { NextRequest } from "next/server"; 2 import { NextResponse } from "next/server"; 3 import { handleNhostProxy } from "./lib/nhost/server"; 4 5 // Define public routes that don't require authentication 6 const publicRoutes = ["/", "/signin", "/signup", "/verify", "/verify/error"]; 7 8 export async function proxy(request: NextRequest) { 9 // Create a response that we'll modify as needed 10 const response = NextResponse.next(); 11 12 // Get the current path 13 const path = request.nextUrl.pathname; 14 15 // Check if this is a public route or a public asset 16 const isPublicRoute = publicRoutes.some( 17 (route) => path === route || path.startsWith(`${route}/`), 18 ); 19 20 // Handle Nhost authentication and token refresh 21 // Always call this to ensure session is up-to-date 22 // even for public routes, so that session changes are detected 23 const session = await handleNhostProxy(request, response); 24 25 // If it's a public route, allow access without checking auth 26 if (isPublicRoute) { 27 return response; 28 } 29 30 // If no session and not a public route, redirect to signin 31 if (!session) { 32 const homeUrl = new URL("/", request.url); 33 return NextResponse.redirect(homeUrl); 34 } 35 36 // Session exists, allow access to protected route 37 return response; 38 } 39 40 // Define which routes this proxy should run on 41 export const config = { 42 matcher: [ 43 /* 44 * Match all request paths except: 45 * - _next/static (static files) 46 * - _next/image (image optimization files) 47 * - favicon.ico (favicon file) 48 * - public files (public directory) 49 */ 50 "/((?!_next/static|_next/image|favicon.ico|public).*)", 51 ], 52 }; ``` ### Run and Test the Application [Section titled “Run and Test the Application”](#run-and-test-the-application) Start your Next.js development server and test the complete authentication flow to ensure everything works properly. ```bash npm run dev ``` Things to try out: 1. **Email Verification Flow**: Try signing up with a new email address. Check your email for the verification link and click it. The verification page will exchange the authorization code for a session using PKCE and redirect you to your profile. 2. **Sign In/Out Flow**: Try signing out and then signing back in with the same credentials using the server actions. 3. **Server-Side Navigation**: Notice how navigation links change based on authentication state - the navigation component is rendered server-side and shows different content based on the session. 4. **Route Protection**: Try accessing protected routes while logged out to see the proxy-based protection in action. 5. **Cross-Tab Consistency**: Open multiple tabs and test signing out from one tab. Unlike client-side React apps, you’ll need to refresh or navigate to see changes in other tabs due to server-side rendering. ## Key Features Demonstrated [Section titled “Key Features Demonstrated”](#key-features-demonstrated) Server Components & Actions Full authentication flow using Next.js App Router with server components and server actions for secure, server-side processing. PKCE Verification Server-side `/verify` page that exchanges authorization codes for sessions using PKCE, with the verifier stored in a cookie and the token exchange performed entirely on the server. Client/Server Separation Clear separation between server components for rendering and client components for interactivity, following Next.js best practices. Error Handling Comprehensive error handling with URL-based error states and dedicated error pages for different failure scenarios. Session Management Server-side session handling with sign out functionality using server actions and proper state management. # GraphQL Operations in Next.js > Learn how to perform GraphQL operations and manage database permissions while building a complete todos application with Nhost and Next.js This part builds upon the previous parts by demonstrating how to perform GraphQL operations with proper [database permissions](/products/graphql/permissions) using Next.js App Router patterns. You’ll learn how to design database tables, configure user permissions, and implement complete CRUD operations through GraphQL queries and mutations using server components, client components, and server actions. Note This is **Part 4** in the Full-Stack Next.js Development with Nhost series. This part focuses on GraphQL operations, database management, and permission-based data access control using Next.js App Router with server/client component separation. ## Full-Stack Next.js Development with Nhost [Section titled “Full-Stack Next.js Development with Nhost”](#full-stack-nextjs-development-with-nhost) ### [1. Create Project](/getting-started/tutorials/nextjs/1-introduction) [Set up your Nhost project](/getting-started/tutorials/nextjs/1-introduction) ### [2. Protected Routes](/getting-started/tutorials/nextjs/2-protected-routes) [Route protection basics](/getting-started/tutorials/nextjs/2-protected-routes) ### [3. User Authentication](/getting-started/tutorials/nextjs/3-user-authentication) [Complete auth flow](/getting-started/tutorials/nextjs/3-user-authentication) ### [4. GraphQL Operations](/getting-started/tutorials/nextjs/4-graphql-operations) [**Current** - CRUD operations with GraphQL](/getting-started/tutorials/nextjs/4-graphql-operations) ### [5. File Uploads](/getting-started/tutorials/nextjs/5-file-uploads) [File upload and management](/getting-started/tutorials/nextjs/5-file-uploads) ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * Complete the [User Authentication part](/getting-started/tutorials/nextjs/3-user-authentication) first * The project from the previous part set up and running ## What You’ll Build [Section titled “What You’ll Build”](#what-youll-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 * **Next.js components** using server/client patterns that interact with GraphQL endpoint * **Server actions** for secure data mutations * **Server components** for efficient data fetching ## Step-by-Step Guide [Section titled “Step-by-Step Guide”](#step-by-step-guide) ### Create the To-Dos Table [Section titled “Create the To-Dos Table”](#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: 1. Navigate to **Database** 2. Click on **SQL Console** Enter the following SQL: * SQL ```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(); ``` * UI ![Database SQL Editor](/images/tutorials/todos/1.png) Caution Please make sure to enable **Track this** so that the new table todos is available through the auto-generated APIs ### Set Up Permissions [Section titled “Set Up Permissions”](#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 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. ![Insert Permissions Configuration](/images/tutorials/todos/2.png) * Select For selecting (reading) todos, we are allowing to read all columns but only for rows where the `user_id` matches the authenticated user’s ID. This ensures that users can only see their own todos. ![Select Permissions Configuration](/images/tutorials/todos/3.png) * Update When updating todos, we are allowing users to modify the `title`, `details`, and `completed` columns but only for rows where the `user_id` matches their own ID. This prevents users from modifying todos that do not belong to them. ![Update Permissions Configuration](/images/tutorials/todos/4.png) * Delete For deleting todos, we are allowing users to delete rows only where the `user_id` matches their own ID. This ensures that users cannot delete todos that belong to other users. ![Delete Permissions Configuration](/images/tutorials/todos/5.png) ### Create the Todos Page System [Section titled “Create the Todos Page System”](#create-the-todos-page-system) Now let’s implement the Next.js page system that uses the database we just configured. We’ll create a server component for the main page, a client component for the interactive todos interface, and server actions for secure data mutations. * Page Component The main todos page is a **server component** that fetches initial data server-side and renders the todos interface. This component runs on the server and provides the initial state to the client component. src/app/todos/page.tsx ```tsx 1 import { createNhostClient } from "../../lib/nhost/server"; 2 import TodosClient from "./TodosClient"; 3 4 // The interfaces below define the structure of our data 5 // They are not strictly necessary but help with type safety 6 7 // Represents a single todo item 8 export interface Todo { 9 id: string; 10 title: string; 11 details: string | null; 12 completed: boolean; 13 created_at: string; 14 updated_at: string; 15 user_id: string; 16 } 17 18 // This matches the GraphQL response structure for fetching todos 19 // Can be used as a generic type on the request method 20 interface GetTodos { 21 todos: Todo[]; 22 } 23 24 export default async function TodosPage() { 25 // Fetch initial todos data server-side 26 const nhost = await createNhostClient(); 27 const session = nhost.getUserSession(); 28 29 let initialTodos: Todo[] = []; 30 let error: string | null = null; 31 32 if (session) { 33 try { 34 // Make GraphQL request to fetch todos using Nhost server client 35 // The query automatically filters by user_id due to permissions 36 const response = await nhost.graphql.request({ 37 query: ` 38 query GetTodos { 39 todos(order_by: { created_at: desc }) { 40 id 41 title 42 details 43 completed 44 created_at 45 updated_at 46 user_id 47 } 48 } 49 `, 50 }); 51 52 // Check for GraphQL errors in the response body 53 if (response.body.errors) { 54 error = response.body.errors[0]?.message || "Failed to fetch todos"; 55 } else { 56 // Extract todos from the GraphQL response data 57 initialTodos = response.body?.data?.todos || []; 58 } 59 } catch (err) { 60 error = err instanceof Error ? err.message : "Failed to fetch todos"; 61 } 62 } 63 64 return ; 65 } ``` * Client Component The todos client component is a **client component** that handles all interactive functionality including form submissions, state management, and user interactions. It receives initial data from the server component and manages the client-side state. src/app/todos/TodosClient.tsx ```tsx 1 "use client"; 2 3 import { useId, useState } from "react"; 4 import { addTodo, deleteTodo, updateTodo } from "./actions"; 5 import type { Todo } from "./page"; 6 7 interface TodosClientProps { 8 initialTodos: Todo[]; 9 initialError: string | null; 10 } 11 12 export default function TodosClient({ 13 initialTodos, 14 initialError, 15 }: TodosClientProps) { 16 const [todos, setTodos] = useState(initialTodos); 17 const [error, setError] = useState(initialError); 18 const [newTodoTitle, setNewTodoTitle] = useState(""); 19 const [newTodoDetails, setNewTodoDetails] = useState(""); 20 const [editingTodo, setEditingTodo] = useState(null); 21 const [showAddForm, setShowAddForm] = useState(false); 22 const [expandedTodos, setExpandedTodos] = useState>(new Set()); 23 const [isLoading, setIsLoading] = useState(false); 24 25 const titleId = useId(); 26 const detailsId = useId(); 27 28 const handleAddTodo = async (e: React.FormEvent) => { 29 e.preventDefault(); 30 if (!newTodoTitle.trim()) return; 31 32 setIsLoading(true); 33 try { 34 // Call server action to add todo 35 const result = await addTodo({ 36 title: newTodoTitle.trim(), 37 details: newTodoDetails.trim() || null, 38 }); 39 40 if (result.success && result.todo) { 41 setTodos([result.todo, ...todos]); 42 setNewTodoTitle(""); 43 setNewTodoDetails(""); 44 setShowAddForm(false); 45 setError(null); 46 } else { 47 setError(result.error || "Failed to add todo"); 48 } 49 } catch (err) { 50 setError(err instanceof Error ? err.message : "Failed to add todo"); 51 } finally { 52 setIsLoading(false); 53 } 54 }; 55 56 const handleUpdateTodo = async ( 57 id: string, 58 updates: Partial>, 59 ) => { 60 try { 61 // Call server action to update todo 62 const result = await updateTodo(id, updates); 63 64 if (result.success && result.todo) { 65 setTodos( 66 todos.map((todo) => (todo.id === id ? (result.todo ?? todo) : todo)), 67 ); 68 setEditingTodo(null); 69 setError(null); 70 } else { 71 setError(result.error || "Failed to update todo"); 72 } 73 } catch (err) { 74 setError(err instanceof Error ? err.message : "Failed to update todo"); 75 } 76 }; 77 78 const handleDeleteTodo = async (id: string) => { 79 if (!confirm("Are you sure you want to delete this todo?")) return; 80 81 try { 82 // Call server action to delete todo 83 const result = await deleteTodo(id); 84 85 if (result.success) { 86 setTodos(todos.filter((todo) => todo.id !== id)); 87 setError(null); 88 } else { 89 setError(result.error || "Failed to delete todo"); 90 } 91 } catch (err) { 92 setError(err instanceof Error ? err.message : "Failed to delete todo"); 93 } 94 }; 95 96 const toggleComplete = async (todo: Todo) => { 97 await handleUpdateTodo(todo.id, { completed: !todo.completed }); 98 }; 99 100 const saveEdit = async () => { 101 if (!editingTodo) return; 102 await handleUpdateTodo(editingTodo.id, { 103 title: editingTodo.title, 104 details: editingTodo.details, 105 }); 106 }; 107 108 const toggleTodoExpansion = (todoId: string) => { 109 const newExpanded = new Set(expandedTodos); 110 if (newExpanded.has(todoId)) { 111 newExpanded.delete(todoId); 112 } else { 113 newExpanded.add(todoId); 114 } 115 setExpandedTodos(newExpanded); 116 }; 117 118 return ( 119
120
121

122 My Todos 123 {!showAddForm && ( 124 132 )} 133

134
135 136 {error && ( 137
138 Error: {error} 139
140 )} 141 142 {showAddForm && ( 143
144
145

Add New Todo

146
147
148 149 setNewTodoTitle(e.target.value)} 154 placeholder="What needs to be done?" 155 required 156 disabled={isLoading} 157 /> 158
159
160 161 297
298
299 302 313
314
315
316
317 {/if} 318 319 {#if !showAddForm} 320 {#if loading} 321
322
323
324 Loading todos... 325
326
327 {:else} 328
329 {#if todos.length === 0} 330
331 345

No todos yet

346

347 Create your first todo to get started! 348

349
350 {:else} 351 {#each todos as todo (todo.id)} 352
353 {#if editingTodo?.id === todo.id} 354
355
356
357 358 363
364
365 366 371
372
373 380 387
388
389
390 {:else} 391
392
393 400
401 411 419 427
428
429 430 {#if expandedTodos.has(todo.id)} 431
432 {#if todo.details} 433
434

{todo.details}

435
436 {/if} 437 438
439
440 441 Created: {new Date(todo.created_at).toLocaleString()} 442 443 444 Updated: {new Date(todo.updated_at).toLocaleString()} 445 446
447 {#if todo.completed} 448
449 463 Completed 464
465 {/if} 466
467
468 {/if} 469
470 {/if} 471
472 {/each} 473 {/if} 474
475 {/if} 476 {/if} 477
478 {/if} ``` ### Update Navigation Links [Section titled “Update Navigation Links”](#update-navigation-links) Add a link to the todos page in the navigation layout. Update your `src/routes/+layout.svelte` file to include the todos link: src/routes/+layout.svelte ```svelte 1 29 30
31 58 59
60 {#if children} 61 {@render children()} 62 {/if} 63
64
``` ### Test Your Complete Application [Section titled “Test Your Complete Application”](#test-your-complete-application) Run your SvelteKit application and test all the functionality: ```bash npm run dev ``` Things to try out: 1. Try signing in and out and see how the Todos page is only available when authenticated 2. Create, view, edit, complete, and delete todos. See how the UI updates accordingly 3. 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 [Section titled “Key Features Implemented”](#key-features-implemented) Database Schema Properly designed todos table with constraints, indexes, and automatic timestamp updates for optimal performance. GraphQL API Auto-generated GraphQL API with queries and mutations for full CRUD operations on todos. Row-Level Security Comprehensive permissions ensuring users can only access their own todos through all GraphQL operations. CRUD Operations Complete Create, Read, Update, Delete functionality with proper error handling and user feedback. Rich Interface Expandable todo items, inline editing, completion status, and detailed timestamps using SvelteKit’s reactive UI patterns. # File Uploads in SvelteKit > Add file uploads to your SvelteKit app with Nhost Storage: configure buckets and permissions and build a file management system. This part builds upon the previous GraphQL operations part by demonstrating how to implement [file upload functionality](/products/storage) with proper storage permissions. You’ll learn how to create [storage buckets](/products/storage/buckets), configure upload permissions, and implement complete file management operations in a SvelteKit application. Note This is **Part 5** in the Full-Stack SvelteKit Development with Nhost series. This part focuses on file storage, upload operations, and permission-based file access control in a production application. ## Full-Stack SvelteKit Development with Nhost [Section titled “Full-Stack SvelteKit Development with Nhost”](#full-stack-sveltekit-development-with-nhost) ### [1. Create Project](/getting-started/tutorials/svelte/1-introduction) [Set up your Nhost project](/getting-started/tutorials/svelte/1-introduction) ### [2. Protected Routes](/getting-started/tutorials/svelte/2-protected-routes) [Route protection basics](/getting-started/tutorials/svelte/2-protected-routes) ### [3. User Authentication](/getting-started/tutorials/svelte/3-user-authentication) [Complete auth flow](/getting-started/tutorials/svelte/3-user-authentication) ### [4. GraphQL Operations](/getting-started/tutorials/svelte/4-graphql-operations) [CRUD operations with GraphQL](/getting-started/tutorials/svelte/4-graphql-operations) ### [5. File Uploads](/getting-started/tutorials/svelte/5-file-uploads) [**Current** - File upload and management](/getting-started/tutorials/svelte/5-file-uploads) ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * Complete the [GraphQL Operations part](/getting-started/tutorials/svelte/4-graphql-operations) first * The project from the previous part set up and running ## What You’ll Build [Section titled “What You’ll Build”](#what-youll-build) By the end of this part, you’ll have: * A **personal bucket** so users can upload their own private files * **File upload functionality** * **File management interface** for viewing and deleting files * **Security permissions** ensuring users can only access their own files ## Step-by-Step Guide [Section titled “Step-by-Step Guide”](#step-by-step-guide) ### Create a Personal Storage Bucket [Section titled “Create a Personal Storage Bucket”](#create-a-personal-storage-bucket) First, we’ll create a storage bucket where users can upload their personal files. In your Nhost project dashboard: 1. Navigate to **Storage** 2. Click **New Bucket** in the sidebar 3. Set the name to `personal` and click **Save** ![Create bucket](/images/tutorials/uploads/1.png) ### Configure Storage Permissions [Section titled “Configure Storage Permissions”](#configure-storage-permissions) Now we need to set up permissions for the storage bucket to ensure the `user` role can only upload, view, and delete their own files. In the dashboard, navigate to **Storage** and click **Permissions** to open the permission grid. * Upload Click the **Upload** cell for the `user` role. 1. Select **With custom check** and add a rule: `bucket_id` equals `personal` 2. Enable the **Uploader identity** toggle — this automatically sets `uploaded_by_user_id` to the current user’s ID on every upload, so you can reference it in Download and Delete rules. ![upload files permissions](/images/tutorials/uploads/2.png) * Download Click the **Download** cell for the `user` role. 1. Select **With custom check** and add two rules combined with AND: * `bucket_id` equals `personal` * `uploaded_by_user_id` equals `X-Hasura-User-Id` ![download files permissions](/images/tutorials/uploads/3.png) * Delete Click the **Delete** cell for the `user` role. 1. Select **With custom check** and add the same two rules as Download: * `bucket_id` equals `personal` * `uploaded_by_user_id` equals `X-Hasura-User-Id` ![delete files permissions](/images/tutorials/uploads/4.png) Note You can read more about storage permissions [here](/products/storage/permissions) ### Create the File Upload Component [Section titled “Create the File Upload Component”](#create-the-file-upload-component) Now let’s implement the SvelteKit page component for file upload functionality. src/routes/files/+page.svelte ```svelte 1 244 245 {#if !$auth.session} 246
247

Please sign in to access file uploads.

248
249 {:else} 250
251 254 255
256

Upload a File

257 258
259 267 295
296 297 {#if error} 298
{error}
299 {/if} 300 301 {#if uploadResult} 302
File uploaded successfully!
303 {/if} 304 305 314
315 316
317

Your Files

318 319 {#if deleteStatus} 320
321 {deleteStatus.message} 322
323 {/if} 324 325 {#if isFetching} 326
327
328
329 Loading files... 330
331
332 {:else if files.length === 0} 333
334 348

No files yet

349

Upload your first file to get started!

350
351 {:else} 352
353 354 355 356 357 358 359 360 361 362 363 {#each files as file (file.id)} 364 365 366 367 368 395 396 {/each} 397 398
NameTypeSizeActions
{file.name}{file.mimeType}{formatFileSize(file.size || 0)} 369
370 384 393
394
399
400 {/if} 401
402
403 {/if} ``` ### Update Navigation Links [Section titled “Update Navigation Links”](#update-navigation-links) Add a link to the files page in the navigation layout. Update your `src/routes/+layout.svelte` file to include the files link: src/routes/+layout.svelte ```svelte 1 29 30
31 59 60
61 {#if children} 62 {@render children()} 63 {/if} 64
65
``` ### Test Your File Upload System [Section titled “Test Your File Upload System”](#test-your-file-upload-system) Run your SvelteKit application and test all the functionality: ```bash npm run dev ``` Things to try out: 1. Try signing in and out and see how the file upload page is only accessible when signed in. 2. Upload different types of files (images, documents, etc.) 3. View and delete files 4. Sign in with another account and verify you cannot see files from the first account ## Key Features Implemented [Section titled “Key Features Implemented”](#key-features-implemented) Storage Bucket Dedicated personal storage bucket with proper configuration for user file isolation. File Upload Interface User-friendly upload interface with file selection, preview, and progress feedback using SvelteKit’s reactive patterns. File Management Complete file listing with metadata, viewing capabilities, and deletion functionality. File Type Handling Intelligent handling of different file types with appropriate viewing/download behavior. Error Handling Comprehensive error handling with user-friendly messages for upload and management operations using Svelte stores. # Create Your Nhost Project for Vue > Create and configure a new Nhost project as the backend for your Vue app, with PostgreSQL, GraphQL, auth, and storage ready to use. Welcome to the **Full-Stack Vue Development with Nhost** series! In this comprehensive tutorial series, you’ll build a complete Vue application with Nhost that demonstrates authentication, database operations, and file management. ## About This Tutorial Series [Section titled “About This Tutorial Series”](#about-this-tutorial-series) This tutorial series is divided into **5 parts**, each focusing on a specific aspect of building modern web applications with Nhost and Vue. By the end of the series, you’ll have built a fully functional application featuring: * **User Authentication** - Complete sign up, sign in, and email verification flow * **Todo Management** - Users can create, update, delete, and mark todos as complete * **File Uploads** - Users can upload and manage files with proper permissions * **Protected Routes** - Secure areas that only authenticated users can access Note This is **Part 1** in the Full-Stack Vue Development with Nhost series. This part sets up the foundation by creating your Nhost project and understanding the series structure. ## Full-Stack Vue Development with Nhost [Section titled “Full-Stack Vue Development with Nhost”](#full-stack-vue-development-with-nhost) ### [1. Create Project](/getting-started/tutorials/vue/1-introduction) [**Current** - Set up your Nhost project](/getting-started/tutorials/vue/1-introduction) ### [2. Protected Routes](/getting-started/tutorials/vue/2-protected-routes) [Route protection basics](/getting-started/tutorials/vue/2-protected-routes) ### [3. User Authentication](/getting-started/tutorials/vue/3-user-authentication) [Complete auth flow](/getting-started/tutorials/vue/3-user-authentication) ### [4. GraphQL Operations](/getting-started/tutorials/vue/4-graphql-operations) [CRUD operations with GraphQL](/getting-started/tutorials/vue/4-graphql-operations) ### [5. File Uploads](/getting-started/tutorials/vue/5-file-uploads) [File upload and management](/getting-started/tutorials/vue/5-file-uploads) ## What You’ll Learn [Section titled “What You’ll Learn”](#what-youll-learn) Throughout this series, you’ll master: * Setting up and configuring Nhost projects * Implementing secure authentication flows * Building protected routes with Vue Router * Performing GraphQL queries and mutations * Managing file uploads and storage * Configuring database permissions and security * Building responsive Vue interfaces ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * Node.js 22+ installed on your machine * Basic knowledge of Vue and JavaScript * Understanding of modern web development concepts Creating an Nhost project is the first step to building your application with Nhost. Let’s get started by setting up your backend infrastructure. ## Step-by-Step Guide [Section titled “Step-by-Step Guide”](#step-by-step-guide) ### Sign Up or Log in [Section titled “Sign Up or Log in”](#sign-up-or-log-in) If you don’t have an Nhost account, sign up at [Nhost](https://app.nhost.io/). If you already have an account, log in. ![sign up/sign in](/images/tutorials/create-nhost-project/1.png) ### Create a New Project [Section titled “Create a New Project”](#create-a-new-project) Click on the “Create Project” button on your dashboard or follow the onboarding prompts if you’re a new user. ![2](/images/tutorials/create-nhost-project/2.png) ### Take note of your project subdomain and region [Section titled “Take note of your project subdomain and region”](#take-note-of-your-project-subdomain-and-region) Take note of your project subdomain and region. You will need this information to connect your application to the Nhost backend in upcoming tutorials. ![3](/images/tutorials/create-nhost-project/3.png) ## What’s Next? [Section titled “What’s Next?”](#whats-next) With your Nhost project created, you now have access to: * [**PostgreSQL Database**](/products/database) - For storing your application data * [**Authentication Service**](/products/auth) - For managing users and sessions * [**GraphQL API**](/products/graphql) - For querying and mutating data * [**File Storage**](/products/storage) - For uploading and managing files * [**Functions**](/products/functions) - For running serverless functions In the [next tutorial](/getting-started/tutorials/vue/2-protected-routes), you’ll start building your Vue application and learn how to protect routes based on user authentication status. Tip Keep your project subdomain and region handy - you’ll need them throughout the series to connect your Vue application to the Nhost backend. # Protecting Routes and Content in Vue > Learn how to protect routes and content in a Vue application using Nhost authentication. This tutorial part demonstrates how to implement robust route protection in a Vue application using [Nhost authentication](/products/auth). You’ll build a complete authentication system with a protected `/profile` page that includes cross-tab session synchronization and automatic redirects. In addition, we will see how to show conditional navigation and content based on authentication status. Note This is **Part 2** in the Full-Stack Vue Development with Nhost series. This part builds a foundation for authentication-protected routes that you can extend to secure any part of your application. ## Full-Stack Vue Development with Nhost [Section titled “Full-Stack Vue Development with Nhost”](#full-stack-vue-development-with-nhost) ### [1. Create Project](/getting-started/tutorials/vue/1-introduction) [Set up your Nhost project](/getting-started/tutorials/vue/1-introduction) ### [2. Protected Routes](/getting-started/tutorials/vue/2-protected-routes) [**Current** - Route protection basics](/getting-started/tutorials/vue/2-protected-routes) ### [3. User Authentication](/getting-started/tutorials/vue/3-user-authentication) [Complete auth flow](/getting-started/tutorials/vue/3-user-authentication) ### [4. GraphQL Operations](/getting-started/tutorials/vue/4-graphql-operations) [CRUD operations with GraphQL](/getting-started/tutorials/vue/4-graphql-operations) ### [5. File Uploads](/getting-started/tutorials/vue/5-file-uploads) [File upload and management](/getting-started/tutorials/vue/5-file-uploads) ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * An [Nhost project](/getting-started/tutorials/vue/1-introduction) set up * Node.js 22+ installed * Basic knowledge of Vue and Vue Router ## Step-by-Step Guide [Section titled “Step-by-Step Guide”](#step-by-step-guide) ### Create a New Vue App [Section titled “Create a New Vue App”](#create-a-new-vue-app) We’ll start by creating a fresh Vue application using Vite with TypeScript support. Vite provides fast development server and optimized builds for modern Vue applications. ```bash npm create vue@latest nhost-vue-tutorial -- --typescript --router cd nhost-vue-tutorial npm install ``` ### Install Required Dependencies [Section titled “Install Required Dependencies”](#install-required-dependencies) Install the Nhost JavaScript SDK. The Nhost SDK handles authentication, while Vue Router (already included) enables protected route navigation. ```bash npm install @nhost/nhost-js ``` ### Environment Configuration [Section titled “Environment Configuration”](#environment-configuration) Configure your Nhost project connection by creating environment variables. This allows the app to connect to your specific Nhost backend. Create a `.env` file in your project root: ```bash VITE_NHOST_REGION= VITE_NHOST_SUBDOMAIN= ``` Caution Replace `` and `` with the actual values from your Nhost project dashboard. ### Create the Nhost Auth Composable [Section titled “Create the Nhost Auth Composable”](#create-the-nhost-auth-composable) Build the core authentication composable that manages user sessions across your Vue application. This composable provides reactive authentication state and handles cross-tab synchronization. src/lib/nhost/auth.ts ````typescript 1 import { createClient } from "@nhost/nhost-js"; 2 import type { StoredSession } from "@nhost/nhost-js/session"; 3 import { computed, reactive } from "vue"; 4 5 // Global reactive state 6 const authState = reactive({ 7 user: null as StoredSession["user"] | null, 8 session: null as StoredSession | null, 9 isLoading: true, 10 }); 11 12 // Create the nhost client 13 const nhost = createClient({ 14 region: (import.meta.env["VITE_NHOST_REGION"] as string) || "local", 15 subdomain: (import.meta.env["VITE_NHOST_SUBDOMAIN"] as string) || "local", 16 }); 17 18 // Subscription cleanup function 19 let unsubscribe: (() => void) | null = null; 20 let lastRefreshTokenIdRef: string | null = null; 21 let isInitialized = false; 22 23 /** 24 * Handles session reload when refresh token changes. 25 * This detects when the session has been updated from other tabs. 26 * 27 * @param currentRefreshTokenId - The current refresh token ID to compare against stored value 28 */ 29 const reloadSession = (currentRefreshTokenId: string | null) => { 30 if (currentRefreshTokenId !== lastRefreshTokenIdRef) { 31 lastRefreshTokenIdRef = currentRefreshTokenId; 32 33 // Update local authentication state to match current session 34 const currentSession = nhost.getUserSession(); 35 authState.user = currentSession?.user || null; 36 authState.session = currentSession; 37 } 38 }; 39 40 // Initialize auth state 41 const initializeAuth = () => { 42 if (isInitialized) return; 43 44 authState.isLoading = true; 45 46 // Set initial values 47 const currentSession = nhost.getUserSession(); 48 authState.user = currentSession?.user || null; 49 authState.session = currentSession; 50 lastRefreshTokenIdRef = currentSession?.refreshTokenId ?? null; 51 authState.isLoading = false; 52 53 // Subscribe to session changes from other browser tabs 54 // This enables real-time synchronization when user signs in/out in another tab 55 unsubscribe = nhost.sessionStorage.onChange((currentSession) => { 56 reloadSession(currentSession?.refreshTokenId ?? null); 57 }); 58 59 // Handle session changes from page focus events (for additional session consistency) 60 const checkSessionOnFocus = () => { 61 reloadSession(nhost.getUserSession()?.refreshTokenId ?? null); 62 }; 63 64 // Monitor page visibility changes (tab switching, window minimizing) 65 document.addEventListener("visibilitychange", () => { 66 if (!document.hidden) { 67 checkSessionOnFocus(); 68 } 69 }); 70 71 // Monitor window focus events (clicking back into the browser window) 72 window.addEventListener("focus", checkSessionOnFocus); 73 74 isInitialized = true; 75 }; 76 77 // Cleanup function 78 const cleanup = () => { 79 if (unsubscribe) { 80 unsubscribe(); 81 unsubscribe = null; 82 } 83 isInitialized = false; 84 }; 85 86 /** 87 * Vue composable for authentication state and operations. 88 * 89 * Provides reactive access to current user session, authentication state, and Nhost client. 90 * Handles cross-tab session synchronization and automatic state updates. 91 * 92 * @returns Object containing reactive authentication state and Nhost client 93 * 94 * @example 95 * ```vue 96 * 101 * 102 * 106 * ``` 107 */ 108 export function useAuth() { 109 // Initialize auth if not already done 110 if (!isInitialized && typeof window !== "undefined") { 111 initializeAuth(); 112 } 113 114 return { 115 user: computed(() => authState.user), 116 session: computed(() => authState.session), 117 isLoading: computed(() => authState.isLoading), 118 isAuthenticated: computed(() => !!authState.session), 119 nhost, 120 }; 121 } 122 123 // Initialize auth immediately (for SSR compatibility) 124 if (typeof window !== "undefined") { 125 initializeAuth(); 126 } 127 128 // Cleanup on window unload 129 if (typeof window !== "undefined") { 130 window.addEventListener("beforeunload", cleanup); 131 } ```` ### Create the Profile Page [Section titled “Create the Profile Page”](#create-the-profile-page) Create a page that displays user information. Note that the page itself doesn’t need any special authentication logic - the route protection is handled entirely by the Vue Router navigation guard we set up in the previous step. src/views/ProfileView\.vue ```vue 1 46 47 ``` ### Create a Simple Home Page [Section titled “Create a Simple Home Page”](#create-a-simple-home-page) Build a public homepage that adapts its content based on authentication status. This shows users different options depending on whether they’re signed in. src/views/HomeView\.vue ```vue 1 15 16 ``` ### Create the Navigation Component [Section titled “Create the Navigation Component”](#create-the-navigation-component) Create a reusable navigation component that provides consistent navigation across all pages. This component adapts its links based on authentication status - showing different options for signed-in and signed-out users. src/components/Navigation.vue ```vue 1 25 26 ``` ### Configure Protected Routes with Vue Router [Section titled “Configure Protected Routes with Vue Router”](#configure-protected-routes-with-vue-router) Set up Vue Router to handle route protection using navigation guards. This approach provides centralized route protection and automatic redirects for unauthenticated users. src/router/index.ts ```typescript 1 import { createRouter, createWebHistory } from "vue-router"; 2 import { useAuth } from "../lib/nhost/auth"; 3 import HomeView from "../views/HomeView.vue"; 4 import ProfileView from "../views/ProfileView.vue"; 5 6 const router = createRouter({ 7 history: createWebHistory(import.meta.env.BASE_URL), 8 routes: [ 9 { 10 path: "/", 11 name: "home", 12 component: HomeView, 13 }, 14 { 15 path: "/profile", 16 name: "profile", 17 component: ProfileView, 18 meta: { requiresAuth: true }, 19 }, 20 { 21 path: "/:pathMatch(.*)*", 22 redirect: "/", 23 }, 24 ], 25 }); 26 27 // Navigation guard for protected routes 28 router.beforeEach((to) => { 29 if (to.meta["requiresAuth"]) { 30 const { isAuthenticated, isLoading } = useAuth(); 31 32 // Show loading state while authentication is being checked 33 if (isLoading.value) { 34 // You can return a loading component path or handle loading in the component 35 return true; // Allow navigation, handle loading in component 36 } 37 38 if (!isAuthenticated.value) { 39 return "/"; // Redirect to home page 40 } 41 } 42 return true; 43 }); 44 45 export default router; ``` ### Update the Main App Component [Section titled “Update the Main App Component”](#update-the-main-app-component) Configure the application’s main component to include the navigation bar and router view. Since we’ve configured route protection in the router itself, the App component mainly needs to provide the layout structure. src/App.vue ```vue 1 9 10 ``` ### Add Application Styles [Section titled “Add Application Styles”](#add-application-styles) Replace the contents of the file `src/assets/main.css` with the following styles to provide a clean and modern look for the application. This file will be used across the rest of series. src/assets/main.css ```css 1 :root { 2 font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; 3 line-height: 1.5; 4 font-weight: 400; 5 6 color-scheme: dark; 7 color: rgba(255, 255, 255, 0.87); 8 background-color: #242424; 9 10 font-synthesis: none; 11 text-rendering: optimizeLegibility; 12 -webkit-font-smoothing: antialiased; 13 -moz-osx-font-smoothing: grayscale; 14 } 15 16 body { 17 margin: 0; 18 display: flex; 19 place-items: center; 20 min-width: 320px; 21 min-height: 100vh; 22 } 23 24 #app { 25 width: 100%; 26 min-height: 100vh; 27 display: block; 28 margin: 0; 29 padding: 0; 30 } 31 32 a { 33 font-weight: 500; 34 color: #646cff; 35 text-decoration: inherit; 36 } 37 38 a:hover { 39 color: #535bf2; 40 } 41 42 h1 { 43 font-size: 3.2em; 44 line-height: 1.1; 45 } 46 47 button { 48 border-radius: 8px; 49 border: 1px solid transparent; 50 padding: 0.6em 1.2em; 51 font-size: 1em; 52 font-weight: 500; 53 font-family: inherit; 54 background-color: #1a1a1a; 55 cursor: pointer; 56 transition: border-color 0.25s; 57 } 58 59 button:hover { 60 border-color: #646cff; 61 } 62 63 button:focus, 64 button:focus-visible { 65 outline: 4px auto -webkit-focus-ring-color; 66 } 67 68 input, 69 textarea { 70 width: 100%; 71 padding: 0.875rem 1rem; 72 border: 1px solid rgba(255, 255, 255, 0.2); 73 border-radius: 8px; 74 font-size: 0.875rem; 75 transition: all 0.2s ease; 76 background: rgba(255, 255, 255, 0.05); 77 color: white; 78 box-sizing: border-box; 79 font-family: inherit; 80 } 81 82 input:focus, 83 textarea:focus { 84 outline: none; 85 border-color: #3b82f6; 86 box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); 87 background: rgba(255, 255, 255, 0.08); 88 } 89 90 input::placeholder, 91 textarea::placeholder { 92 color: rgba(255, 255, 255, 0.5); 93 } 94 95 textarea { 96 resize: vertical; 97 min-height: 4rem; 98 } 99 100 label { 101 display: block; 102 margin: 0 0 0.5rem 0; 103 font-weight: 500; 104 color: rgba(255, 255, 255, 0.9); 105 font-size: 0.875rem; 106 } 107 108 @keyframes spin { 109 0% { 110 transform: rotate(0deg); 111 } 112 100% { 113 transform: rotate(360deg); 114 } 115 } 116 117 /* Global Layout */ 118 .app-content { 119 padding: 0 2rem 2rem; 120 max-width: 800px; 121 margin: 0 auto; 122 } 123 124 .page-center { 125 text-align: center; 126 padding: 2rem; 127 } 128 129 .page-header { 130 margin-bottom: 2rem; 131 } 132 133 .page-title { 134 font-weight: 700; 135 margin: 0; 136 display: flex; 137 align-items: center; 138 gap: 1rem; 139 } 140 141 .margin-bottom { 142 margin-bottom: 1rem; 143 } 144 145 .margin-top { 146 margin-top: 1rem; 147 } 148 149 .container { 150 width: 800px; 151 max-width: calc(100vw - 4rem); 152 min-width: 320px; 153 margin: 0 auto; 154 padding: 2rem; 155 box-sizing: border-box; 156 position: relative; 157 } 158 159 /* Status Messages */ 160 .success-message { 161 padding: 1rem; 162 background-color: #d4edda; 163 color: #155724; 164 border-radius: 8px; 165 margin-bottom: 1rem; 166 } 167 168 .error-message { 169 background: rgba(239, 68, 68, 0.1); 170 color: #ef4444; 171 border: 1px solid rgba(239, 68, 68, 0.3); 172 border-radius: 12px; 173 padding: 1rem 1.5rem; 174 margin: 1rem 0; 175 } 176 177 .help-text { 178 color: #666; 179 } 180 181 .verification-status { 182 color: #28a745; 183 font-size: 1.2rem; 184 font-weight: bold; 185 margin-bottom: 1rem; 186 } 187 188 .verification-status.error { 189 color: #dc3545; 190 font-size: 1.1rem; 191 } 192 193 /* Email Verification Status */ 194 .email-verified { 195 color: #10b981; 196 font-weight: bold; 197 margin-left: 0.5rem; 198 } 199 200 .email-unverified { 201 color: #ef4444; 202 font-weight: bold; 203 margin-left: 0.5rem; 204 } 205 206 /* Debug Info */ 207 .debug-panel { 208 margin-bottom: 1rem; 209 padding: 1rem; 210 background-color: #f8f9fa; 211 border-radius: 8px; 212 text-align: left; 213 max-height: 200px; 214 overflow: auto; 215 } 216 217 .debug-title { 218 font-weight: bold; 219 margin-bottom: 0.5rem; 220 } 221 222 .debug-item { 223 margin-bottom: 0.25rem; 224 } 225 226 .debug-key { 227 font-family: monospace; 228 color: #007bff; 229 } 230 231 .debug-value { 232 font-family: monospace; 233 } 234 235 /* Session Display */ 236 .session-display { 237 font-size: 0.75rem; 238 overflow: auto; 239 margin: 0; 240 line-height: 1.4; 241 white-space: pre-wrap; 242 word-break: break-word; 243 } 244 245 /* Loading Spinner */ 246 .spinner-verify { 247 width: 32px; 248 height: 32px; 249 border: 3px solid #f3f3f3; 250 border-top: 3px solid #007bff; 251 border-radius: 50%; 252 animation: spin 1s linear infinite; 253 margin: 0 auto; 254 } 255 256 /* Navigation */ 257 .navigation { 258 background: rgba(255, 255, 255, 0.05); 259 backdrop-filter: blur(10px); 260 border-bottom: 1px solid rgba(255, 255, 255, 0.1); 261 position: sticky; 262 top: 0; 263 z-index: 100; 264 margin-bottom: 2rem; 265 } 266 267 .nav-container { 268 max-width: 800px; 269 margin: 0 auto; 270 padding: 1rem 2rem; 271 display: flex; 272 justify-content: space-between; 273 align-items: center; 274 } 275 276 .nav-logo { 277 font-size: 1.25rem; 278 font-weight: 700; 279 color: white; 280 text-decoration: none; 281 background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); 282 -webkit-background-clip: text; 283 -webkit-text-fill-color: transparent; 284 background-clip: text; 285 } 286 287 .nav-logo:hover { 288 opacity: 0.8; 289 } 290 291 .nav-links { 292 display: flex; 293 align-items: center; 294 gap: 1.5rem; 295 } 296 297 .nav-link { 298 color: rgba(255, 255, 255, 0.8); 299 text-decoration: none; 300 font-weight: 500; 301 font-size: 0.875rem; 302 padding: 0.5rem 0.75rem; 303 border-radius: 6px; 304 transition: all 0.2s ease; 305 border: none; 306 background: none; 307 cursor: pointer; 308 font-family: inherit; 309 } 310 311 .nav-link:hover { 312 color: white; 313 background: rgba(255, 255, 255, 0.1); 314 } 315 316 .nav-button { 317 color: #ef4444; 318 } 319 320 .nav-button:hover { 321 background: rgba(239, 68, 68, 0.2); 322 } 323 324 /* Buttons */ 325 .btn { 326 padding: 0.75rem 1.5rem; 327 border: none; 328 border-radius: 8px; 329 font-weight: 500; 330 cursor: pointer; 331 transition: all 0.2s ease; 332 font-size: 0.875rem; 333 display: inline-flex; 334 align-items: center; 335 justify-content: center; 336 gap: 0.5rem; 337 min-width: 120px; 338 } 339 340 .btn-primary { 341 background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); 342 color: white; 343 } 344 345 .btn-primary:hover { 346 transform: translateY(-1px); 347 box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4); 348 } 349 350 .btn-secondary { 351 background: rgba(255, 255, 255, 0.1); 352 color: white; 353 border: 1px solid rgba(255, 255, 255, 0.2); 354 } 355 356 .btn-secondary:hover { 357 background: rgba(255, 255, 255, 0.2); 358 } 359 360 .btn-cancel { 361 background: rgba(239, 68, 68, 0.1); 362 color: #ef4444; 363 border: 1px solid rgba(239, 68, 68, 0.3); 364 } 365 366 .btn-cancel:hover { 367 background: rgba(239, 68, 68, 0.2); 368 } 369 370 /* Loading State */ 371 .loading-container { 372 display: flex; 373 justify-content: center; 374 align-items: center; 375 padding: 4rem 2rem; 376 } 377 378 .loading-content { 379 display: flex; 380 align-items: center; 381 gap: 1rem; 382 } 383 384 .spinner { 385 width: 2rem; 386 height: 2rem; 387 border: 3px solid rgba(59, 130, 246, 0.3); 388 border-top: 3px solid #3b82f6; 389 border-radius: 50%; 390 animation: spin 1s linear infinite; 391 } 392 393 .loading-text { 394 color: rgba(255, 255, 255, 0.7); 395 font-size: 0.875rem; 396 } 397 398 /* Empty State */ 399 .empty-state { 400 text-align: center; 401 padding: 4rem 2rem; 402 background: rgba(255, 255, 255, 0.02); 403 border: 1px solid rgba(255, 255, 255, 0.1); 404 border-radius: 16px; 405 } 406 407 .empty-icon { 408 width: 4rem; 409 height: 4rem; 410 color: rgba(255, 255, 255, 0.4); 411 margin: 0 auto 1rem; 412 } 413 414 .empty-title { 415 font-size: 1.25rem; 416 font-weight: 600; 417 color: white; 418 margin: 0 0 0.5rem 0; 419 } 420 421 .empty-description { 422 color: rgba(255, 255, 255, 0.6); 423 margin: 0; 424 } 425 426 /* Forms */ 427 .form-card { 428 background: rgba(255, 255, 255, 0.05); 429 backdrop-filter: blur(10px); 430 border: 1px solid rgba(255, 255, 255, 0.1); 431 border-radius: 16px; 432 padding: 2rem; 433 margin-bottom: 2rem; 434 width: 100%; 435 box-sizing: border-box; 436 } 437 438 .form-title { 439 font-size: 1.5rem; 440 font-weight: 600; 441 color: white; 442 margin: 0 0 1.5rem 0; 443 } 444 445 .form-fields { 446 display: flex; 447 flex-direction: column; 448 gap: 1.5rem; 449 } 450 451 .field-group { 452 display: flex; 453 flex-direction: column; 454 } 455 456 .form-actions { 457 display: flex; 458 gap: 1rem; 459 margin-top: 1rem; 460 } 461 462 /* Auth Pages */ 463 .auth-form { 464 max-width: 400px; 465 } 466 467 .auth-form-field { 468 margin-bottom: 1rem; 469 } 470 471 .auth-input { 472 width: 100%; 473 padding: 0.5rem; 474 margin-top: 0.25rem; 475 } 476 477 .auth-error { 478 color: red; 479 margin-bottom: 1rem; 480 padding: 0.5rem; 481 background-color: #fee; 482 border-radius: 4px; 483 } 484 485 .auth-button { 486 width: 100%; 487 padding: 0.75rem; 488 color: white; 489 border: none; 490 border-radius: 4px; 491 cursor: pointer; 492 } 493 494 .auth-button:disabled { 495 cursor: not-allowed; 496 } 497 498 .auth-button.primary { 499 background-color: #28a745; 500 } 501 502 .auth-button.secondary { 503 background-color: #007bff; 504 } 505 506 .auth-links { 507 margin-top: 1rem; 508 } 509 510 /* Todos */ 511 512 .todo-form { 513 width: 100%; 514 } 515 516 /* Todo List */ 517 .todos-list { 518 display: flex; 519 flex-direction: column; 520 gap: 1rem; 521 } 522 523 .todo-card { 524 background: rgba(255, 255, 255, 0.03); 525 backdrop-filter: blur(10px); 526 border: 1px solid rgba(255, 255, 255, 0.1); 527 border-radius: 12px; 528 transition: all 0.2s ease; 529 overflow: hidden; 530 width: 100%; 531 box-sizing: border-box; 532 } 533 534 .todo-card:hover { 535 border-color: rgba(255, 255, 255, 0.2); 536 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); 537 } 538 539 .todo-card.completed { 540 opacity: 0.7; 541 } 542 543 /* Todo Content */ 544 .todo-content { 545 padding: 1rem 1.5rem; 546 } 547 548 .todo-edit { 549 padding: 1.5rem; 550 min-height: 200px; 551 } 552 553 .edit-fields { 554 display: flex; 555 flex-direction: column; 556 gap: 1rem; 557 } 558 559 .edit-actions { 560 display: flex; 561 gap: 1rem; 562 margin-top: 1.5rem; 563 } 564 565 .todo-header { 566 display: flex; 567 justify-content: space-between; 568 align-items: center; 569 gap: 1rem; 570 } 571 572 .todo-title-btn { 573 background: none; 574 border: none; 575 padding: 0; 576 text-align: left; 577 font-size: 1.25rem; 578 font-weight: 600; 579 color: white; 580 cursor: pointer; 581 transition: color 0.2s ease; 582 flex: 1; 583 line-height: 1.4; 584 word-wrap: break-word; 585 overflow-wrap: break-word; 586 max-width: calc(100% - 140px); 587 } 588 589 .todo-title-btn:hover { 590 color: #3b82f6; 591 } 592 593 .todo-title-btn.completed { 594 text-decoration: line-through; 595 color: rgba(255, 255, 255, 0.5); 596 } 597 598 .todo-actions { 599 display: flex; 600 gap: 0.5rem; 601 flex-shrink: 0; 602 min-width: 132px; 603 justify-content: flex-end; 604 } 605 606 /* Action Buttons */ 607 .action-btn { 608 width: 40px; 609 height: 40px; 610 border: none; 611 border-radius: 6px; 612 cursor: pointer; 613 background: rgba(255, 255, 255, 0.05); 614 display: flex; 615 align-items: center; 616 justify-content: center; 617 font-size: 18px; 618 transition: all 0.2s ease; 619 -webkit-text-fill-color: currentColor; 620 } 621 622 .action-btn-complete { 623 color: #10b981; 624 font-size: 20px; 625 } 626 627 .action-btn-complete:hover { 628 background: rgba(16, 185, 129, 0.2); 629 color: #34d399; 630 } 631 632 .action-btn-edit { 633 color: #3b82f6; 634 } 635 636 .action-btn-edit:hover { 637 background: rgba(59, 130, 246, 0.2); 638 color: #60a5fa; 639 } 640 641 .action-btn-delete { 642 color: #ef4444; 643 } 644 645 .action-btn-delete:hover { 646 background: rgba(239, 68, 68, 0.2); 647 color: #f87171; 648 } 649 650 /* Add Todo Button */ 651 .add-todo-btn { 652 width: 36px; 653 height: 36px; 654 border: none; 655 border-radius: 6px; 656 cursor: pointer; 657 background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); 658 display: flex; 659 align-items: center; 660 justify-content: center; 661 color: white; 662 font-size: 18px; 663 font-weight: normal; 664 -webkit-text-fill-color: white; 665 transition: all 0.2s ease; 666 } 667 668 .add-todo-btn:hover { 669 transform: scale(1.1); 670 box-shadow: 0 4px 20px rgba(59, 130, 246, 0.4); 671 } 672 673 /* Todo Details */ 674 .todo-details { 675 margin-top: 1rem; 676 padding-top: 1rem; 677 border-top: 1px solid rgba(255, 255, 255, 0.1); 678 } 679 680 .description { 681 background: rgba(255, 255, 255, 0.02); 682 border: 1px solid rgba(255, 255, 255, 0.1); 683 border-radius: 8px; 684 padding: 1rem; 685 margin-bottom: 1rem; 686 } 687 688 .description p { 689 margin: 0; 690 color: rgba(255, 255, 255, 0.8); 691 line-height: 1.6; 692 } 693 694 .description.completed p { 695 text-decoration: line-through; 696 color: rgba(255, 255, 255, 0.5); 697 } 698 699 .todo-meta { 700 display: flex; 701 justify-content: space-between; 702 align-items: center; 703 gap: 1rem; 704 } 705 706 .meta-dates { 707 display: flex; 708 gap: 1rem; 709 flex-wrap: wrap; 710 } 711 712 .meta-item { 713 font-size: 0.75rem; 714 color: rgba(255, 255, 255, 0.5); 715 } 716 717 .completion-badge { 718 display: flex; 719 align-items: center; 720 gap: 0.25rem; 721 font-size: 0.75rem; 722 color: #10b981; 723 font-weight: 500; 724 } 725 726 .completion-icon { 727 width: 0.875rem; 728 height: 0.875rem; 729 } 730 731 /* Responsive Design */ 732 @media (max-width: 768px) { 733 .nav-container { 734 padding: 1rem; 735 flex-direction: column; 736 gap: 1rem; 737 } 738 739 .nav-links { 740 gap: 1rem; 741 flex-wrap: wrap; 742 justify-content: center; 743 } 744 745 .container { 746 padding: 1rem; 747 } 748 749 .form-actions { 750 flex-direction: column; 751 } 752 753 .edit-actions { 754 flex-direction: column; 755 } 756 757 .todo-header { 758 flex-direction: column; 759 align-items: flex-start; 760 gap: 1rem; 761 } 762 763 .todo-actions { 764 align-self: stretch; 765 justify-content: center; 766 } 767 768 .meta-dates { 769 flex-direction: column; 770 gap: 0.25rem; 771 } 772 773 .todo-meta { 774 flex-direction: column; 775 align-items: flex-start; 776 gap: 0.5rem; 777 } 778 } 779 780 /* File Upload */ 781 .file-upload-btn { 782 min-height: 120px; 783 flex-direction: column; 784 gap: 0.5rem; 785 width: 100%; 786 border: 2px dashed rgba(255, 255, 255, 0.3); 787 display: flex; 788 align-items: center; 789 justify-content: center; 790 cursor: pointer; 791 transition: all 0.2s ease; 792 } 793 794 .file-upload-btn:hover { 795 border-color: rgba(255, 255, 255, 0.5); 796 background-color: rgba(255, 255, 255, 0.05); 797 } 798 799 .file-upload-info { 800 margin-top: 0.5rem; 801 font-size: 0.875rem; 802 color: rgba(255, 255, 255, 0.8); 803 } 804 805 /* File Table */ 806 .file-table { 807 width: 100%; 808 border-collapse: collapse; 809 } 810 811 .file-table th { 812 padding: 0.75rem; 813 text-align: left; 814 border-bottom: 1px solid rgba(255, 255, 255, 0.1); 815 color: rgba(255, 255, 255, 0.7); 816 font-weight: 500; 817 font-size: 0.875rem; 818 } 819 820 .file-table th:last-child { 821 text-align: center; 822 } 823 824 .file-table td { 825 padding: 0.75rem; 826 border-bottom: 1px solid rgba(255, 255, 255, 0.05); 827 } 828 829 .file-table tr:hover { 830 background-color: rgba(255, 255, 255, 0.02); 831 } 832 833 .file-name { 834 color: white; 835 font-weight: 500; 836 } 837 838 .file-meta { 839 color: rgba(255, 255, 255, 0.6); 840 font-size: 0.875rem; 841 } 842 843 .file-actions { 844 display: flex; 845 gap: 0.5rem; 846 justify-content: center; 847 } 848 849 /* Responsive File Table */ 850 @media (max-width: 768px) { 851 .file-table { 852 font-size: 0.875rem; 853 } 854 855 .file-table th, 856 .file-table td { 857 padding: 0.5rem; 858 } 859 860 .file-actions { 861 flex-direction: column; 862 gap: 0.25rem; 863 } 864 } ``` ### Run and test the Application [Section titled “Run and test the Application”](#run-and-test-the-application) Start the development server to test your route protection implementation: ```bash npm run dev ``` Things to try out: 1. Try navigating to `/profile` - you should be redirected to the homepage `/` since you’re not authenticated. 2. Because you are not signed in, the navigation bar should only show the “Home” link and the placeholder for signin/signup links. 3. On the homepage, you should see a message indicating that you are not signed in. After we complete the next tutorial on user authentication, you will be able to sign in and access the protected `/profile` page and see how the navigation bar and homepage updates accordingly. ## How It Works [Section titled “How It Works”](#how-it-works) 1. **useAuth Composable**: Manages authentication state using Nhost’s client and provides reactive state through Vue’s composition API 2. **Vue Router Guards**: Navigation guards check authentication status before allowing access to protected routes 3. **Profile Page**: A protected page that displays user information, only accessible when authenticated 4. **Automatic Redirects**: Unauthenticated users are redirected to `/`, authenticated users can access `/profile` ## Key Features Demonstrated [Section titled “Key Features Demonstrated”](#key-features-demonstrated) Route Protection Routes are protected using Vue Router navigation guards and authentication composable, preventing unauthorized access to sensitive areas. Loading States Smooth loading indicators are shown during authentication checks to improve user experience. Automatic Redirects Users are automatically redirected based on their authentication status, ensuring proper navigation flow. Cross-tab Synchronization Authentication state is synchronized across multiple browser tabs using Nhost’s session storage events. Session Management Complete user session and profile information is displayed and managed throughout the application. # User Authentication in Vue > Add sign up, sign in, sign out, and email verification to your Vue app with Nhost Auth and reactive composables for session state. This tutorial part builds upon the [Protected Routes part](/getting-started/tutorials/vue/2-protected-routes) by adding complete email/password authentication with email verification functionality. You’ll implement sign up, sign in, email verification, and sign out features to create a full authentication flow. Note This is **Part 3** in the Full-Stack Vue Development with Nhost series. This part creates a production-ready authentication system with secure email verification and proper error handling. ## Full-Stack Vue Development with Nhost [Section titled “Full-Stack Vue Development with Nhost”](#full-stack-vue-development-with-nhost) ### [1. Create Project](/getting-started/tutorials/vue/1-introduction) [Set up your Nhost project](/getting-started/tutorials/vue/1-introduction) ### [2. Protected Routes](/getting-started/tutorials/vue/2-protected-routes) [Route protection basics](/getting-started/tutorials/vue/2-protected-routes) ### [3. User Authentication](/getting-started/tutorials/vue/3-user-authentication) [**Current** - Complete auth flow](/getting-started/tutorials/vue/3-user-authentication) ### [4. GraphQL Operations](/getting-started/tutorials/vue/4-graphql-operations) [CRUD operations with GraphQL](/getting-started/tutorials/vue/4-graphql-operations) ### [5. File Uploads](/getting-started/tutorials/vue/5-file-uploads) [File upload and management](/getting-started/tutorials/vue/5-file-uploads) ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * Complete the [Protected Routes part](/getting-started/tutorials/vue/2-protected-routes) first * The project from the previous part set up and running ## Step-by-Step Guide [Section titled “Step-by-Step Guide”](#step-by-step-guide) ### Create the Sign In Page [Section titled “Create the Sign In Page”](#create-the-sign-in-page) Build a comprehensive sign-in form with proper error handling and loading states. This page handles user authentication and includes special logic for post-verification sign-in. src/views/SignIn.vue ```vue 1 48 49 ``` ### Create the Sign Up Page [Section titled “Create the Sign Up Page”](#create-the-sign-up-page) Implement user registration with email verification flow. This page collects user information, creates accounts, and guides users through the email verification process. src/views/SignUp.vue ```vue 1 74 75 ``` ### Create the Email Verification Page [Section titled “Create the Email Verification Page”](#create-the-email-verification-page) Build a dedicated verification page that exchanges authorization codes for sessions using PKCE. This page handles the verification flow when users click the email verification link. src/views/Verify.vue ```vue 1 51 52 ``` Caution **Important Configuration Required:** Before testing email verification, you must configure your Nhost project’s authentication settings: 1. Go to your Nhost project dashboard 2. Navigate to **Auth → Settings → Authentication** 3. Add your local development URL (e.g., `http://localhost:5173`) to the **Allowed Redirect URLs** field 4. Ensure your production domain is also added when deploying Without this configuration, you’ll receive a `redirectTo not allowed` error when users attempt to sign up or verify their email addresses. ### Update Router Configuration [Section titled “Update Router Configuration”](#update-router-configuration) Update your router configuration to include the new authentication routes: src/router/index.ts ```ts 1 import { createRouter, createWebHistory } from "vue-router"; 2 import { useAuth } from "../lib/nhost/auth"; 3 import HomeView from "../views/HomeView.vue"; 4 import ProfileView from "../views/ProfileView.vue"; 5 import SignIn from "../views/SignIn.vue"; 6 import SignUp from "../views/SignUp.vue"; 7 import Verify from "../views/Verify.vue"; 8 9 const router = createRouter({ 10 history: createWebHistory(import.meta.env.BASE_URL), 11 routes: [ 12 { 13 path: "/", 14 name: "home", 15 component: HomeView, 16 }, 17 { 18 path: "/signin", 19 name: "SignIn", 20 component: SignIn, 21 }, 22 { 23 path: "/signup", 24 name: "SignUp", 25 component: SignUp, 26 }, 27 { 28 path: "/verify", 29 name: "Verify", 30 component: Verify, 31 }, 32 { 33 path: "/profile", 34 name: "profile", 35 component: ProfileView, 36 meta: { requiresAuth: true }, 37 }, 38 { 39 path: "/:pathMatch(.*)*", 40 redirect: "/", 41 }, 42 ], 43 }); 44 45 // Navigation guard for protected routes 46 router.beforeEach((to) => { 47 if (to.meta["requiresAuth"]) { 48 const { isAuthenticated, isLoading } = useAuth(); 49 50 // Show loading state while authentication is being checked 51 if (isLoading.value) { 52 // You can return a loading component path or handle loading in the component 53 return true; // Allow navigation, handle loading in component 54 } 55 56 if (!isAuthenticated.value) { 57 return "/"; // Redirect to home page 58 } 59 } 60 return true; 61 }); 62 63 export default router; ``` ### Add Navigation Links and Sign Out Functionality [Section titled “Add Navigation Links and Sign Out Functionality”](#add-navigation-links-and-sign-out-functionality) Update the navigation component to include links to the sign-in and sign-up pages, and implement the sign-out. src/components/Navigation.vue ```vue 1 36 37 ``` ### Run and Test the Application [Section titled “Run and Test the Application”](#run-and-test-the-application) Start your development server and test the complete authentication flow to ensure everything works properly. ```bash npm run dev ``` Things to try out: 1. Try signing up with a new email address. Check your email for the verification link and click it. See how you are sent to the verification page and then redirected to your profile. 2. Try signing out and then signing back in with the same credentials. 3. Notice how navigation links change based on authentication state showing “Sign In” and “Sign Up” when logged out, and “Profile” and “Sign Out” when logged in. 4. Check how the homepage also reflects the authentication state with appropriate messages. 5. Open multiple tabs and test signing out from one tab to see how other tabs respond. Now sign back in and see the changes propagate across tabs. ## Key Features Demonstrated [Section titled “Key Features Demonstrated”](#key-features-demonstrated) Complete Registration Flow Full email/password registration with proper form validation and user feedback. Email Verification Custom `/verify` endpoint that securely processes email verification tokens. Error Handling Comprehensive error handling for unverified emails, failed authentication, and network issues. Visual Feedback Loading states, success messages, and clear error displays throughout the authentication flow. Session Management Complete sign out functionality and proper session state management across the application. # GraphQL Operations in Vue > Learn how to perform GraphQL operations and manage database permissions while building a complete todos application with Nhost and Vue This part builds upon the previous parts by demonstrating how to perform GraphQL operations with proper [database permissions](/products/graphql/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. Note This is **Part 4** in the Full-Stack Vue Development with Nhost series. This part focuses on GraphQL operations, database management, and permission-based data access control in a production application. ## Full-Stack Vue Development with Nhost [Section titled “Full-Stack Vue Development with Nhost”](#full-stack-vue-development-with-nhost) ### [1. Create Project](/getting-started/tutorials/vue/1-introduction) [Set up your Nhost project](/getting-started/tutorials/vue/1-introduction) ### [2. Protected Routes](/getting-started/tutorials/vue/2-protected-routes) [Route protection basics](/getting-started/tutorials/vue/2-protected-routes) ### [3. User Authentication](/getting-started/tutorials/vue/3-user-authentication) [Complete auth flow](/getting-started/tutorials/vue/3-user-authentication) ### [4. GraphQL Operations](/getting-started/tutorials/vue/4-graphql-operations) [**Current** - CRUD operations with GraphQL](/getting-started/tutorials/vue/4-graphql-operations) ### [5. File Uploads](/getting-started/tutorials/vue/5-file-uploads) [File upload and management](/getting-started/tutorials/vue/5-file-uploads) ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * Complete the [User Authentication part](/getting-started/tutorials/vue/3-user-authentication) first * The project from the previous part set up and running ## What You’ll Build [Section titled “What You’ll Build”](#what-youll-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 * **Vue components** that interact with GraphQL endpoint ## Step-by-Step Guide [Section titled “Step-by-Step Guide”](#step-by-step-guide) ### Create the To-Dos Table [Section titled “Create the To-Dos Table”](#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: 1. Navigate to **Database** 2. Click on **SQL Console** Enter the following SQL: * SQL ```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(); ``` * UI ![Database SQL Editor](/images/tutorials/todos/1.png) Caution Please make sure to enable **Track this** so that the new table todos is available through the auto-generated APIs ### Set Up Permissions [Section titled “Set Up Permissions”](#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 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. ![Insert Permissions Configuration](/images/tutorials/todos/2.png) * Select For selecting (reading) todos, we are allowing to read all columns but only for rows where the `user_id` matches the authenticated user’s ID. This ensures that users can only see their own todos. ![Select Permissions Configuration](/images/tutorials/todos/3.png) * Update When updating todos, we are allowing users to modify the `title`, `details`, and `completed` columns but only for rows where the `user_id` matches their own ID. This prevents users from modifying todos that do not belong to them. ![Update Permissions Configuration](/images/tutorials/todos/4.png) * Delete For deleting todos, we are allowing users to delete rows only where the `user_id` matches their own ID. This ensures that users cannot delete todos that belong to other users. ![Delete Permissions Configuration](/images/tutorials/todos/5.png) ### Create the Todos Page Component [Section titled “Create the Todos Page Component”](#create-the-todos-page-component) Now let’s implement the Vue component that uses the database we just configured. src/views/Todos.vue ```vue 1