← Back to blog

Updated 2 October 2026

Use Own Auth with the Next.js App Router


Why Own Auth with Next.js App Router

The Next.js App Router introduces a new mental model for building server side applications. Instead of API routes and getServerSideProps, you work with React Server Components, Server Actions, and route handlers that execute on the server. This shift changes how you implement authentication. According to the Next.js authentication guide, server side authentication should place cookies and authorization checks close to your data source, keeping sensitive logic where it belongs.

Own Auth fits naturally into this architecture. It is a framework independent authentication package that keeps authentication data in your own Postgres database. Rather than delegating user records to a hosted vendor, you maintain complete ownership of your users, sessions, credentials, and audit logs. This aligns with the App Router philosophy of keeping your data and business logic on the server.

Setting Up Own Auth in a Next.js Project

First, install the Own Auth package from npm. You will also need a Postgres database connection string. In your Next.js project, create an auth configuration file that initializes Own Auth with your database connection. The package uses the DATABASE_URL environment variable by default, but you can configure custom storage adapters if needed.

lib/auth.ts
import { createOwnAuth, defineOwnAuthConfig } from 'own-auth';
import { InMemoryAuthStorage } from 'own-auth';

const config = defineOwnAuthConfig({
  emailVerification: {
    enabled: true,
    expirySeconds: 3600,
  },
  passwordReset: {
    enabled: true,
    expirySeconds: 3600,
  },
  magicLink: {
    enabled: true,
    expirySeconds: 300,
  },
});

export const auth = createOwnAuth(config);

This configuration enables email verification, password reset, and magic link flows. The expiry seconds control how long each one time token remains valid. You can adjust these values based on your security requirements and user experience preferences.

Creating Sign Up and Sign In Routes

Next.js route handlers handle HTTP requests. For sign up, you create an API route that receives user input, validates it, and calls the appropriate Own Auth method. The sign in flow is slightly more complex because it may return an MFA challenge if the user has enabled multi factor authentication.

app/api/auth/signup/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/lib/auth';

export async function POST(request: NextRequest) {
  const body = await request.json();
  const { email, password } = body;

  const result = await auth.signUpEmailPassword({
    email,
    password,
  });

  if (result.status === 'success') {
    return NextResponse.json({
      userId: result.user.id,
      email: result.user.email,
    });
  }

  return NextResponse.json(
    { error: result.error?.message || 'Sign-up failed' },
    { status: 400 }
  );
}

The sign up route returns a user object with safe client fields. The returned User type contains only the id and email properties that your frontend needs. Internal implementation details and password hashes never leave the server.

Handling MFA Challenges in Sign In

When a user signs in, the response may indicate that an MFA challenge is required. According to the Next.js authentication guide, you should handle authentication states explicitly rather than treating them as errors. The mfa_required status represents a successful first factor that now needs a second factor.

app/api/auth/signin/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/lib/auth';

export async function POST(request: NextRequest) {
  const body = await request.json();
  const { email, password } = body;

  const result = await auth.signInEmailPassword({
    email,
    password,
  });

  if (result.status === 'mfa_required') {
    return NextResponse.json({
      status: 'mfa_required',
      challengeToken: result.challengeToken,
      methods: result.methods,
      expiresAt: result.expiresAt,
    });
  }

  if (result.status === 'success') {
    const session = result.session;
    return NextResponse.json({
      userId: session.user.id,
      email: session.user.email,
      sessionToken: session.token,
    });
  }

  return NextResponse.json(
    { error: result.error?.message || 'Sign-in failed' },
    { status: 401 }
  );
}

This route explicitly handles the mfa_required status by returning the challengeToken, available MFA methods, and expiration timestamp. Your frontend uses this information to prompt the user for their second factor. Only after completing MFA does the user receive a session token.

Managing Sessions with Cookies

The Next.js cookies API reads request cookies in Server Components and writes response cookies in Server Functions or Route Handlers. Own Auth creates a session token after successful authentication, and you store this token in an HTTP cookie. The MDN HTTP cookies guide documents cookie attributes like HttpOnly, Secure, and SameSite that protect your session.

app/api/auth/signin/route.ts
import { cookies } from 'next/headers';
import { createSessionCookie } from 'own-auth';

