Vibe coding with AI course Β· Module 9: Discord Clone

Authentication - NextAuth.js

3 min read
In this lesson4

Today anyone can open the guild panel and change roles, because the app doesn't know who is sitting on the other side of the screen. The roles from the previous lessons say what each person may do, but first you have to establish who someone is. That is the job of logging in, or authentication. A homemade password system is easy to get wrong, which is why the Next.js documentation recommends ready-made libraries such as NextAuth.js.

Installation

You install the package with a single command:

npm install next-auth

This command installs the stable version 4 of the library, and that is the API this lesson shows.

Signing in with Google

The configuration goes into a Route Handler that will handle every address starting with /api/auth:

// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';

const handler = NextAuth({
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!
    })
  ]
});

export { handler as GET, handler as POST };

The [...nextauth] folder catches every address under /api/auth: the sign-in page, signing out and the return from Google. We use GoogleProvider with a clientId and a clientSecret, which you get from Google Cloud Console and keep in environment variables, never in code. In Google's list of allowed redirect addresses you enter http://localhost:3000/api/auth/callback/google, and in production the same address with your domain. The last line exports the same function as GET and POST, because signing in uses both methods.

A protected page

Only a logged-in pirate should see the panel. In a Server Component you get the session on the server side:

import { getServerSession } from 'next-auth';

export default async function Dashboard() {
  const session = await getServerSession();

  if (!session) {
    redirect('/api/auth/signin');
  }

  return <div>Welcome {session.user.name}!</div>;
}

await getServerSession() returns the session or null. When there is no session, redirect('/api/auth/signin') takes place, a jump to NextAuth's built-in sign-in page. A logged-in pirate is greeted by the name from session.user.name.

The example is missing one import, without which redirect won't work:

import { redirect } from 'next/navigation';

In a full project you also pass getServerSession the same configuration object (authOptions) you initialize NextAuth with, as the library's documentation recommends. The session is checked on the server before anything reaches the browser, so a user who isn't logged in won't see even a fragment of the panel.

What changes in newer versions

NextAuth.js is now developed under the name Auth.js. Version 5 is still marked as beta: the configuration moves to an auth.ts file that exports auth, handlers, signIn and signOut, and instead of getServerSession you call a single function:

const session = await auth();

Since September 2025, Auth.js has been maintained by the Better Auth team, which recommends its own Better Auth library for new projects and promises existing apps continued security fixes. In Next.js 16 the middleware.ts file, often used to protect many pages at once, is called proxy.ts.

Captain Redbeard's tip: don't post a single guard at the gangway. A redirect in the proxy is convenient, but check the session also where you read and change data, as the Next.js documentation advises.

Remember: logging in answers the question of who you are, and roles answer what you are allowed to do. Pirate's Cove was built in this order: servers with categories and channels, voice channels with rich presence, roles with moderation, and finally integrations and login. The editor below contains the role panel from the permissions lesson, exactly the place that login should protect. Then solve the tasks.

Code for this lesson: App.jsx
// Role Manager
import { useState } from 'react';

const roles = [
  { id: 1, name: 'Captain', color: '#e74c3c', memberCount: 3, position: 4 },
  { id: 2, name: 'First Mate', color: '#3498db', memberCount: 5, position: 3 },
  { id: 3, name: 'Navigator', color: '#2ecc71', memberCount: 8, position: 2 },
  { id: 4, name: 'Crew', color: '#95a5a6', memberCount: 45, position: 1 }
];

const permissions = [
  'ADMINISTRATOR',
  'MANAGE_SERVER',
  'MANAGE_ROLES',
  'MANAGE_CHANNELS',
  'KICK_MEMBERS',
  'BAN_MEMBERS',
  'SEND_MESSAGES',
  'MANAGE_MESSAGES'
];

