Thanks to visit codestin.com
Credit goes to isitme.dev

Documentation

API reference and guide for isitme. Everything you need to add passkey authentication to your app.

npm i isitme

How it works

You add isitme to your app, and your pages are protected by passkey authentication. No user accounts, no passwords, no OAuth, no database. When you first visit, you register a passkey and become the owner — everyone else is locked out. Authentication uses your device's biometrics (fingerprint, Face ID) via the WebAuthn standard.

Simplest example

Wrap any component in <IsItMe> and it only renders for authenticated users:

your app
import { IsItMe } from "isitme/react";

<IsItMe>
  <h1>Admin dashboard</h1>
  <p>Secret tools here...</p>
</IsItMe>

// Not you? → shows passkey registration / login
// You?     → renders your content

By default, unauthenticated visitors see a built-in login / registration page. Use fallback={false} to hide content silently instead — great for debug UI, admin tools, or anything you want quick access to but hidden from regular visitors:

debug UI
<IsItMe fallback={false}>
  <DebugPanel />
  <PerformanceMonitor />
  <FeatureFlagEditor />
</IsItMe>

<main>
  {/* your regular app — always visible */}
</main>

Nothing sensitive — just stuff you want to keep out of the way for visitors and have easy access to yourself. The content only renders after you authenticate with your passkey, and disappears completely for everyone else. If you need to protect server routes and API endpoints too, add the server middleware:

Protecting server routes

There are two layers of auth — data gating (server middleware) and UI gating (React/Vue components):

  • Data gating: protectedPaths blocks unauthenticated requests at the server. Use for API routes that return sensitive data. Returns 401 JSON.
  • UI gating: <IsItMe> wraps your page component and shows a login/register form until authenticated. The page HTML always loads — auth happens client-side via conditional rendering.

Use protectedPaths for API routes. Don't put page paths there — the middleware would block the HTML before your <IsItMe> component can load. Unprotected routes still get req.isAuthenticated for conditional UI:

your server
// Protect API routes on the server — returns 401 for unauthenticated requests
app.use(isitme({
  protectedPaths: ["/api/admin/"],
  loginPage: false,
}));

app.get("/", (req, res) => {
  // public — req.isAuthenticated is still available
  res.render("home", { isOwner: req.isAuthenticated });
});

protectedPaths secures your data at the server level (API routes return 401). Use <IsItMe> on admin pages for the auth UI — it handles login/register client-side and renders your dashboard only after authentication. All routes still get req.isAuthenticated for conditional server-side rendering.

Deployment modes

Server middleware
recommended

Drop-in Express/Hono/Next.js middleware with cloud credential storage. Full route protection, cookie sessions, zero credential management. The recommended path for most apps.

Browser only

No server needed. Browser talks directly to api.isitme.dev. Session in localStorage. View protection only — source code and APIs are still accessible. Good for static sites and prototypes. Use <IsItMe clientOnly> in React or mode: "cloud" in the browser API.

Custom storage
advanced

Server middleware with your own storage adapter (file, env, memory, custom). Full control, no external dependencies. For power users who want to self-host credentials.

Quick Start

Pick your framework and paste the code. Protect entire routes or show parts of the UI only to the owner.

app.tsx
import { IsItMe } from "isitme/react";

export default function App() {
  return (
    <IsItMe>
      <h1>Welcome back!</h1>
      <p>Only you can see this.</p>
    </IsItMe>
  );
}

What to expect

After pasting the code above and starting your app:

  1. Open your app — you see a registration page (or the <IsItMe> fallback UI)
  2. Tap your fingerprint or use Face ID — you're now the owner of this domain
  3. Refresh the page — still authenticated (session cookie persists)
  4. Open an incognito window — locked out. The login page appears.
  5. On another device with the same passkey manager (iCloud, Google, 1Password) — you can sign in
  6. Anyone else who visits — sees the login page but can't authenticate. You're the only owner.

Server Middleware

The server middleware protects your API routes and serves a built-in login page. Pick your framework.

Handles all /_isitme/* routes, serves a built-in login page, and protects everything else.

server.js
import express from "express";
import cookieParser from "cookie-parser";
import { isitme } from "isitme/express";

const app = express();
app.use(cookieParser());
app.use(isitme());

app.get("/", (req, res) => {
  res.send(`Hello! Authenticated: ${req.isAuthenticated}`);
});

app.listen(3000);

To use a custom login UI instead of the built-in one, set loginPage: false and use the React components or browser API.

Browser API

Plain JS functions from isitme/browser for building your own auth UI without React or Vue.

Client-only = view protection only. Using the browser API without server middleware will hide UI from unauthenticated users, but your JavaScript source code, API routes, and any data fetched client-side are still accessible. For real protection, pair this with the server middleware.
browser
import { signin, isItMe, logout } from "isitme/browser";

await signin();                   // register or login — one call
const session = await isItMe();  // silent check — no UI
await logout();                   // end session

signin()

All-in-one authentication. Checks if a passkey is registered for this domain — if not, triggers registration; if yes, triggers login. Returns { verified, action: "register" | "login" }. This is the simplest way to integrate — no branching logic needed.

isItMe()

Silent session check. Returns session info if authenticated, null otherwise. Never shows UI — use this for feature gating or conditional rendering.

logout()

End the current session. In server mode, clears the session cookie via /_isitme/logout. In cloud mode, clears the session from localStorage. Returns void.

login()
manual

Triggers the browser's passkey authentication modal directly. Use when you want explicit control over when the login prompt appears. Session cookie is set automatically on success. Returns { verified: boolean }.

Throws IsitmeError with code "NOT_SETUP" if no passkey is registered. Use signin() to avoid this — it picks register or login automatically.

register()
manual

Triggers passkey registration directly. The first person to register claims the domain. Returns { verified: boolean, authData?: string }.

Throws IsitmeError with code "ALREADY_SETUP" if a passkey already exists. Use signin() to avoid this — it picks register or login automatically.

getAuthStatus()
legacy

Returns detailed auth status (isAuthenticated, setupComplete). Prefer isItMe() for most use cases.

Example: conditional signin / dashboard button

A common pattern — show a "Sign in" button to visitors and a "Dashboard" link to the owner. No login page needed.

app.js
import { signin, isItMe } from "isitme/browser";

const session = await isItMe();
const btn = document.getElementById("auth-btn");

if (session) {
  btn.textContent = "Dashboard";
  btn.onclick = () => location.href = "/dashboard";
} else {
  btn.textContent = "Sign in";
  btn.onclick = async () => {
    await signin();
    location.href = "/dashboard";
  };
}

Plain HTML (no build tools)

Load isitme/browser from a CDN — no npm, no bundler. The same API as the npm package.

index.html
<script type="module">
import { signin, isItMe }
  from "https://esm.sh/isitme/browser";

const session = await isItMe();

if (!session) {
  await signin();  // registers or logs in automatically
}

document.body.innerHTML = "<h1>You're in!</h1>";
</script>

Credentials are stored in the cloud automatically — one owner per domain, zero config.

Error handling

All functions throw IsitmeError with a code property you can switch on. Import it from isitme/browser.

error-handling.js
import { signin, IsitmeError } from "isitme/browser";

try {
  await signin();
} catch (err) {
  if (err instanceof IsitmeError) {
    switch (err.code) {
      case "NOT_SETUP":
        console.log("No passkey yet — register first");
        break;
      case "ALREADY_SETUP":
        console.log("Domain already claimed — use login");
        break;
      case "PASSKEY_CANCELLED":
        console.log("User cancelled the prompt");
        break;
      case "NETWORK_ERROR":
        console.log("Could not reach the auth server");
        break;
      default:
        console.log(err.message);
    }
  }
}

Error codes

CodeWhenWhat to do
NOT_SETUPlogin() called but no passkey registeredCall register() or signin() instead
ALREADY_SETUPregister() called but domain already claimedCall login() or signin() instead
PASSKEY_CANCELLEDUser dismissed the browser passkey promptShow a "try again" button
NETWORK_ERRORAuth server unreachableCheck middleware is running or API URL is correct
CREDENTIAL_NOT_FOUNDPasskey not recognized by serverPasskey may have been removed — re-register
CHALLENGE_EXPIREDTook too long to complete the promptRetry the operation
VERIFICATION_FAILEDCryptographic verification rejectedRetry or re-register
SITE_NOT_FOUNDDomain not found in cloud APICheck API URL configuration
SITE_BLOCKEDDomain has been blockedContact support
DOMAIN_NOT_ALLOWEDDomain not in allowedOriginsAdd the domain to your allowedOrigins list

When to use manual functions

signin() handles everything for most apps. Use the manual functions when you need explicit control:

You want to...Use
Authenticate with zero branching logicsignin()
Silently check if the user has a sessionisItMe()
Show separate register / login buttonsregister() + login()
Build a custom onboarding flowregister() at the right step
Trigger login from a specific buttonlogin() in an onclick handler
End the session / add a logout buttonlogout()

React Components

Drop-in components from isitme/react for building a custom auth UI. Works with Next.js, Vite, CRA, or any React setup.

<IsItMe>

Checks auth status on mount and shows the right UI automatically: not registered → RegisterForm, registered → LoginForm, authenticated → children.

app/page.tsx
import { IsItMe } from "isitme/react";

export default function Page() {
  return (
    <IsItMe>
      <h1>You're in!</h1>
    </IsItMe>
  );
}
PropertyTypeDefaultDescription
children
ReactNode
Content shown when authenticated.
className
string
Class name applied to the wrapper.
loading
ReactNode
Custom loading element. Defaults to a centered spinner.
fallback
ReactNode | false
What to show when not authenticated. Default: built-in login/register UI. Set to false to render nothing.
loginProps
LoginFormProps
Props passed through to the LoginForm.
registerProps
RegisterFormProps
Props passed through to the RegisterForm.
onAuthenticated
() => void
Called when authentication succeeds.
mode
"server" | "cloud"
"server"Auth mode. Server uses cookie sessions via middleware, cloud uses localStorage.
clientOnly
boolean
falseConvenience shorthand: when true, sets mode to 'cloud'. Use for client-only auth without server middleware.
serverPrefix
string
"/_isitme"Server middleware prefix.
cloudApi
string
"https://api.isitme.dev"Cloud API URL.

Client-only mode (no server)

For static sites or prototypes without server middleware, use clientOnly to skip the server and talk directly to api.isitme.dev. Session is stored in localStorage — this protects UI only, not your APIs or data.

static-site.tsx
import { IsItMe } from "isitme/react";

<IsItMe clientOnly>
  <AdminPanel />
</IsItMe>

This is equivalent to mode="cloud" but more readable. For useAuth(), pass mode: "cloud" directly.

useAuth()

Headless hook for full control. Wraps the browser API with React state management — loading states, error handling, and auto-refresh on mount.

app/dashboard/page.tsx
"use client";
import { useAuth } from "isitme/react";

export default function Dashboard() {
  const { session, loading, logout } = useAuth();

  if (loading) return <p>Loading...</p>;
  if (!session) return <p>Not authenticated</p>;

  return (
    <div>
      <p>Authenticated!</p>
      <button onClick={logout}>Log out</button>
    </div>
  );
}

Return value

PropertyTypeDefaultDescription
session
Session | null
Current session object, or null if not authenticated.
loading
boolean
True during any auth operation (initial check, login, register, logout).
error
Error | null
Most recent error, or null.
isRegistered
boolean
Whether a passkey has been registered for this domain.
login
() => Promise<void>
Trigger passkey login. Updates session on success.
register
() => Promise<void>
Trigger passkey registration. Updates session on success.
logout
() => Promise<void>
Log out and clear the session.
refresh
() => Promise<void>
Re-check session via isItMe().

<SigninForm>
recommended

A single component that handles both registration and login automatically. Checks if a passkey is registered — if not, shows RegisterForm; if yes, shows LoginForm. Use this when you want a standalone form that "just works" without the gate pattern of <IsItMe>.

auth-page.tsx
import { SigninForm } from "isitme/react";

export default function AuthPage() {
  return (
    <SigninForm
      onSuccess={() => router.push("/dashboard")}
    />
  );
}
PropertyTypeDefaultDescription
className
string
Class name applied to the wrapper.
onSuccess
() => void
Called when authentication succeeds (login or register).
onError
(error: Error) => void
Called on authentication failure.
mode
"server" | "cloud"
"server"Auth mode. Server uses cookie sessions via middleware, cloud uses localStorage.
serverPrefix
string
"/_isitme"Server middleware prefix.
cloudApi
string
"https://api.isitme.dev"Cloud API URL.
loginProps
LoginFormProps
Props passed through to the LoginForm.
registerProps
RegisterFormProps
Props passed through to the RegisterForm.

<LoginForm> & <RegisterForm>

Standalone form components with built-in loading and error states. Use these when you want to place the auth UI somewhere specific instead of using IsItMe.

login-page.tsx
import { LoginForm } from "isitme/react";

<LoginForm
  heading="Welcome back"
  onSuccess={() => router.push("/dashboard")}
/>
PropertyTypeDefaultDescription
className
string
Class name for the form container.
heading
string
"Welcome back"Heading text.
description
string
"Sign in with…"Description text below the heading.
buttonText
string
"Sign in with passkey"Button label.
onSuccess
() => void
Called after successful login.
onError
(error: Error) => void
Called on login failure.
mode
"server" | "cloud"
"server"Auth mode.
serverPrefix
string
"/_isitme"Server middleware prefix.
cloudApi
string
"https://api.isitme.dev"Cloud API URL.
setup-page.tsx
import { RegisterForm } from "isitme/react";

<RegisterForm
  heading="Claim this site"
  onSuccess={() => router.push("/")}
/>
PropertyTypeDefaultDescription
className
string
Class name for the form container.
heading
string
"Create your passkey"Heading text.
description
string
"Register a passkey…"Description text below the heading.
buttonText
string
"Register passkey"Button label.
onSuccess
() => void
Called after successful registration.
onError
(error: Error) => void
Called on registration failure.
mode
"server" | "cloud"
"server"Auth mode.
serverPrefix
string
"/_isitme"Server middleware prefix.
cloudApi
string
"https://api.isitme.dev"Cloud API URL.

shadcn Components

Pre-built auth components for shadcn/ui projects. Install them into your project as editable source files — they use your existing Button and Card components.

Unlike the isitme/react package (which is a dependency you import), these are files you own and customize.

Installation

Pick the component you need and install it with the shadcn CLI. Each component will auto-install its shadcn dependencies (Button, Card) and the @isitme/browser npm package.

Terminal
# Login form — passkey sign-in card
npx shadcn@latest add https://isitme.dev/r/login-form.json

# Register form — passkey registration card
npx shadcn@latest add https://isitme.dev/r/register-form.json

# Signin form — auto-detects login vs register
npx shadcn@latest add https://isitme.dev/r/signin-form.json

# Auth gate — renders children only when authenticated
npx shadcn@latest add https://isitme.dev/r/auth-gate.json

# useAuth hook only (no UI)
npx shadcn@latest add https://isitme.dev/r/use-auth.json

Available components

ComponentInstallsDescription
login-formcomponents/isitme/login-form.tsxPasskey sign-in card with loading state and error display.
register-formcomponents/isitme/register-form.tsxPasskey registration card for first-time domain setup.
signin-formcomponents/isitme/signin-form.tsx + login-form + register-form + hooks/use-auth.tsSmart form that auto-switches between login and register.
auth-gatecomponents/isitme/auth-gate.tsx + login-form + register-form + hooks/use-auth.tsGate wrapper — shows children only when authenticated.
use-authhooks/use-auth.tsHeadless hook for full control over auth state.

Usage

After installing, import from the file path in your project. Customize the component to match your design.

app/auth/page.tsx
import { SigninForm } from "@/components/isitme/signin-form";

export default function AuthPage() {
  return (
    <div className="flex min-h-screen items-center justify-center">
      <SigninForm
        onSuccess={() => router.push("/dashboard")}
      />
    </div>
  );
}

shadcn vs npm package

shadcn registryisitme/react
Installnpx shadcn@latest add <url>npm i isitme
OwnershipCopied into your project — you own the filesDependency in node_modules
CustomizationEdit the source directlyVia props and className
StylingUses your shadcn theme (Card, Button)Built-in Tailwind styles (stone palette)
Best forFull control, design systems, shadcn projectsQuick setup, minimal config

Vue Components

Vue 3 composables and components from isitme/vue. Same functionality as the React package, using the Composition API.

<IsItMe>

Auth gate component with named slots. Shows login/register UI or your content based on auth state.

App.vue
<script setup>
import { IsItMe } from "isitme/vue";
</script>

<template>
  <IsItMe>
    <template #default>
      <h1>You're in!</h1>
    </template>
    <template #login="{ login, error }">
      <button @click="login">Sign in with Passkey</button>
    </template>
    <template #register="{ register, error }">
      <button @click="register">Register Passkey</button>
    </template>
    <template #loading>
      <p>Loading...</p>
    </template>
  </IsItMe>
</template>

Available slots

  • #default — Content shown when authenticated.
  • #login="{ login, error }" — Custom login UI. Receives the login function and current error.
  • #register="{ register, error }" — Custom registration UI. Receives the register function and current error.
  • #loading — Custom loading indicator.

useAuth()

Composable that returns reactive refs for auth state. Equivalent to the React hook.

Dashboard.vue
<script setup>
import { useAuth } from "isitme/vue";

const { session, loading, login, logout } = useAuth();
</script>

<template>
  <p v-if="loading">Loading...</p>
  <div v-else-if="session">
    <p>Authenticated!</p>
    <button @click="logout">Log out</button>
  </div>
  <button v-else @click="login">Sign in</button>
</template>

Return value

PropertyTypeDefaultDescription
session
Ref<Session | null>
Reactive session object, or null if not authenticated.
loading
Ref<boolean>
True during any auth operation.
error
Ref<Error | null>
Most recent error, or null.
isRegistered
Ref<boolean>
Whether a passkey has been registered for this domain.
login
() => Promise<void>
Trigger passkey login.
register
() => Promise<void>
Trigger passkey registration.
logout
() => Promise<void>
Log out and clear the session.
refresh
() => Promise<void>
Re-check session via isItMe().

<SigninForm>
recommended

Auto-toggles between RegisterForm and LoginForm based on registration status. Emits success and error events.

auth.vue
<script setup>
import { SigninForm } from "isitme/vue";
import { useRouter } from "vue-router";

const router = useRouter();
</script>

<template>
  <SigninForm
    @success="router.push('/dashboard')"
  />
</template>

Props are the same as the React <SigninForm>, except Vue uses @success and @error events instead of callback props.

PropertyTypeDefaultDescription
className
string
Class name applied to the wrapper.
onSuccess
() => void
Called when authentication succeeds (login or register).
onError
(error: Error) => void
Called on authentication failure.
mode
"server" | "cloud"
"server"Auth mode. Server uses cookie sessions via middleware, cloud uses localStorage.
serverPrefix
string
"/_isitme"Server middleware prefix.
cloudApi
string
"https://api.isitme.dev"Cloud API URL.
loginProps
LoginFormProps
Props passed through to the LoginForm.
registerProps
RegisterFormProps
Props passed through to the RegisterForm.

<LoginForm> & <RegisterForm>

Standalone form components with loading/error states. Emit success and error events.

login.vue
<script setup>
import { LoginForm } from "isitme/vue";
import { useRouter } from "vue-router";

const router = useRouter();
</script>

<template>
  <LoginForm
    heading="Welcome back"
    @success="router.push('/dashboard')"
  />
</template>

Vue forms emit success and error events instead of using callback props. The heading, description, and buttonText props are the same as the React versions.

Examples

Complete, runnable examples for every framework. Clone the repo and run any example with pnpm dev.

Express
Node.js

Minimal Express server with local JSON file storage. The simplest way to get started.

examples/express-local-storage/
Hono
Edge

Hono server with @isitme/hono adapter. Runs on Node, Bun, Deno, and Cloudflare Workers.

examples/hono/
Next.js
React

Next.js App Router with isitme/next middleware and <IsItMe> client component.

examples/nextjs/
Plain HTML
No build

Single HTML file with inline <script type="module"> using CDN imports. Zero build tools needed.

examples/plain-html/
examples/express-local-storage/index.ts
import express from "express";
import cookieParser from "cookie-parser";
import { isitme } from "@isitme/express";

const app = express();
app.use(cookieParser());
app.use(isitme({ storage: "./auth.json" }));

app.get("/", (_req, res) => {
  res.send("You are authenticated!");
});

app.listen(3002);

Recipes

Common patterns for using isitme in real apps.

Conditional content

Show admin tools or debug panels only to the owner — invisible to everyone else. Use fallback={false} to hide content silently instead of showing a login page.

app.tsx
import { IsItMe } from "isitme/react";

<main>
  <h1>My App</h1>
  <IsItMe fallback={false}>
    <AdminToolbar />
    <DebugPanel />
  </IsItMe>
  {/* Regular app content — always visible */}
</main>

Dashboard with logout

Use the useAuth hook for full control over the auth UI — loading states, session data, and a logout button.

dashboard.tsx
import { useAuth, SigninForm } from "isitme/react";

export default function Dashboard() {
  const { session, loading, logout } = useAuth();

  if (loading) return <p>Loading...</p>;
  if (!session) return <SigninForm />;

  return (
    <div>
      <h1>Dashboard</h1>
      <button onClick={logout}>Log out</button>
    </div>
  );
}

Public landing + protected routes

Use publicPaths to keep some routes open. Protected routes redirect to the login page automatically.

server.js
app.use(isitme({ publicPaths: ["/", "/about", "/pricing"] }));

app.get("/", (req, res) => {
  res.send(req.isAuthenticated ? "Welcome back, boss" : "Public landing");
});

app.get("/admin", (req, res) => {
  // Only the owner can reach this
  res.send("Admin panel");
});

CSR admin page (UI gating + data gating)

For SPAs and CSR-only pages, protect API routes on the server and let <IsItMe> handle the auth UI on the client. Don't put page paths in protectedPaths — the middleware would block the HTML before React can render.

server.js
// Only protect API routes — the /admin page loads freely
app.use(isitme({
  protectedPaths: ["/api/admin/"],
  loginPage: false,
}));
AdminPage.tsx
// <IsItMe> shows login form until authenticated, then renders children
export default function AdminPage() {
  return (
    <IsItMe>
      <AdminDashboard />
    </IsItMe>
  );
}

