---
title: "Add sign-in to a page"
description: "Mount the sign-in routes with an adapter, then sign people in from the browser with passkeys, WhatsApp codes or an emailed link using the small client."
section: "guides"
status: "Planned for 0.4"
spec: "auth-client"
canonical: "https://mainsdk.dev/docs/guides/sign-in-page"
updated: "2026-10-28"
---

# Add sign-in to a page

> Mount the sign-in routes with an adapter, then sign people in from the browser with passkeys, WhatsApp codes or an emailed link using the small client.

Status: Planned for 0.4 · Canonical: https://mainsdk.dev/docs/guides/sign-in-page · Updated: 2026-10-28

You will let someone sign in from a web page with a passkey or a WhatsApp code, show who is signed in across tabs, and sign out. It takes about 20 minutes. The server side is an adapter; the page uses a client under 6 KB with no dependencies.

## You need

- A MainSDK app run from the repository, on Next.js, Hono or Express.
- `AUTH_KEY` in your secrets.

## 1. Turn on the methods

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

## 2. Mount the adapter

```ts title="server.ts"
import { Hono } from "hono";
import { mainsdk } from "mainsdk/hono";

const app = new Hono();
app.use("*", mainsdk());
```

You should see `GET /_mainsdk/auth/session` answer 401 with `MAIN-AUTH-0001` and `Cache-Control: no-store`.

## 3. Sign in from the page

```ts title="sign-in.ts"
import { createAuthClient } from "mainsdk/auth/client";

declare function render(session: unknown): void;   // your own
declare const codeFromTheInput: string;            // from your own <input>

const client = createAuthClient();

client.session.watch((s) => render(s));                      // this tab and every other tab

// Passkey, with browser autofill on <input autocomplete="username webauthn">
await client.passkeys.signIn({ mediation: "conditional" });

// WhatsApp code
const { challengeId } = await client.whatsapp.send({ phone: "+919876543210" });
await client.whatsapp.verify({ challengeId, code: codeFromTheInput });

await client.signOut();
```

`render` is your own function. A session never holds a token: the browser keeps the cookie and the client holds a CSRF value it sends on every change.

## 4. Handle step-up

When a sign-in needs a second check, the client throws `StepUpRequired`. Finish it:

```ts
import { createAuthClient, StepUpRequired } from "mainsdk/auth/client";

declare const challengeId: string;
declare const code: string;

const client = createAuthClient();

try {
  await client.whatsapp.verify({ challengeId, code });
} catch (err) {
  if (err instanceof StepUpRequired) {
    await client.stepUp.verify(err);          // passkey prompt
    // or: await client.stepUp.send(err); await client.stepUp.verify(err, { code });
  } else throw err;
}
```

## Verify

Open the page, sign in, reload: `client.session.get()` returns the same user. Sign out in one tab and the other tab's `watch` callback receives `null`.

## If it fails

| Code | Cause | Fix |
| --- | --- | --- |
| `MAIN-AUTH-0081` | The person cancelled the passkey prompt | Ask again from a click |
| `MAIN-AUTH-0082` | The routes answered 404 or HTML | Mount the adapter or set `base` |
| `MAIN-AUTH-0083` | This browser has no WebAuthn or conditional mediation | Offer the WhatsApp code |
| `MAIN-AUTH-0084` | The method is not in `auth.methods` | Add it to `main.yaml` |
| `MAIN-WEB-0002` | A cross-site POST | Serve the page from the same origin as the app |

## For agents

```text
Goal: add sign-in to this page using the MainSDK client.
Touch: main.yaml (auth.methods), the server entry (adapter), one client module.
Do not: store a session token in JavaScript, or write your own sign-in endpoint.
Verify: GET /_mainsdk/auth/session answers 401 signed out and 200 after sign-in.
Stop and ask if: no adapter exists for the framework.
```
