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
- Expose
GET /api/instant-work/agent-session. Any path works; set it in Board Details if you change it. - 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. - Compare the
x-instant-work-agent-secretrequest header with the variable, using a constant-time comparison. Reject a mismatch with 401. - 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.