Custom login page styling

Customize the text and appearance of the built-in auth UI by passing props through <IsItMe>.

app.tsx
<IsItMe
  loginProps={{
    heading: "Welcome back, boss",
    buttonText: "Unlock",
  }}
  registerProps={{
    heading: "Claim this site",
    buttonText: "Set up passkey",
  }}
>
  <Dashboard />
</IsItMe>

Biometric 2FA layer

Already have auth in your app? Add isitme as a second factor for sensitive pages. Your existing auth verifies the user, isitme adds a biometric gate on top.

settings.tsx
// Your existing auth already verified the user
// Add isitme as a biometric second factor

<IsItMe fallback={<p>Verify your identity with a passkey to continue.</p>}>
  <SensitiveSettings />
  <BillingPanel />
  <DangerZone />
</IsItMe>

Allowed origins

Control which domains can use authentication. Useful for blocking temporary deploy previews (Vercel, Netlify, Cloudflare) from claiming ownership.

server middleware
isitme({
  allowedOrigins: ["myapp.com", "*.myapp.com", "localhost"]
})

When allowedOrigins is set:

  • Registration and login are blocked on any domain not in the list (returns 403)
  • Status checks and logout always work on any domain
  • Exact hostnames: "myapp.com"
  • Full origins: "https://myapp.com"
  • Wildcards: "*.myapp.com" (matches any subdomain)
  • Regex: /.*\.vercel\.app/ (wrapped in slashes)
  • If omitted, all origins are allowed (default)

Advanced Configuration

Middleware options for self-hosting, credential storage, sessions, and the raw auth routes.

IsitmeOptions

Pass options to isitme() to customize behavior.

options
isitme({
  publicPaths: ["/", "/about"],
  storage: file("./.credentials.json"),
  sessionMaxAge: 3600,
})
PropertyTypeDefaultDescription
publicPaths
string[]
[]Routes accessible without authentication.
storage
StorageAdapter
cloud()Where credentials are persisted. Built-in: cloud, file, env, memory.
sessionSecret
string
autoSecret used to sign JWT session cookies. Auto-generated if omitted.
sessionMaxAge
number
86400Session duration in seconds (default 24 hours).
loginPage
PageOptions | false
{}Customize the built-in login page, or set to false to disable it.
rpName
string
hostnameRelying Party name shown in the browser's WebAuthn prompt.
rpID
string
hostnameRelying Party ID for WebAuthn. Defaults to the request hostname.
origin
string
autoExpected origin for WebAuthn responses. Auto-detected from the request.
allowedOrigins
string[]
undefinedRestrict auth to specific hostnames or origins. Supports exact hostnames, full origins, wildcards ("*.example.com"), and regex patterns ("/.*\\.vercel\\.app/").

Login Page

isitme ships a built-in login and registration page. Customize it with loginPage options, or disable it with false and handle auth UI yourself.

