Extending the Login Cookie to a One‑Year Session for Kiosk‑Mode TV Screens in a Next.js API Route
TL;DR: I changed the maxAge of the authentication cookie from 30 days to 365 days so that TV kiosks stay logged in without manual refresh. The change is a one‑line edit in src/app/api/login/route.ts, but I added type safety, comments, and a quick test to verify the new lifespan.
The Problem
Our TV‑screen kiosk runs a thin client that points to the same Next.js app used by desktop users. In production the login endpoint creates a Set‑Cookie header with the following options:
cookie: {
httpOnly: true,
secure: true,
sameSite: "lax",
path: "/",
maxAge: 60 * 60 * 24 * 30, // 30 days
}
After a month the kiosk automatically logs out, forcing a manual re‑login. Because the device is mounted behind a wall and rarely accessed, this is a usability nightmare. The symptom we see in the logs is a 401 Unauthorized after exactly 30 days of continuous uptime.
What I Tried First
My first instinct was to bump the maxAge value in the client‑side code that consumes the cookie, thinking the server would respect the new value. I added a maxAge param to the fetch call:
fetch("/api/login", {
method: "POST",
body: JSON.stringify(payload),
credentials: "include",
maxAge: 60 * 60 * 24 * 365, // <-- wrong place
});
That obviously did nothing—the server still sent the original 30‑day cookie because the Set‑Cookie header is generated on the server side. The request succeeded, but the cookie expiration stayed at 30 days. I also tried to replace maxAge with expires: new Date(Date.now() + ONE_YEAR_MS), but the next/response helper only accepts maxAge.
The Implementation
The fix lives entirely in the API route that issues the cookie. Below is the diff from commit f3b83c16:
--- a/src/app/api/login/route.ts
+++ b/src/app/api/login/route.ts
@@ -23,7 +23,7 @@ export async function POST(req: Request) {
secure: true,
sameSite: "lax",
path: "/",
- maxAge: 60 * 60 * 24 * 30,
+ // Extend session to 1 year for kiosk‑mode TV screens.
+ // 60s * 60m * 24h * 365d = 31,536,000 seconds
+ maxAge: 60 * 60 * 24 * 365, // 1 year — pensad
});
Full file context
// src/app/api/login/route.ts
import { NextResponse } from "next/server";
import { verifyUser } from "@/lib/auth";
import { setLoginCookie } from "@/lib/cookies";
export async function POST(req: Request) {
const { email, password } = await req.json();
// Authenticate against DB / third‑party provider
const user = await verifyUser(email, password);
if (!user) {
return NextResponse.json({ error: "Invalid credentials" }, { status: 401 });
}
// Create a JWT (or session token) – omitted for brevity
const token = await createSessionToken(user.id);
// Set the authentication cookie
const response = NextResponse.json({ success: true });
setLoginCookie(response, token); // <-- implementation lives in lib/cookies.ts
return response;
}
Cookie helper (src/lib/cookies.ts)
I kept the cookie creation logic isolated so we could tweak it without touching the route again:
// src/lib/cookies.ts
import { NextResponse } from "next/server";
export function setLoginCookie(
res: ReturnType<typeof NextResponse.json>,
token: string
) {
// NOTE: maxAge is expressed in **seconds**.
// For kiosk‑mode we need a 1‑year lifespan.
const ONE_YEAR = 60 * 60 * 24 * 365; // 31,536,000 seconds
res.cookies.set("auth_token", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
path: "/",
maxAge: ONE_YEAR,
});
}
Why the change matters
-
Units:
maxAgeexpects seconds, not milliseconds. A common pitfall is to multiply by1000(as you would forDate.now()), which would set the cookie to ~365 000 seconds (≈4 days). By definingONE_YEARexplicitly we avoid that mistake. -
Security: The cookie remains
httpOnlyandsecure. Extending the lifetime does not relax those flags, but we must be aware that a longer‑lived token increases the attack surface if the token is ever leaked. -
Environment safety: The
secureflag is toggled based onNODE_ENV. In local dev the cookie is still sent over HTTP, which is useful for debugging.
Automated verification
I added a tiny Jest test to ensure the cookie’s maxAge matches the constant:
ts
// __tests__/cookies.test.ts
import { NextResponse } from "next/server";
import { setLoginCookie } from "@/lib/cookies";
test("sets a 1‑year maxAge on auth_token", () => {
const res = NextResponse.json({ ok: true });
setLoginCookie(res, "dummy-token");
const cookie = res.headers.get("set-cookie")!;
---
*Part of my [Build in Public](https://dev.to/zaerohell) series — sharing the real process of building SaaS projects from Playa del Carmen, México.*
*Repo: `zaerohell/tvview` · 2026-08-20*
\#playadev #buildinpublic
Top comments (0)