passkey auth / cloudflare workers + d1

Auth primitives.
No hosted dependency.

Passkey-only signup, login, sessions, and invite codes for TypeScript applications. Your users, credentials, and authentication data stay in your own database.

model
library-first
hosting
self-hosted
release
pre-1.0

install

Four focused packages cover the complete Worker and D1 path.

pnpm add @passkeeper/core @passkeeper/client @passkeeper/cloudflare @passkeeper/d1

packages

Protocol rules stay separate from browser, runtime, and storage concerns.

  1. 01

    @passkeeper/core

    Challenges, verification, registration, authentication, sessions, and invites.

    runtime agnostic
  2. 02

    @passkeeper/client

    Browser ceremonies, option conversion, credential serialization, and errors.

    browser
  3. 03

    @passkeeper/cloudflare

    Worker routes, trusted origins, bounded bodies, cookies, and request hooks.

    worker
  4. 04

    @passkeeper/d1

    Inspectable SQL, atomic storage operations, migrations, and scheduled cleanup.

    storage

secure defaults

Security properties are wired into the ordinary path, not left as integration chores.

  • Trusted relying-party and browser-origin validation
  • One-time, expiring challenges consumed atomically
  • Hashed session tokens and invite codes
  • Guarded credential counters and invite usage
  • HttpOnly, Secure, SameSite session cookies
  • Oslo parsing with runtime WebCrypto verification

live demo

Run a real passkey ceremony against the same packages shown above. Local development uses the seeded invite launch-code; deployed demos require an issued invite.

Demo credentials belong only to this origin. Do not use a production identity here.

worker setup

Configure the trusted domain, bind D1, and mount auth where the application already runs.

typescript worker / module
import { createPasskeeperRoutes } from "@passkeeper/cloudflare";
import { d1Adapter } from "@passkeeper/d1";

export default {
  async fetch(request: Request, env: Env) {
    const auth = createPasskeeperRoutes({
      rpName: "My App",
      rpId: env.RP_ID,
      origin: env.RP_ORIGIN,
      storage: d1Adapter(env.DB),
      inviteRequired: true,
    });

    return auth.handle(request);
  },
};

keep ownership

Passkeys without handing auth to another platform.

start with the packages