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 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.
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_KEYin your secrets.
1. Turn on the methods
auth:
key: secret:AUTH_KEY
methods: [passkeys, whatsapp]
passkeys:
origins: ["https://app.example.com"]2. Mount the adapter
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
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:
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
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?