Skip to content

Agent sign-in route

Before-and-after screenshots compare your production site with a pull request preview. Previews live on a different host, so a browser session from production does not carry over. One small route in your app lets HPA Work's agent sign in as a dedicated account on any preview, whatever your framework or auth library.

The contract

  1. Expose GET /api/hpa-work/agent-session. Any path works; set it in Board Details if you change it.
  2. Read the environment variable HPA_WORK_AGENT_SESSION_SECRET. Define it only in your preview environment. Where it is missing, the route must answer 404 and do nothing.
  3. Compare the x-hpa-work-agent-secret request header with the variable, using a constant-time comparison. Reject a mismatch with 401.
  4. On a match, sign in a dedicated agent account with your normal auth library, creating the account if it does not exist, set your usual session cookie, and redirect to a signed-in page such as /.

Paste the same secret into Board Details → UI screenshots → Agent sign-in. HPA Work stores it encrypted and sends it only as a header on the single request to your route, on preview hosts. The browser then holds a genuine session and captures pages the way a signed-in user sees them. The model that plans screenshots never receives the secret.

To capture the production "before" signed in as well, define the same variable in production for a read-only agent account and enable Also sign in on production. Leave it off unless you have done both.

Set this up when the product was called Instant Work? Your route keeps working: HPA Work also sends the secret as x-instant-work-agent-secret, and the route path saved in Board Details doesn't change.

Recipes

The shape is the same everywhere: guard on the variable, compare the header, create your session, redirect. Adapt createSessionForAgent to your auth library.

Next.js App Router

// app/api/hpa-work/agent-session/route.ts
import { NextResponse } from "next/server";
import { timingSafeEqual } from "node:crypto";

export async function GET(request: Request) {
  const secret = process.env.HPA_WORK_AGENT_SESSION_SECRET;
  if (!secret) return new NextResponse("Not found", { status: 404 });
  const given = request.headers.get("x-hpa-work-agent-secret") ?? "";
  const ok = given.length === secret.length && timingSafeEqual(Buffer.from(given), Buffer.from(secret));
  if (!ok) return new NextResponse("Unauthorized", { status: 401 });

  const response = NextResponse.redirect(new URL("/", request.url));
  // Your auth library here: find or create the agent user, then set its session cookie on `response`.
  await createSessionForAgent(response);
  return response;
}

Express

app.get("/api/hpa-work/agent-session", async (req, res) => {
  const secret = process.env.HPA_WORK_AGENT_SESSION_SECRET;
  if (!secret) return res.status(404).end();
  const given = req.get("x-hpa-work-agent-secret") ?? "";
  if (given.length !== secret.length || !crypto.timingSafeEqual(Buffer.from(given), Buffer.from(secret))) return res.status(401).end();
  const user = await findOrCreateAgentUser();
  req.session.userId = user.id;          // express-session, Passport (req.login), or your own cookie
  res.redirect("/");
});

Remix / React Router

// app/routes/api.hpa-work.agent-session.ts
export async function loader({ request }: LoaderFunctionArgs) {
  const secret = process.env.HPA_WORK_AGENT_SESSION_SECRET;
  if (!secret) throw new Response("Not found", { status: 404 });
  if (!safeEqual(request.headers.get("x-hpa-work-agent-secret") ?? "", secret)) throw new Response("Unauthorized", { status: 401 });
  const user = await findOrCreateAgentUser();
  const session = await sessionStorage.getSession();
  session.set("userId", user.id);
  return redirect("/", { headers: { "Set-Cookie": await sessionStorage.commitSession(session) } });
}

Nuxt

// server/api/hpa-work/agent-session.get.ts
export default defineEventHandler(async (event) => {
  const secret = process.env.HPA_WORK_AGENT_SESSION_SECRET;
  if (!secret) throw createError({ statusCode: 404 });
  if (!safeEqual(getHeader(event, "x-hpa-work-agent-secret") ?? "", secret)) throw createError({ statusCode: 401 });
  const user = await findOrCreateAgentUser();
  await setUserSession(event, { user: { id: user.id } });   // nuxt-auth-utils, or set your own cookie
  return sendRedirect(event, "/");
});

Hosted auth providers such as Clerk, Auth0 or Supabase usually offer a server-side way to mint a session for a known user, for example a sign-in token or an admin API. Use that inside the route; the header check and environment guard stay the same.

Security notes

  • The variable is the switch. Do not define it in production unless you deliberately want signed-in production screenshots with a read-only account.
  • Give the agent account the least access that still shows the pages you want captured. Screenshots are posted to pull requests and visible to everyone who can see the repository.
  • Rotate the secret by changing the variable and pasting the new value into Board Details.
  • HPA Work's browser never submits forms during a capture and blocks navigation away from your site. It does not type into your pages.

See also the Vercel integration for deployment protection and Support.