custom login page
isitme({
  loginPage: {
    title: "Welcome back",
    brandName: "My App",
    brandColor: "#6366f1",
  },
})

PageOptions

PropertyTypeDefaultDescription
title
string
"Protect this site"Heading displayed on the login/register page.
description
string
"Use your fingerprint…"Subheading text below the title.
brandName
string
Your app name, shown in the page header.
brandColor
string
"#1c1917"Primary accent color for buttons and links.
logoUrl
string
URL to a logo image displayed on the login page.

Credential Storage

By default, credentials are stored in isitme's cloud service — no config needed. Just use isitme() with no storage option. For full control, pass a custom storage adapter.

Cloud (default) — Credentials stored on api.isitme.dev, scoped to your domain. No signup, no API key — the first person to register becomes the owner. We only store public keys; private keys never leave your device.

Custom storage
advanced

Pass a storage option to manage credentials yourself. Full control, no external dependencies.

file(path)

Persists credentials to a local JSON file. Great for development and single-server deployments.

env(key)

Read-only adapter that loads credentials from an environment variable. Set the var at deploy time.

memory()

In-memory storage, lost on restart. Useful for testing and ephemeral environments.

Custom adapter

Implement load() and save() to use your own database or backend.

PropertyTypeDefaultDescription
load
() => Promise<Data>
Load persisted credential data. Return null if nothing stored yet.
save
(data: Data) => Promise<void>
Persist credential data after registration or changes.
custom adapter
isitme({
  storage: {
    async load() {
      return db.get("isitme_credentials");
    },
    async save(data) {
      await db.set("isitme_credentials", data);
    },
  },
});

