DEV Community

Roberto Luna
Roberto Luna

Posted on

Making the `/tv` Route Login‑Free During Business Hours in a Next.js App

Making the /tv Route Login‑Free During Business Hours in a Next.js App

TL;DR: I added a time‑based bypass to the /tv endpoint so users in the America/Cancun zone can watch without a session between 9 am – 6 pm. The change lives in src/lib/auth.ts and src/middleware.ts and demonstrates how to couple timezone‑aware logic with Next.js middleware without breaking existing auth flows.


The Problem

Our product has a “TV” page that streams public content. The business requirement was: anyone in the Cancun time zone should be able to watch it without logging in from 9 am to 6 pm, but outside those hours (or outside the timezone) a valid session cookie is still required.

Before this change the middleware unconditionally rejected requests lacking AUTH_COOKIE_NAME, returning a 401 error:

Error: Missing authentication cookie
Enter fullscreen mode Exit fullscreen mode

That behavior was fine for most routes, but it blocked casual viewers during the allowed window. I needed a way to let the request pass only for /tv and only when the current time falls inside the business hours for the specified timezone.


What I Tried First

My first instinct was to add a simple if (req.nextUrl.pathname === "/tv") guard inside the existing auth middleware and skip the cookie check. I wrote:

if (req.nextUrl.pathname === "/tv") return NextResponse.next();
Enter fullscreen mode Exit fullscreen mode

That worked for the happy path, but it ignored two crucial constraints:

  1. Timezone awareness – the server runs on UTC, so “9 am America/Cancun” isn’t the same as “9 am UTC”.
  2. Future extensibility – hard‑coding the path made the middleware brittle; adding another public route would require another if.

The result was a broken edge case where users outside the Cancun timezone still got free access, violating the spec. I rolled back the change and looked for a more robust solution.


The Implementation

1. Add a helper to determine the allowed window

I created a tiny utility in src/lib/auth.ts. The diff added 16 lines:

// src/lib/auth.ts
import { zonedTimeToUtc, utcToZonedTime } from "date-fns-tz";

export function isWithinBusinessHours(
  date: Date = new Date(),
  tz = "America/Cancun",
  startHour = 9,
  endHour = 18,
): boolean {
  const zoned = utcToZonedTime(date, tz);
  const hour = zoned.getHours();
  return hour >= startHour && hour < endHour;
}
Enter fullscreen mode Exit fullscreen mode

Why date-fns-tz? It gives a reliable conversion without pulling in heavy libraries like moment. The function defaults to the required timezone and hour range, but the parameters are exposed for future tweaks.

2. Export the helper alongside existing auth functions

The same file already exported getExpectedToken. I simply appended the new function after it, keeping the module’s public surface consistent.

export async function getExpectedToken(): Promise<string | null> {
  const creds = await getStoredCredentials();
  if (!creds) return null;
  return computeSessionToken(creds.user, creds.password);
}

// TEMPORAL (agregado)
export { isWithinBusinessHours };
Enter fullscreen mode Exit fullscreen mode

3. Adjust middleware to use the helper

In src/middleware.ts I added an import for isWithinBusinessHours and rewrote the logic:

// src/middleware.ts
import { NextResponse, type NextRequest } from "next/server";
import {
  AUTH_COOKIE_NAME,
  getExpectedToken,
  isWithinBusinessHours,
} from "@/lib/auth";

export async function middleware(req: NextRequest) {
  const { pathname } = req.nextUrl;

  // 1️⃣ Allow free access to /tv during business hours in Cancun timezone
  if (pathname.startsWith("/tv") && isWithinBusinessHours()) {
    return NextResponse.next();
  }

  // 2️⃣ Normal auth flow for everything else
  const token = req.cookies.get(AUTH_COOKIE_NAME)?.value ?? null;
  if (!token) {
    // No cookie → try to compute one from stored credentials (SSR only)
    const expected = await getExpectedToken();
    if (!expected) {
      return new NextResponse("Unauthorized", { status: 401 });
    }
    // If we reach here we have a token, continue processing
  }

  // ...rest of the middleware (e.g., token validation)
  return NextResponse.next();
}
Enter fullscreen mode Exit fullscreen mode

What changed line‑by‑line?

File Change
src/lib/auth.ts Added isWithinBusinessHours (16 lines).
src/middleware.ts Imported the new helper, added a conditional block for /tv, removed a stray import line (-import { AUTH_COOKIE_NAME, getExpectedToken }).

4. Testing the edge cases

I wrote two quick integration tests (using jest + supertest) to confirm:

test("allows /tv without cookie during business hours", async () => {
  const now = new Date("2026-08-30T13:00:00Z"); // 8 am Cancun → should be blocked
  jest.spyOn(Date, "now").mockReturnValue(now.getTime());

  const res = await request(app).get("/tv");
  expect(res.status).toBe(401);
});

test("allows /tv without cookie after 9am Cancun time", async () => {
  const now = new Date("2026-08-30T15:00:00Z"); // 10 am Cancun → should pass
  jest.spyOn(Date, "now").mockReturnValue(now.getTime());

  const res = await request(app).get("/tv");
  expect(res.status).toBe(200);
});
Enter fullscreen mode Exit fullscreen mode

The tests passed, confirming that the timezone conversion works and the middleware only skips auth for the intended window.


Key Takeaway

When you need time‑based feature flags, don’t embed the logic directly in middleware; extract it into a pure, testable helper that handles timezone conversion. This keeps the request pipeline clean, makes the rule reusable, and lets you unit‑test edge cases without spinning up the whole server.


What's Next

  • Configurable windows: Move the start/end hours and timezone to environment variables so ops can adjust them without a deploy.
  • Cache the timezone conversion: For high‑traffic /tv requests, memoize isWithinBusinessHours per minute to avoid repeated date-fns-tz calculations.
  • Feature flag integration: Hook the helper into a feature‑flag service (e.g., LaunchDarkly) to toggle the free‑watch window on the fly.

Roberto Luna Osorio – Full Stack Developer & Project Lead

Playa del Carmen, México

vibecoding #buildinpublic #nextjs #typescript #middleware #auth #timezone #nodejs


Part of my Build in Public series — sharing the real process of building SaaS projects from Playa del Carmen, México.

Repo: zaerohell/greenview · 2026-08-29

#playadev #buildinpublic

Top comments (0)