MainSDK
Search⌘K
How-to

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.

Planned for 0.4

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 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

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

2. Mount the adapter

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

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

CodeCauseFix
MAIN-AUTH-0081The person cancelled the passkey promptAsk again from a click
MAIN-AUTH-0082The routes answered 404 or HTMLMount the adapter or set base
MAIN-AUTH-0083This browser has no WebAuthn or conditional mediationOffer the WhatsApp code
MAIN-AUTH-0084The method is not in auth.methodsAdd it to main.yaml
MAIN-WEB-0002A cross-site POSTServe 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.

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet