MainSDK
Search⌘K
How-to

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.

Building for 0.3

Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.

Stability The promise an interface ships with.

  • Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
  • Beta Usable in production; may change before it becomes stable.
  • Experimental May change or go away; needs an explicit opt-in in main.yaml.
Stability levels
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.

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

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

2. Guard a route

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

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 for step-up, organisations and single sign-on.

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet