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.userobject 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:
- 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!,
}),
};
- Register the plugin in
src/lib/plugins/auth/index.ts:
import { examplePlugin } from "./example";
registerAuthPlugin(examplePlugin);
- Enable in configuration by adding
"example"to theauth.providersarray inprompts.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.tsstandardizes provider integration through anid,name, andgetProvider()method. - A global registry in
src/lib/plugins/registry.tsmaintains runtime plugin storage withregisterAuthPlugin()andgetAuthPlugin()utilities. - Dynamic configuration occurs in
src/lib/auth/index.ts, wherebuildAuthConfig()assembles NextAuth settings from registered plugins andprompts.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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →