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.
Pattern 1 — Cookie-only guest
Section titled “Pattern 1 — Cookie-only guest”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.}Auto-issuing on first visit
Section titled “Auto-issuing on first visit”Drop this into middleware so every visitor gets a session id immediately — useful for analytics and cart-before-signup flows.
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).*)'],};import { createMiddleware } from '@tanstack/start';import { auth } from '~/lib/auth';
export const guestMiddleware = createMiddleware().server(async ({ next }) => { const session = auth(); if (!(await session.check())) { await session.login({ id: crypto.randomUUID(), role: 'guest', }); } return next();});Pattern 2 — Database-backed guest
Section titled “Pattern 2 — Database-backed guest”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.
Upgrading a guest to a real account
Section titled “Upgrading a guest to a real account”The clean trick: keep the same id. Don’t logout() then login() — that would orphan any data already keyed to the guest id.
Cookie-only guests
Section titled “Cookie-only guests”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);}Database-backed guests
Section titled “Database-backed guests”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.
Distinguishing guests in your app
Section titled “Distinguishing guests in your app”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;}Security notes
Section titled “Security notes”-
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
userstable. 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 }tologin()so the cookie expires when the browser closes:await session.login({ id: crypto.randomUUID(), role: 'guest' }, { remember: false });
When not to use guest login
Section titled “When not to use guest login”- 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
createTokenVerifierinstead. 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.