# NextAuth + JWT + Express + Prisma — Complete Authentication Flow Explained

When you’re building a full-stack app with **Next.js (NextAuth)** and a separate **Express backend**, one of the biggest challenges is:

> How does authentication actually flow across frontend → backend → database?

In this blog, I’ll break down a **real production-style setup** using:

*   NextAuth (OAuth login)
    
*   JWT (session strategy)
    
*   Express (API layer)
    
*   Prisma (database)
    

* * *

# Big Picture Architecture

```text
User → NextAuth → JWT (cookie) → Express → Prisma → DB
```

This setup handles **authentication + authorization cleanly**.

* * *

# 1\. Providers (Login Methods)

```ts
providers: [
  GoogleProvider({...}),
  GitHubProvider({...}),
]
```

### What it does:

Allows users to log in via:

*   Google
    
*   GitHub
    

* * *

### Flow:

```text
User clicks login → OAuth provider → returns profile → NextAuth receives it
```

* * *

# 2\. Secret (VERY IMPORTANT)

```ts
secret: process.env.NEXTAUTH_SECRET
```

### Why this matters:

*   Signs JWT
    
*   Verifies JWT
    
*   MUST be same in backend (Express)
    

* * *

# 3\. Session Strategy (Critical)

```ts
session: {
  strategy: "jwt"
}
```

* * *

### What this means:

```text
Session is stored as JWT inside cookie
```

Instead of:

```text
Database sessions 
```

* * *

### Why you need this

Your backend uses:

```ts
jwtVerify(token, secret)
```

This ONLY works if session = JWT

* * *

# 4\. JWT Callback (Core Identity Logic)

```ts
jwt: async ({ user, token }) => {
  if (user) {
    token.sub = user.id;
    token.name = user.name;
    token.email = user.email;
  }
  return token;
}
```

* * *

### When it runs:

```text
Only during login
```

* * *

### What it does:

Stores user info inside JWT

* * *

### Resulting JWT payload:

```json
{
  "sub": "abc123",
  "name": "Roodius",
  "email": "user@email.com"
}
```

* * *

### Key Insight

```text
sub = user.id (primary identity)
```

This is what your backend uses

* * *

# 5\. Session Callback (Frontend Access)

```ts
session: async ({ session, token }) => {
  if (session.user && token) {
    session.user.id = token.sub;
    session.user.name = token.name;
    session.user.email = token.email;
  }
  return session;
}
```

* * *

### When it runs:

```text
Every time session is fetched
```

* * *

### What it does:

Moves data from:

```text
JWT → session object
```

* * *

### Final session object:

```json
{
  "user": {
    "id": "abc123",
    "name": "Roodius",
    "email": "user@email.com"
  }
}
```

* * *

### Why this matters

Frontend uses:

```ts
const session = await getSession();
```

Without this, `user.id` won’t exist

* * *

# 6\. signIn Callback (Database Sync)

```ts
signIn: async ({ user }) => {
  const existing = await prisma.user.findUnique({
    where: { email: user.email }
  });

  if (!existing) {
    await prisma.user.create({
      data: {
        name: user.name,
        email: user.email
      }
    });
  }

  return true;
}
```

* * *

### What it does:

```text
Checks if user exists → creates if not
```

* * *

### Flow:

```text
OAuth login → NextAuth → DB check → create user if needed
```

* * *

### ⚠️ Important

If you use:

```ts
PrismaAdapter(prisma)
```

REMOVE this block (handled automatically)

* * *

# End-to-End Flow

```text
1. User logs in via Google/GitHub
2. signIn → ensures user exists in DB
3. jwt → stores user.id in token.sub
4. Cookie stores JWT
5. Frontend sends request with cookie
6. Express middleware:
     → verifies JWT
     → extracts payload.sub
     → sets req.user.id
7. Controller uses req.user.id
```

* * *

# Backend Middleware (Express)

```ts
const { payload } = await jwtVerify(token, secret);

req.user = {
  id: payload.sub
};
```

* * *

### Result:

```text
req.user.id = authenticated user
```

No need to send userId manually

* * *

# Common Mistakes

* * *

## Sending userId from frontend

```json
{
  "userId": "abc123"
}
```

Security risk

* * *

## Not using JWT strategy

Backend verification fails

* * *

## Different NEXTAUTH\_SECRET

Token verification breaks

* * *

## Using DB session + JWT verify together

Mismatch

* * *

# Best Practices

✔ Use JWT session strategy

✔ Use same secret across apps

✔ Extract identity from token (not request body)

✔ Keep auth logic in middleware

✔ Keep controllers clean

* * *

[Github](https://guthub.com/Roodius-hub)

[Twitter](https://x.com/Roodius_)
