How the NextAuth.js Authentication Plugin Architecture Works in prompts.chat

The prompts.chat repository implements a modular NextAuth.js authentication system where each provider (Google, GitHub, Azure, etc.) is encapsulated as a pluggable AuthPlugin, registered in a global registry at runtime, and dynamically assembled into the final NextAuth configuration based on the prompts.config.ts settings.

The open-source prompts.chat project demonstrates an elegant approach to authentication scalability by wrapping NextAuth.js providers in a plugin-based architecture. This design decouples provider-specific logic from the core application, allowing developers to add or remove authentication methods by simply registering plugins and updating configuration. Understanding this NextAuth.js authentication plugin architecture reveals how to build extensible, maintainable auth systems in Next.js applications.

Core Components of the Plugin Architecture

The AuthPlugin Contract

The foundation of the system is the AuthPlugin interface defined in src/lib/plugins/types.ts (lines 7-14). This contract standardizes how authentication providers integrate with the application.

// src/lib/plugins/types.ts
export interface AuthPlugin {
  id: string;
  name: string;
  getProvider: () => any;
}

Each plugin must expose a unique id, a display name, and a getProvider() method that returns a configured NextAuth provider object.

Global Plugin Registry

The registry in src/lib/plugins/registry.ts (lines 4-19) maintains global Map instances that store active plugins. It exposes registerAuthPlugin() for adding providers and getAuthPlugin() for retrieval by ID, enabling runtime plugin resolution without hardcoded provider lists.

Built-in Provider Implementations

Concrete implementations reside in src/lib/plugins/auth/. For example, src/lib/plugins/auth/google.ts (lines 1-12) implements the Google provider:

// src/lib/plugins/auth/google.ts
import GoogleProvider from "next-auth/providers/google";
import type { AuthPlugin } from "../types";

export const googlePlugin: AuthPlugin = {
  id: "google",
  name: "Google",
  getProvider: () =>
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
};

Configuration Flow from Registry to NextAuth

Plugin Initialization

When the server boots, initializePlugins()—called from src/lib/auth/index.ts at line 10—executes registerBuiltInAuthPlugins(). This function iterates over each file in src/lib/plugins/auth/ and registers them via registerAuthPlugin(), as implemented in src/lib/plugins/auth/index.ts (lines 9-15).

// src/lib/auth/index.ts (excerpt)
import { initializePlugins } from "@/lib/plugins";
initializePlugins();

Dynamic Config Assembly

The buildAuthConfig() function in src/lib/auth/index.ts (lines 22-53) orchestrates the dynamic configuration. It reads the application configuration via getConfig(), supporting both the modern auth.providers: [] array and the legacy auth.provider string (lines 8-20).

For each configured provider ID, getAuthPlugin(id) looks up the plugin in the registry (lines 27-40). Missing plugins are logged and skipped; if no valid plugins are found, the system throws an error to prevent startup with broken authentication.

Custom Prisma Adapter Integration

The architecture wraps the default NextAuth Prisma adapter in a CustomPrismaAdapter() (lines 30-66 in src/lib/auth/index.ts). This customization adds automatic username generation, handles unclaimed accounts, and implements retry logic for username collisions before delegating to the standard adapter.

JWT Session Management and Callbacks

The final configuration object assembled in src/lib/auth/index.ts specifies session.strategy: "jwt" for stateless sessions and implements custom callbacks to maintain data consistency:

  • JWT Callback (lines 54-95): Enriches the token with fresh database user data on every sign-in and subsequent request, invalidating tokens when users are deleted from the database.
  • Session Callback (lines 96-112): Copies JWT fields into the session.user object delivered to the client application.

Implementing a Custom Authentication Provider

Adding a new provider requires three steps following the established NextAuth.js authentication plugin architecture:

  1. Create the plugin file at src/lib/plugins/auth/example.ts:
import ExampleProvider from "next-auth/providers/example";
import type { AuthPlugin } from "../types";

export const examplePlugin: AuthPlugin = {
  id: "example",
  name: "Example",
  getProvider: () =>
    ExampleProvider({
      clientId: process.env.EXAMPLE_CLIENT_ID!,
      clientSecret: process.env.EXAMPLE_CLIENT_SECRET!,
    }),
};
  1. Register the plugin in src/lib/plugins/auth/index.ts:
import { examplePlugin } from "./example";
registerAuthPlugin(examplePlugin);
  1. Enable in configuration by adding "example" to the auth.providers array in prompts.config.ts:
{
  "auth": {
    "providers": ["credentials", "google", "example"]
  }
}

Consuming Auth Helpers in the Application

The configured NextAuth instance exports handlers, signIn, signOut, and auth from src/lib/auth/index.ts (lines 17-21):

export const { handlers, signIn, signOut, auth } = NextAuth(authConfig);

Use these in client components:

// src/app/login/page.tsx
"use client";
import { signIn } from "@/lib/auth";

export function LoginButtons() {
  return (
    <button onClick={() => signIn("google")}>
      Continue with Google
    </button>
  );
}

And in API routes:

// src/app/api/auth/route.ts
import { handlers } from "@/lib/auth";

export const GET = handlers;
export const POST = handlers;

Summary

  • The AuthPlugin interface in src/lib/plugins/types.ts standardizes provider integration through an id, name, and getProvider() method.
  • A global registry in src/lib/plugins/registry.ts maintains runtime plugin storage with registerAuthPlugin() and getAuthPlugin() utilities.
  • Dynamic configuration occurs in src/lib/auth/index.ts, where buildAuthConfig() assembles NextAuth settings from registered plugins and prompts.config.ts.
  • The CustomPrismaAdapter extends the default adapter to handle username generation and collision retries.
  • The system exports standard NextAuth helpers (signIn, signOut, auth, handlers) for consumption throughout the application.

Frequently Asked Questions

What is the AuthPlugin interface structure?

The AuthPlugin interface, defined in src/lib/plugins/types.ts at lines 7-14, requires three properties: id (unique string identifier), name (display name), and getProvider() (a method returning a configured NextAuth provider instance).

How does prompts.chat handle missing auth providers?

During configuration assembly in src/lib/auth/index.ts (lines 27-40), the system calls getAuthPlugin(id) for each configured provider. If a plugin is missing, it logs a warning and continues; however, if no valid providers are found at all, it throws an error to prevent startup with broken authentication.

Can multiple authentication providers be active simultaneously?

Yes. The architecture supports an array of providers via the auth.providers configuration key in prompts.config.ts. The buildAuthConfig() function iterates through all configured IDs and aggregates their provider objects into the final NextAuth configuration.

Where is the session strategy configured?

The session strategy is set to "jwt" within the configuration object built in src/lib/auth/index.ts. This enables stateless sessions where the JWT callback (lines 54-95) enriches tokens with fresh user data from the database on each request.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →