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:
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 contentBy 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:
<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:
protectedPathsblocks 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:
// 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
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.
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.
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.
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:
- Open your app — you see a registration page (or the <IsItMe> fallback UI)
- Tap your fingerprint or use Face ID — you're now the owner of this domain
- Refresh the page — still authenticated (session cookie persists)
- Open an incognito window — locked out. The login page appears.
- On another device with the same passkey manager (iCloud, Google, 1Password) — you can sign in
- 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.
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.
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 sessionsignin()
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.
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.
<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.
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
| Code | When | What to do |
|---|---|---|
NOT_SETUP | login() called but no passkey registered | Call register() or signin() instead |
ALREADY_SETUP | register() called but domain already claimed | Call login() or signin() instead |
PASSKEY_CANCELLED | User dismissed the browser passkey prompt | Show a "try again" button |
NETWORK_ERROR | Auth server unreachable | Check middleware is running or API URL is correct |
CREDENTIAL_NOT_FOUND | Passkey not recognized by server | Passkey may have been removed — re-register |
CHALLENGE_EXPIRED | Took too long to complete the prompt | Retry the operation |
VERIFICATION_FAILED | Cryptographic verification rejected | Retry or re-register |
SITE_NOT_FOUND | Domain not found in cloud API | Check API URL configuration |
SITE_BLOCKED | Domain has been blocked | Contact support |
DOMAIN_NOT_ALLOWED | Domain not in allowedOrigins | Add 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 logic | signin() |
| Silently check if the user has a session | isItMe() |
| Show separate register / login buttons | register() + login() |
| Build a custom onboarding flow | register() at the right step |
| Trigger login from a specific button | login() in an onclick handler |
| End the session / add a logout button | logout() |
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.
import { IsItMe } from "isitme/react";
export default function Page() {
return (
<IsItMe>
<h1>You're in!</h1>
</IsItMe>
);
}| Property | Type | Default | Description |
|---|---|---|---|
| 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 | false | Convenience 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.
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.
"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
| Property | Type | Default | Description |
|---|---|---|---|
| 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>.
import { SigninForm } from "isitme/react";
export default function AuthPage() {
return (
<SigninForm
onSuccess={() => router.push("/dashboard")}
/>
);
}| Property | Type | Default | Description |
|---|---|---|---|
| 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.
import { LoginForm } from "isitme/react";
<LoginForm
heading="Welcome back"
onSuccess={() => router.push("/dashboard")}
/>| Property | Type | Default | Description |
|---|---|---|---|
| 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. |
import { RegisterForm } from "isitme/react";
<RegisterForm
heading="Claim this site"
onSuccess={() => router.push("/")}
/>| Property | Type | Default | Description |
|---|---|---|---|
| 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.
# 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.jsonAvailable components
| Component | Installs | Description |
|---|---|---|
| login-form | components/isitme/login-form.tsx | Passkey sign-in card with loading state and error display. |
| register-form | components/isitme/register-form.tsx | Passkey registration card for first-time domain setup. |
| signin-form | components/isitme/signin-form.tsx + login-form + register-form + hooks/use-auth.ts | Smart form that auto-switches between login and register. |
| auth-gate | components/isitme/auth-gate.tsx + login-form + register-form + hooks/use-auth.ts | Gate wrapper — shows children only when authenticated. |
| use-auth | hooks/use-auth.ts | Headless 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.
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 registry | isitme/react | |
|---|---|---|
| Install | npx shadcn@latest add <url> | npm i isitme |
| Ownership | Copied into your project — you own the files | Dependency in node_modules |
| Customization | Edit the source directly | Via props and className |
| Styling | Uses your shadcn theme (Card, Button) | Built-in Tailwind styles (stone palette) |
| Best for | Full control, design systems, shadcn projects | Quick 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.
<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 theloginfunction and currenterror.#register="{ register, error }"— Custom registration UI. Receives theregisterfunction and currenterror.#loading— Custom loading indicator.
useAuth()
Composable that returns reactive refs for auth state. Equivalent to the React hook.
<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
| Property | Type | Default | Description |
|---|---|---|---|
| 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.
<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.
| Property | Type | Default | Description |
|---|---|---|---|
| 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.
<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.
Minimal Express server with local JSON file storage. The simplest way to get started.
examples/express-local-storage/Hono server with @isitme/hono adapter. Runs on Node, Bun, Deno, and Cloudflare Workers.
examples/hono/Next.js App Router with isitme/next middleware and <IsItMe> client component.
examples/nextjs/Single HTML file with inline <script type="module"> using CDN imports. Zero build tools needed.
examples/plain-html/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.
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.
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.
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.
// Only protect API routes — the /admin page loads freely
app.use(isitme({
protectedPaths: ["/api/admin/"],
loginPage: false,
}));// <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>.
<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.
// 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.
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.
isitme({
publicPaths: ["/", "/about"],
storage: file("./.credentials.json"),
sessionMaxAge: 3600,
})| Property | Type | Default | Description |
|---|---|---|---|
| publicPaths | string[] | [] | Routes accessible without authentication. |
| storage | StorageAdapter | cloud() | Where credentials are persisted. Built-in: cloud, file, env, memory. |
| sessionSecret | string | auto | Secret used to sign JWT session cookies. Auto-generated if omitted. |
| sessionMaxAge | number | 86400 | Session duration in seconds (default 24 hours). |
| loginPage | PageOptions | false | {} | Customize the built-in login page, or set to false to disable it. |
| rpName | string | hostname | Relying Party name shown in the browser's WebAuthn prompt. |
| rpID | string | hostname | Relying Party ID for WebAuthn. Defaults to the request hostname. |
| origin | string | auto | Expected origin for WebAuthn responses. Auto-detected from the request. |
| allowedOrigins | string[] | undefined | Restrict 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.
isitme({
loginPage: {
title: "Welcome back",
brandName: "My App",
brandColor: "#6366f1",
},
})PageOptions
| Property | Type | Default | Description |
|---|---|---|---|
| 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.
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.
Persists credentials to a local JSON file. Great for development and single-server deployments.
Read-only adapter that loads credentials from an environment variable. Set the var at deploy time.
In-memory storage, lost on restart. Useful for testing and ephemeral environments.
Implement load() and save() to use your own database or backend.
| Property | Type | Default | Description |
|---|---|---|---|
| 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. |
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.
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.
isitme({
publicPaths: ["/", "/about", "/api/health"],
})Auth Routes
The middleware mounts these endpoints under /_isitme. They power the login page and can be called directly.
/_isitme/statusCheck if authenticated and whether credentials are registered.
Response
{ "authenticated": true, "registered": true }/_isitme/register/startBegin passkey registration. Returns WebAuthn creation options.
/_isitme/register/finishComplete registration. Stores credential and creates a session.
/_isitme/login/startBegin passkey authentication. Returns WebAuthn request options.
/_isitme/login/finishComplete authentication. Verifies credential and creates a session.
/_isitme/logoutClear 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.
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/isitmeThen 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.
| Problem | Cause | Fix |
|---|---|---|
| Passkey prompt doesn't appear | WebAuthn requires HTTPS (localhost is exempt) | Use localhost for development, or set up HTTPS |
NOT_SETUP error after registering | Different browser profile or domain | Check you're on the same origin where you registered |
| Works locally, fails in production | Origin or rpID mismatch | Set rpID and origin in isitme options to match your production domain |
req.cookies is undefined | Missing cookie-parser middleware | npm i cookie-parser and add app.use(cookieParser()) before isitme |
| Session expires too fast | Default is 24 hours | Set sessionMaxAge to a longer value (in seconds) |
| How to reset and start over | Need to clear stored credentials | Cloud: use the debug tool at isitme.dev/debug. File: delete the JSON file. Env: clear the env var. |
| Passkey works on laptop but not phone | Passkeys not syncing across devices | Check your passkey manager (iCloud Keychain, Google Password Manager, 1Password) is enabled and syncing |