// After successful sign-in, create session cookie
const sessionCookie = await createSessionCookie(result.session.token, {
  httpOnly: true,
  secure: process.env.NODE_ENV === 'production',
  sameSite: 'lax',
  maxAge: 60 * 60 * 24 * 7, // 7 days
});

cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.options);

Setting httpOnly prevents JavaScript on the client from reading the session token. The secure flag ensures the cookie is only sent over HTTPS in production. The sameSite attribute restricts cross site requests, mitigating CSRF attacks. These attributes are essential for secure session management in the App Router.

Protecting Routes with Server Components

One advantage of the App Router is that you can check authentication directly in Server Components. Import the getCurrentSession method and redirect unauthenticated users. This keeps authorization logic where your data lives, matching the Next.js recommendation to place checks close to the data source.

app/dashboard/page.tsx
import { redirect } from 'next/navigation';
import { auth } from '@/lib/auth';
import { cookies } from 'next/headers';

export default async function DashboardPage() {
  const cookieStore = await cookies();
  const session = await auth.getCurrentSession(cookieStore);

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

  return (
    <main>
      <h1>Welcome, {session.user.email}</h1>
      <p>Your account is protected with {session.assuranceLevel} assurance.</p>
    </main>
  );
}

This Server Component checks the session on every request. If no valid session exists, it redirects to the sign in page. The session object contains safe client fields including the user id, email, and assurance level. Your application never exposes raw database records to the client.

Revoking Sessions and Signing Out

When users sign out, you must revoke their session and clear the cookie. Own Auth provides the revokeSession method for invalidating sessions in the database. Clear the session cookie by setting its value to an empty string and its maxAge to zero.

app/api/auth/signout/route.ts
import { NextResponse } from 'next/server';
import { auth } from '@/lib/auth';
import { clearSessionCookie, readSessionToken } from 'own-auth';
import { cookies } from 'next/headers';

export async function POST() {
  const cookieStore = await cookies();
  const token = cookieStore.get('session')?.value;

  if (token) {
    const payload = readSessionToken(token);
    if (payload?.sessionId) {
      await auth.revokeSession(payload.sessionId);
    }
  }

  const cleared = clearSessionCookie();
  cookieStore.set(cleared.name, '', cleared.options);

  return NextResponse.json({ success: true });
}

This route handler revokes the session in your Postgres database and instructs the browser to delete the cookie. The combination of database revocation and cookie clearing ensures that the session cannot be reused.

Database Backed Sessions for Security

Own Auth stores sessions in your Postgres database rather than in memory or JWT encoded tokens. Database backed sessions support absolute expiration, optional idle timeout, listing active sessions, individual revocation, and full user revocation. This gives you operational control that JWTs cannot match. When a user reports a compromised device, you can revoke that specific session immediately without affecting other devices.

The session table also stores an assurance level that indicates how strongly the user has proven their identity. A password only sign in might yield lower assurance than a sign in completed with TOTP or a passkey. Your application can use this level to gate access to sensitive features.

Conclusion

The Next.js App Router and Own Auth work well together because both prioritize server side control. You keep authentication data in your own Postgres database, implement routes with proper MFA handling, manage sessions through HTTP cookies with appropriate security attributes, and protect pages directly in Server Components. This architecture gives you full ownership of user data while leveraging modern Next.js patterns for a responsive, secure application.

Frequently Asked Questions

How does Own Auth handle MFA in Next.js?

When auth.signInEmailPassword returns status mfa_required, the response includes a challengeToken, available methods array, and expiresAt timestamp. Your frontend prompts the user for their second factor and sends it to a verification endpoint that calls auth.completeMfaWithTotp or auth.completeMfaWithRecoveryCode.

What cookie attributes should I use for sessions?

Set httpOnly to prevent XSS attacks, secure to true in production for HTTPS only transmission, and sameSite to 'lax' or 'strict' to mitigate CSRF. Use maxAge or expires to control session duration.

Can I use Own Auth with Next.js Server Actions?

Yes. You can call auth methods directly in Server Actions. They run on the server and can read cookies from the incoming request, create new sessions, and return data to your client components.

Does Own Auth support OAuth providers like Google and Apple?

Yes. The package includes GoogleOAuthOptions, GitHubOAuthOptions, and AppleOAuthOptions for configuring redirect based OAuth flows. You verify the external identity through the backend rather than client side tokens.