DEV Community

Cover image for Full-Stack Authentication Demystified — A Step-by-Step Guide with React, Node.js, and Supabase
Abdul Fadiga
Abdul Fadiga

Posted on

Full-Stack Authentication Demystified — A Step-by-Step Guide with React, Node.js, and Supabase

Authentication is the foundation of any modern web application. It's the mechanism that powers everything from social media logins to secure online banking. But for many developers, the flow of tokens, sessions, and server handshakes can feel like a complex puzzle.

This guide will demystify that puzzle. We will build a complete, secure, and modern authentication system from the ground up. We won't just write code; we'll start with the foundational concepts, explore when and why to use them, and use a clear analogy to make it all stick.

Table of Contents

  • Part 1: The Foundations - Core Security Concepts
    • 1.1 Authentication (AuthN): "Who Are You?"
    • 1.2 Authorization (AuthZ): "What Can You Do?"
    • 1.3 Sessions: The Server's Memory
    • 1.4 JSON Web Tokens (JWTs): The Self-Contained Pass
  • Part 2: The Blueprint - Our Tools & The Restaurant Analogy
    • 2.1 Introducing Our Stack: React, Node.js, and Supabase
    • 2.2 The Analogy: Tying a Restaurant's Service to Our App's Flow
  • Part 3: The Build - Step-by-Step Implementation
    • 3.1 Setting Up the Service (Supabase)
    • 3.2 Building the Backend API (The Host)
    • 3.3 Building the Frontend UI (The Dining Room)
  • Part 4: The Grand Finale - Tracing a Live Request
  • Conclusion & Next Steps

Part 1: The Foundations - Core Security Concepts

Before we can build, we must understand. These four terms are the bedrock of our system.

1.1 Authentication (AuthN): "Who Are You?"

  • Definition: Authentication is the process of verifying that a user is who they claim to be. It’s the act of proving your identity.
  • Use Cases:
    • Logging into your email account with a password.
    • Using your fingerprint to unlock your phone.
    • Entering a 2FA (Two-Factor Authentication) code sent to your phone.
  • Analogy: When you arrive at a restaurant for a reservation, the host asks for your name and checks your ID. They are authenticating your identity.

1.2 Authorization (AuthZ): "What Can You Do?"

  • Definition: Authorization is the process of verifying what a specific user is allowed to do. It happens after successful authentication.
  • Use Cases:
    • A regular user on a blog can read comments, but an admin user can delete them.
    • You can view your own profile on a social media site, but you can't edit someone else's.
    • A "premium" subscriber can access video content, while a "free" user cannot.
  • Analogy: Once the host confirms your identity (Authentication), they check their list to see your permissions (Authorization). Are you a regular guest for the main dining room, or a VIP with access to the exclusive rooftop lounge?

1.3 Sessions: The Server's Memory

  • Definition: A session is a stateful way for a server to remember an authenticated user. When you log in, the server creates a unique Session ID, stores it in its own database or memory, and sends that ID to your browser as a cookie. On every future request, your browser sends the cookie back, and the server looks it up to identify you.
  • Use Cases:
    • Traditional websites and monolithic applications.
    • Applications where server-side rendering is prevalent.
  • Analogy: The host gives you a simple claim ticket (like #37). They then write down in their own notebook: "Ticket #37 belongs to John Doe." When you return, you just show your ticket. The host has to do the work of looking it up in their notebook. This is stateful because the host's notebook (the server) must maintain the state.

1.4 JSON Web Tokens (JWTs): The Self-Contained Pass

  • Definition: A JWT is a stateless, secure, and self-contained digital "pass." When you log in, the server generates an encrypted string (the JWT) containing your identity information (like your user ID) and gives it to you. You (the client) hold onto it. On future requests, you present this pass. The server can verify its authenticity on its own without needing to look anything up.
  • Use Cases:
    • Modern web applications, especially SPAs (Single-Page Applications) like React, Vue, or Angular.
    • Mobile applications communicating with a backend API.
    • Securely communicating between different microservices.
  • Analogy: Instead of a simple ticket, the host gives you a detailed, laminated, tamper-proof VIP Wristband. This wristband has your name and access level (e.g., "John Doe - VIP Lounge") encoded directly on it with a special holographic seal. When you approach the VIP lounge, the bouncer doesn't need a list. They just look at your wristband and check the seal. If the seal is intact, they know the pass is authentic and who you are. This is stateless because the bouncer (the server) doesn't need to remember you; all the information is on the pass itself.

Part 2: The Blueprint - Our Tools & The Restaurant Analogy

Now, let's map our analogy to our technology stack.

2.1 Introducing Our Stack

  • React (Frontend): The beautiful and interactive "dining room" where the user interacts.
  • Node.js/Express (Backend): The smart and logical "host and kitchen staff" that takes requests, processes them, and returns results.
  • Supabase (Service): The official "master reservation system" and security team. It manages the user database and verifies credentials.

2.2 The Analogy: Tying It All Together

Restaurant Concept Technical Component Role
The Customer React Application Displays the UI, collects user input (email/password).
The Host Node.js/Express API The public-facing entry point. Takes the order from the customer.
The Master Reservation System Supabase The source of truth. Securely stores user data and verifies passwords.
The VIP Wristband JWT (Access Token) The secure, self-contained pass given to the customer after a successful login.

The flow is simple: The Customer gives their details to the Host. The Host checks with the Master System. If everything is correct, the Host issues a VIP Wristband to the Customer, who can then use it to access protected areas.


Part 3: The Build - Step-by-Step Implementation

Time to bring our blueprint to life.

3.1 Setting Up the Service (Supabase)

  1. Create a new project on supabase.com.
  2. Navigate to Project Settings > API.
  3. Copy your Project URL and anon public API Key. Keep these ready.

3.2 Building the Backend API (The Host)

In your backend directory, create an index.js file and a .env file for your Supabase credentials.

File: backend/.env

SUPABASE_URL="YOUR_SUPABASE_PROJECT_URL_HERE"
SUPABASE_ANON_KEY="YOUR_SUPABASE_ANON_PUBLIC_KEY_HERE"
Enter fullscreen mode Exit fullscreen mode

File: backend/index.js

// 1. Import dependencies
const express = require("express");
const { createClient } = require("@supabase/supabase-js");
const cors = require('cors');
require("dotenv").config(); // Load .env variables

// 2. Initialize Express app and Supabase client
const app = express();
const PORT = 4000;
const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY);