export default function App() {
  const [selectedRoleId, setSelectedRoleId] = useState(1);

  const selectedRole = roles.find(r => r.id === selectedRoleId);

  return (
    <div className="flex h-screen bg-gray-800">
      {/* Roles List */}
      <div className="w-64 bg-gray-900 p-4">
        <div className="flex items-center justify-between mb-4">
          <h3 className="font-bold text-white">Roles</h3>
          <button className="w-8 h-8 bg-green-600 hover:bg-green-700 rounded-full text-white">
            +
          </button>
        </div>

        <div className="space-y-1">
          {roles.sort((a, b) => b.position - a.position).map(role => (
            <button
              key={role.id}
              onClick={() => setSelectedRoleId(role.id)}
              className={`w-full text-left px-3 py-2 rounded flex items-center justify-between ${
                selectedRoleId === role.id ? 'bg-gray-700' : 'hover:bg-gray-800'
              }`}
            >
              <div className="flex items-center gap-2 flex-1">
                <div
                  className="w-3 h-3 rounded-full"
                  style={{ backgroundColor: role.color }}
                />
                <span className="text-white text-sm truncate">{role.name}</span>
              </div>
              <span className="text-xs text-gray-400">{role.memberCount}</span>
            </button>
          ))}
        </div>
      </div>

      {/* Role Editor */}
      {selectedRole && (
        <div className="flex-1 p-6 overflow-y-auto">
          <h2 className="text-2xl font-bold text-white mb-6">
            Edit Role: {selectedRole.name}
          </h2>

          {/* Display */}
          <div className="bg-gray-700 rounded-lg p-4 mb-6">
            <h3 className="font-bold text-white mb-3">Display</h3>

            <div className="mb-4">
              <label className="block text-sm text-gray-300 mb-2">Role Name</label>
              <input
                type="text"
                key={selectedRole.id}
                defaultValue={selectedRole.name}
                className="w-full bg-gray-900 text-white px-3 py-2 rounded"
              />
            </div>

            <div className="mb-4">
              <label className="block text-sm text-gray-300 mb-2">Role Color</label>
              <div className="flex items-center gap-3">
                <input
                  type="color"
                  key={selectedRole.id}
                  defaultValue={selectedRole.color}
                  className="w-16 h-10 rounded cursor-pointer"
                />
                <span className="text-gray-400 text-sm">{selectedRole.color}</span>
              </div>
            </div>

            <label className="flex items-center gap-2 text-sm text-gray-300 mb-2">
              <input type="checkbox" className="w-4 h-4" />
              Display role members separately
            </label>

            <label className="flex items-center gap-2 text-sm text-gray-300">
              <input type="checkbox" className="w-4 h-4" />
              Allow anyone to @mention this role
            </label>
          </div>

          {/* Permissions */}
          <div className="bg-gray-700 rounded-lg p-4">
            <h3 className="font-bold text-white mb-3">Permissions</h3>

            <div className="space-y-2">
              {permissions.map(permission => (
                <label
                  key={permission}
                  className="flex items-center gap-3 text-sm text-gray-300 hover:bg-gray-600 p-2 rounded cursor-pointer"
                >
                  <input type="checkbox" className="w-4 h-4" />
                  <span>{permission.split('_').join(' ')}</span>
                </label>
              ))}
            </div>
          </div>

          {/* Actions */}
          <div className="mt-6 flex gap-3">
            <button className="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded font-semibold">
              Save Changes
            </button>
            <button className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded font-semibold">
              Delete Role
            </button>
          </div>
        </div>
      )}
    </div>
  );
}

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. Which provider is used in the NextAuth configuration from the example?

  2. 2. What happens in Dashboard when getServerSession() returns no session?

Hands-on tasks in the game

  • Horizontal ordering

    Arrange access to the signed-in user's name: session.user.name

  • Click in order

    Click to build the server-side session retrieval: await getServerSession()

  • Vertical ordering

    Arrange the order of building the Discord clone in this module:

Useful articles