Sessions & Guard

Sessions are stored as a signed JWT in an HTTP-only, secure, SameSite=Lax cookie named isitme_session. Set automatically on login, cleared on logout. No session store required.

checking auth in handlers
app.get("/dashboard", (req, res) => {
  if (req.isAuthenticated) {
    res.send("Welcome back!");
  } else {
    res.send("Public content");
  }
});

publicPaths

Routes listed in publicPaths skip the auth redirect. Unauthenticated users can access them but req.isAuthenticated is still set.

public paths
isitme({
  publicPaths: ["/", "/about", "/api/health"],
})

Auth Routes

The middleware mounts these endpoints under /_isitme. They power the login page and can be called directly.

GET
/_isitme/status

Check if authenticated and whether credentials are registered.

Response

{ "authenticated": true, "registered": true }
POST
/_isitme/register/start

Begin passkey registration. Returns WebAuthn creation options.

POST
/_isitme/register/finish

Complete registration. Stores credential and creates a session.

POST
/_isitme/login/start

Begin passkey authentication. Returns WebAuthn request options.

POST
/_isitme/login/finish

Complete authentication. Verifies credential and creates a session.

POST
/_isitme/logout

Clear the session cookie and log out.

Manual setup (without an adapter)

If you need fine-grained control over the auth endpoints — for example, in Next.js App Router without edge middleware — you can wire up @isitme/core/edge directly using route handlers. See examples/nextjs/app/api/auth/[...path]/route.ts for a complete reference implementation (~120 lines). The isitme/express, isitme/next, and isitme/hono adapters are the recommended approach for most users.