// 3. Apply middleware
app.use(cors()); // Allow requests from our frontend
app.use(express.json()); // Allow the server to read JSON from request bodies

// 4. Define the Authentication endpoints
// SIGNUP
app.post("/api/auth/signup", async (req, res) => {
  const { email, password } = req.body;
  const { data, error } = await supabase.auth.signUp({ email, password });
  if (error) return res.status(400).json({ error: error.message });
  return res.status(201).json({ user: data.user });
});

// LOGIN
app.post("/api/auth/login", async (req, res) => {
  const { email, password } = req.body;
  const { data, error } = await supabase.auth.signInWithPassword({ email, password });
  if (error) return res.status(400).json({ error: error.message });
  return res.status(200).json({ session: data.session });
});

// 5. Start the server
app.listen(PORT, () => {
  console.log(`[BACKEND] Server is live on http://localhost:${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

3.3 Building the Frontend UI (The Dining Room)

In your React project, we'll create two key components.

File: frontend/src/App.js (The State Manager)

import { useState } from 'react';
import Auth from './Auth';
import './index.css';

function App() {
  const [session, setSession] = useState(null);
  const handleLoginSuccess = (sessionData) => setSession(sessionData);
  const handleLogout = () => setSession(null);

  return (
    <>
      {!session ? (
        <Auth onLoginSuccess={handleLoginSuccess} />
      ) : (
        <div className="container">
          <h1>Welcome, {session.user.email}!</h1>
          <button onClick={handleLogout}>Log Out</button>
        </div>
      )}
    </>
  );
}
export default App;
Enter fullscreen mode Exit fullscreen mode

File: frontend/src/Auth.js (The Form & API Caller)

import { useState } from 'react';

export default function Auth({ onLoginSuccess }) {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  // This single function handles both login and signup
  const handleAuth = async (endpoint) => {
    setLoading(true);
    setError(null);
    try {
      const response = await fetch(`http://localhost:4000/api/auth/${endpoint}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password }),
      });
      const data = await response.json();
      if (!response.ok) throw new Error(data.error);

      if (endpoint === 'login') {
        onLoginSuccess(data.session); // Pass the VIP Wristband (session) up to App.js
      } else {
        alert('Signup successful! Please log in.');
      }
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  // Your form JSX goes here...
  return (
    <div className="container">
      {/* (You can add your full form JSX here) */}
      <input type="email" placeholder="Email" value={email} onChange={(e) => setEmail(e.target.value)} />
      <input type="password" placeholder="Password" value={password} onChange={(e) => setPassword(e.target.value)} />
      <button onClick={() => handleAuth('login')} disabled={loading}>Log In</button>
      <button className="secondary" onClick={() => handleAuth('signup')} disabled={loading}>Create Account</button>
      {error && <p className="error-message">{error}</p>}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Part 4: The Grand Finale - Tracing a Live Request

Let's trace a login from start to finish:

  1. Click: The user clicks "Log In" in the React app.
  2. Frontend: handleAuth('login') is called. It sends a POST fetch request to our backend at http://localhost:4000/api/auth/login.
  3. Backend: The Express server receives this request, takes the email/password, and asks Supabase to verify them.
  4. Service: Supabase confirms the credentials are correct.
  5. Backend: Supabase gives our backend a session object containing the user's data and a fresh JWT. Our server sends this session object back to the frontend.
  6. Frontend: The fetch call succeeds. onLoginSuccess is called with the session data.
  7. Re-render: The state in App.js changes. React automatically re-renders the screen, showing the "Welcome!" message instead of the login form.

Conclusion & Next Steps

Congratulations! You have successfully built a complete, decoupled, and secure authentication system. You've learned the difference between AuthN and AuthZ, the power of stateless JWTs, and how to orchestrate a full-stack data flow.

The next logical step is Authorization. You can now take the JWT stored on the frontend, include it in the Authorization: Bearer <token> header of your API calls, and build a middleware in Express to verify it, ensuring that only authenticated users can access protected data.

Happy coding!


Enter fullscreen mode Exit fullscreen mode

Top comments (0)