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 InstantWork's agent sign in as a dedicated account on any preview, whatever your framework or auth library.

The contract

  1. Expose GET /api/instant-work/agent-session. Any path works; set it in Board Details if you change it.
  2. Read the environment variable INSTANT_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-instant-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. InstantWork 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.

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/instant-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.INSTANT_WORK_AGENT_SESSION_SECRET;
  if (!secret) return new NextResponse("Not found", { status: 404 });
  const given = request.headers.get("x-instant-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/instant-work/agent-session", async (req, res) => {
  const secret = process.env.INSTANT_WORK_AGENT_SESSION_SECRET;
  if (!secret) return res.status(404).end();
  const given = req.get("x-instant-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.instant-work.agent-session.ts
export async function loader({ request }: LoaderFunctionArgs) {
  const secret = process.env.INSTANT_WORK_AGENT_SESSION_SECRET;
  if (!secret) throw new Response("Not found", { status: 404 });
  if (!safeEqual(request.headers.get("x-instant-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/instant-work/agent-session.get.ts
export default defineEventHandler(async (event) => {
  const secret = process.env.INSTANT_WORK_AGENT_SESSION_SECRET;
  if (!secret) throw createError({ statusCode: 404 });
  if (!safeEqual(getHeader(event, "x-instant-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.
  • InstantWork'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.