Recovery

What happens when you lose a device, switch platforms, or need to start fresh.

Passkey sync

Passkeys are synced automatically by your platform's credential manager — iCloud Keychain on Apple devices, Google Password Manager on Android/Chrome, or third-party managers like 1Password. When you register a passkey on one device, it's available on all your devices signed into the same account. Nothing to configure.

Lost device

If you lose a device, your passkeys are still available on any other device synced to the same account. Sign in from another device — your Mac, your phone, a new laptop — and you're back in.

Cross-device login

Need to sign in from a device that doesn't have your passkey? Most browsers support QR code cross-device authentication — scan a QR code with your phone to approve the login on the other device.

Full recovery via DNS

If you lose access to all your devices and your passkey manager, you can re-register by proving domain ownership. Add a DNS TXT record to your domain — isitme verifies it and lets you register a new passkey.

DNS TXT record
Type:  TXT
Name:  _isitme.yourdomain.com
Value: isitme-verify=<token>

You own the domain — that's all the proof we need. Visit /debug on your site or the admin dashboard to initiate recovery.

Agent Skills

isitme ships with an Agent Skill — a structured instruction file that AI coding assistants can discover and use automatically. When you ask an AI agent to "add passkey auth", it finds the isitme skill and knows exactly which packages to install, how to set up the middleware, and how to wire up the client.

Install the skill

Add the isitme skill to your project with the skills CLI:

npx skills add isitme-dev/isitme

Then open your project in any supported tool and tell it to add passkey auth. The skill is also bundled in the npm package — compatible tools discover it automatically when isitme is installed as a dependency.

Supported tools

Any tool that supports the Agent Skills spec will discover isitme automatically. This includes Claude Code, Cursor, Gemini CLI, GitHub Copilot, VS Code, and many more.

What's in the skill

  • Quick start instructions for every framework (Express, Hono, Next.js)
  • Cloud mode vs local mode setup
  • React and browser client integration
  • UI gating vs data gating patterns
  • Custom storage adapter interface
  • Full API reference

Try it

Ask your AI coding assistant:

"Add passkey auth to my Express app"

"Set up isitme with protected admin routes"

"Add a React auth gate to my dashboard page"

Troubleshooting

Common issues and how to fix them.

ProblemCauseFix
Passkey prompt doesn't appearWebAuthn requires HTTPS (localhost is exempt)Use localhost for development, or set up HTTPS
NOT_SETUP error after registeringDifferent browser profile or domainCheck you're on the same origin where you registered
Works locally, fails in productionOrigin or rpID mismatchSet rpID and origin in isitme options to match your production domain
req.cookies is undefinedMissing cookie-parser middlewarenpm i cookie-parser and add app.use(cookieParser()) before isitme
Session expires too fastDefault is 24 hoursSet sessionMaxAge to a longer value (in seconds)
How to reset and start overNeed to clear stored credentialsCloud: use the debug tool at isitme.dev/debug. File: delete the JSON file. Env: clear the env var.
Passkey works on laptop but not phonePasskeys not syncing across devicesCheck your passkey manager (iCloud Keychain, Google Password Manager, 1Password) is enabled and syncing