---
title: "Require sign-in on your routes"
description: "Turn on passkeys and WhatsApp codes, guard a route with auth.require, read the session, and test the signed-out and signed-in cases."
section: "guides"
status: "Building for 0.3"
spec: "auth"
canonical: "https://mainsdk.dev/docs/guides/require-sign-in"
updated: "2026-10-28"
---

# Require sign-in on your routes

> Turn on passkeys and WhatsApp codes, guard a route with auth.require, read the session, and test the signed-out and signed-in cases.

Status: Building for 0.3 · Canonical: https://mainsdk.dev/docs/guides/require-sign-in · Updated: 2026-10-28

You will turn on two sign-in methods, guard one route and test it. It takes about 15 minutes. To build the sign-in page itself, see [add sign-in to a page](https://mainsdk.dev/docs/guides/sign-in-page).

## You need

- A MainSDK app run from the repository.
- `AUTH_KEY` in your secrets. It signs sessions and is required in production.

## 1. Turn on the methods

```yaml title="main.yaml"
auth:
  key: secret:AUTH_KEY
  methods: [passkeys, whatsapp]
  passkeys:
    origins: ["https://app.example.com"]
```

## 2. Guard a route

```ts title="app/api/me/route.ts"
import { auth, problem } from "mainsdk";

export async function GET(req: Request) {
  try {
    const session = await auth.require(req);   // throws MAIN-AUTH-0001 (401) if not signed in
    return Response.json({ user: session.user, aal: session.aal });
  } catch (err) {
    return problem(err);                       // RFC 9457 body with code and fix
  }
}
```

Where signing in is optional, call `auth.session(req)`: it returns the session or `null`.

## 3. Test both cases

```ts title="app/api/me/route.test.ts"
import { it } from "node:test";
import assert from "node:assert/strict";
import { createTestApp } from "@mainsdk/testing";
import { auth, problem } from "mainsdk";

// the handler under test, as in app/api/me/route.ts
async function GET(req: Request) {
  try {
    const session = await auth.require(req);
    return Response.json({ user: session.user });
  } catch (err) {
    return problem(err);
  }
}

it("auth AC1 refuses a request with no session and accepts one", async () => {
  createTestApp({ yaml: "schema: 1\nproject: shop\nauth:\n  methods: [password]\n" });
  const none = await GET(new Request("http://localhost/api/me"));
  assert.equal(none.status, 401);

  const issued = await auth.passwords.register(new Request("http://localhost/"), {
    email: "jane@x.com",
    password: "correct horse battery",
  });
  const cookie = issued.cookies.map((c) => c.split(";")[0]).join("; ");
  const ok = await GET(new Request("http://localhost/api/me", { headers: { cookie } }));
  assert.equal(ok.status, 200);
});
```

## Check it worked

Without a cookie the route answers 401 with `MAIN-AUTH-0001` and a `fix`. With a valid session it answers 200. The test passes.

## Troubleshooting

- 401 on every call in production: check that `AUTH_KEY` resolves and that the browser sends the session cookie.
- `MAIN-AUTH-0084`: you called a sign-in route for a method `auth.methods` does not list.
- A passkey fails on one domain: add its origin under `auth.passkeys.origins`.

See the [auth interface](https://mainsdk.dev/docs/interfaces/auth) for step-up, organisations and single sign-on.
