Skip to content

Guest Login

ideal-auth doesn’t ship a dedicated guest mode, but you can build one cleanly with the same primitives you already use for authenticated users. A guest session is just a session whose uid belongs to an anonymous identity rather than a registered user.

This guide covers two patterns — pick the one that matches whether guest activity needs to persist server-side.


Best for ephemeral state: shopping carts, UI preferences, A/B test buckets, draft form data. No database row, no migration, no cleanup job.

Use sessionFields mode so the guest’s identity lives entirely in the signed cookie:

import { createAuth } from 'ideal-auth';
type SessionUser = {
id: string;
role: 'guest' | 'user';
email?: string;
};
export const auth = createAuth<SessionUser>({
secret: process.env.IDEAL_AUTH_SECRET!,
cookie: cookieBridge,
sessionFields: ['id', 'role', 'email'],
});

Issue a guest session anywhere you’d normally redirect to login:

const session = auth();
if (!(await session.check())) {
await session.login({
id: crypto.randomUUID(),
role: 'guest',
});
}

Read the role like any other field:

const user = await session.user();
if (user?.role === 'guest') {
// gate features, show signup CTA, etc.
}

Drop this into middleware so every visitor gets a session id immediately — useful for analytics and cart-before-signup flows.

middleware.ts
import { NextResponse, type NextRequest } from 'next/server';
import { auth } from '@/lib/auth';
import { nextCookieBridge } from '@/lib/cookie-bridge';
export async function middleware(req: NextRequest) {
const res = NextResponse.next();
const session = auth({ cookie: nextCookieBridge(req, res) });
if (!(await session.check())) {
await session.login({
id: crypto.randomUUID(),
role: 'guest',
});
}
return res;
}
export const config = {
matcher: ['/((?!_next|api/auth).*)'],
};

Best when guest activity must survive a logout, get audited, or join other tables (orders, reservations, support tickets). Create a real user row marked as a guest, then login() as that user.

import { db } from '@/lib/db';
async function createGuestUser() {
return db.user.create({
data: {
role: 'guest',
email: null,
password: null,
},
});
}
const session = auth();
if (!(await session.check())) {
const guest = await createGuestUser();
await session.login(guest);
}

Your resolveUser callback already handles the lookup on subsequent requests — no changes needed.


The clean trick: keep the same id. Don’t logout() then login() — that would orphan any data already keyed to the guest id.

Re-login() with the upgraded user shape. The cookie is overwritten in place; the id you carry forward is up to you:

async function signup(email: string, password: string) {
const session = auth();
const guest = await session.user();
const guestId = guest?.id;
const user = await db.user.create({
data: {
id: guestId, // reuse so cart/etc. keyed to guestId still resolves
email,
password: await hash.make(password),
role: 'user',
},
});
await session.login(user);
}

Update the existing row instead of creating a new one. Same uid, same session — no re-login needed for routing, but login() again to refresh the cookie payload:

async function signup(email: string, password: string) {
const session = auth();
const guestId = await session.id();
if (!guestId) throw new Error('No guest session to upgrade');
const user = await db.user.update({
where: { id: guestId },
data: {
email,
password: await hash.make(password),
role: 'user',
},
});
await session.login(user);
}

Carts, draft posts, and anything else with a userId foreign key now belongs to the real account.


The session payload tells you everything you need:

const user = await session.user();
if (!user) {
// Truly unauthenticated — middleware hasn't issued a guest session yet
}
if (user?.role === 'guest') {
// Show signup prompt, limit features, etc.
}
if (user?.role === 'user') {
// Full account
}

A guard helper keeps route handlers tidy:

export async function requireRealUser() {
const user = await auth().user();
if (!user || user.role === 'guest') {
throw new Response('Sign in required', { status: 401 });
}
return user;
}

  • Guests can do everything sessions can. Treat role: 'guest' as a less-privileged user, not as “unauthenticated.” Apply CSRF protection, rate limits, and authorization checks just like any other session.

  • Don’t let guests escalate. Verify the role on every server-side mutation. Never trust the role field returned by the client — read it from session.user().

  • Watch for guest-row spam. Pattern 2 lets unauthenticated traffic write to your users table. Rate-limit guest creation by IP, and consider deferring row creation until the guest performs a stateful action.

  • Use shorter cookie lifetimes for guests. Optional, but reduces the value of stealing a guest cookie. Pass { remember: false } to login() so the cookie expires when the browser closes:

    await session.login({ id: crypto.randomUUID(), role: 'guest' }, { remember: false });

  • You only need anonymous analytics. Use a tracking cookie, not a session. Sessions are for state your server reads on every request.
  • You’re tempted to use guests as API keys. Issue real tokens via createTokenVerifier instead. Sessions are designed for browsers.
  • You want a “skip login” button that does nothing. If the guest can’t do anything the unauthenticated user couldn’t, you don’t need the session.