The most lightweight, pluggable, and developer-friendly authentication library for the modern web.
Bring your own database, define your own user structure, and let NanoAuth handle the heavy lifting of authentication flows, security, and stateless sessions.
NanoAuth was built on a simple philosophy: You should control your data, and the auth library should just orchestrate the logic.
Unlike bloated full-stack mono-libraries that try to insert their own rigid database schemas into your project, NanoAuth provides a robust, zero-dependency, completely stateless core that you extend via Plugins and Adapters.
- ✨ 100% Type-Safe: Built from the ground up with TypeScript Generics. If your user has a custom
subscriptionStatusfield, all NanoAuth hooks and plugins will know about it. - 🛡️ Secure Stateless Architecture: The server core is pure and stateless. It relies on standard Web Request/Response flows to read contexts via
auth.getSession(request), ensuring no global state leaks between concurrent requests. - 🧩 Pluggable Architecture: Only bundle what you use. Need Email/Password? Add the plugin. Need Session Management? Add the plugin.
- ⚡ Reactive Client SDK: An ultra-lightweight frontend SDK powered by Nanostores makes bridging your UI with the authentication state incredibly easy and framework-agnostic.
- 🔥 Generic Web Handler: Use our built-in
auth.handler(request)to automatically generate all authentication endpoints for modern environments (Next.js, Cloudflare Workers, Hono, Bun, etc.).
bun add nanoauthNanoAuth is intended to be framework-agnostic. Use it in Node, Bun, Deno, Cloudflare Workers, or wherever TypeScript runs!
To master NanoAuth, you just need to understand its 4 core pillars:
- The User Type: Your custom data structure.
- The Database Adapter: How NanoAuth talks to your database (built via
defineAdapter). - The Plugins: The actual auth logic and endpoints (built via
definePlugin). - The Client SDK: Reading auth state gracefully in the browser.
Let's dive into each one! 👇
NanoAuth doesn't force a database schema on you.
You define your schema by extending the base User interface. NanoAuth will infer this type everywhere.
import type { User } from 'nanoauth';
export interface MyAwesomeUser extends User {
role: 'admin' | 'customer';
tenantId: string;
favoriteColor: string; // Because why not? 🎨
}The Adapter is the bridge between NanoAuth and your actual database. The library provides the defineAdapter helper to give you strict type checking and perfect autocomplete.
import { defineAdapter } from 'nanoauth';
import type { MyAwesomeUser } from './types';
import { db } from './my-database';
// defineAdapter provides perfect autocompletion for the required methods!
export const myAdapter = defineAdapter<MyAwesomeUser>({
// NanoAuth asks: "Hey, fetch a user by this ID"
async getUser(userId) {
return await db.users.findUnique({ where: { id: userId } });
},
// NanoAuth asks: "Hey, store this session payload"
async saveSession(sessionId, data) {
await db.sessions.insert({ id: sessionId, payload: data });
},
// NanoAuth asks: "Hey, delete this session"
async deleteSession(sessionId) {
await db.sessions.delete({ where: { id: sessionId } });
},
// NanoAuth asks: "Hey, is this token valid?"
async validateToken(token) {
const session = await db.sessions.findByToken(token);
return session !== null;
},
// 💡 You can also add ANY extra custom method you want!
async assignRole(userId, role) {
await db.users.updateRole(userId, role);
}
});The true power of NanoAuth lies in its declarative, plugin-first architecture. A Plugin is responsible for defining how your feature integrates with the auth lifecycle, what HTTP endpoints it needs, and what methods it exports.
To provide the best Developer Experience (DX), use the definePlugin helper.
A modern NanoAuth plugin consists of four main properties:
name: The unique identifier of the plugin.endpoints: Custom HTTP routes (e.g.,/api/auth/magic/verify) that the handler will process automatically.hooks: Listeners to lifecycle events.exports: Inject new stateless methods into theauthobject (e.g.,auth.signin.magicLink()). Crucially, exported methods must return{ user, token }to allow the handler to create the session cookes securely.
import { definePlugin } from 'nanoauth';
import { createHS256JWT } from 'nanoauth/utils';
export interface MagicLinkConfig {
sendEmail: (email: string, link: string) => Promise<void>;
generateToken: () => string;
}
export const magicLinkPlugin = definePlugin<MagicLinkConfig>((config) => ({
name: 'magic-link',
// 1. Export stateless methods to the Auth instance
exports: (auth) => ({
signin: {
magicLinkVerify: async (token: string) => {
// Validation logic here...
const user = await auth.adapter.getUserByToken(token);
// Return user and the newly generated session token!
// The NanoAuth core will automatically handle cookies based on this return.
const sessionToken = await createHS256JWT({ userId: user.id }, 'my-secret');
return { user, token: sessionToken };
}
}
}),
// 2. Register HTTP endpoints processed by auth.handler()
endpoints: {
verify: {
path: '/magic/verify',
method: 'GET',
handler: async (req, auth) => {
const url = new URL(req.url);
const token = url.searchParams.get('token');
// Here we call the exported method!
const { user, token: sessionToken } = await auth.signin.magicLinkVerify(token);
// Return a successful response, the handler handles the rest.
return new Response('Success!', { status: 200 });
}
}
},
// 3. React to core events statelessly
hooks: {
afterSignin: async ({ user, provider }) => {
console.log(`User ${user.email} signed in via ${provider || 'credentials'}`);
}
}
}));Weave your adapter and plugins together using the factory:
import { nanoauth, emailPasswordPlugin, sessionPlugin } from 'nanoauth';
export const auth = nanoauth<MyAwesomeUser>({
adapter: myAdapter,
secret: process.env.AUTH_SECRET,
plugins: [
sessionPlugin(),
emailPasswordPlugin({ ... })
]
});Since the backend is strictly stateless, you retrieve the current user context directly from the active HTTP Request:
// Inside any Next.js API Route, Hono Handler, or Bun Server:
const { user, token } = await auth.getSession(request);
if (!user) {
return new Response('Unauthorized', { status: 401 });
}
console.log(`Hello, ${user.name}`);Once you initialize nanoauth, the resulting auth instance provides the following core capabilities:
auth.getSession(request: Request): Promise<{ user, token }>Statelessly parses the request (cookies/headers) to validate the session. Returns the user object and the session token if active, ornullif unauthenticated.auth.handler(request: Request): Promise<Response>The Web Standard handler that automatically intercepts plugin endpoints (e.g./api/auth/signin/email) and executes them, handling cookies and redirects automatically.auth.use(plugin: Plugin)Allows dynamically attaching additional plugins to an existingauthinstance.auth.on(event, callback)Registers a dynamic listener for reactive events likeafterSignin,afterSignup,afterLogout, andonError.
defineAdapter<UserType>(config): An identity pass-through function just likedefineConfigin Vite. It gives you 100% autocompletion for Database Adapter methods (likegetUser,saveSession) and infers your custom genericUserType.definePlugin<Options, Exports, UserType>(factory): Creates a robust, fully-typed authentication plugin. It structures the plugin intoendpoints(for HTTP routing),hooks(for listening to core events), andexports(for attaching new methods to theauthinstance statelessly).
NanoAuth provides an ultra-lightweight React integration. Instead of handling fragmented states, you use a single unified hook.
import { createAuthClient } from 'nanoauth/react';
// 1. Initialize the client
export const auth = createAuthClient({
baseURL: 'http://localhost:3000'
});
// 2. Use the unified hook in your components!
export function Navbar() {
const { user, isLoading, signOut } = auth.useSession();
if (isLoading) return <span>Verifying identity...</span>;
return (
<nav>
{user ? (
<>
<span>Welcome, {user.name}</span>
<button onClick={signOut}>Sign Out</button>
</>
) : (
<a href="/signin">Sign In</a>
)}
</nav>
);
}
// 3. Simple API for actions
async function handleLogin() {
const { user } = await auth.signIn('email', { email, password });
console.log('Logged in as', user.name);
}Framework Agnostic? Yes! If you aren't using React, you can still use
nanoauth/clientwhich provides raw Nanostores (Atoms/Maps) to integrate with Vue, Svelte, or Vanilla JS.
Tired of writing /auth/login and /auth/signup controllers? Let our built-in Web Standard Handler process standard Fetch Request objects automatically.
// Example using Hono, but works the same in Next.js App Router, Cloudflare, or Bun!
import { Hono } from 'hono';
import { auth } from './auth'; // Your NanoAuth instance
const app = new Hono();
// Boom! 🔥 One line handles all your endpoints:
// POST /api/auth/signin/:provider
// POST /api/auth/signup/:provider
// POST /api/auth/signout
app.all('/api/auth/*', (c) => auth.handler(c.req.raw));
export default app;NanoAuth takes security seriously. The core functions and crypto utilities are tested exhaustively. Run the test suite:
bun run testWe love contributions! The core principle is keeping the main package dependency-free (or as close to zero as possible) while building powerful wrappers around it. Feel free to open a PR!
Built with ❤️ by fhorray