Skip to content
LogoLogo

Custom Passkey Server

Run your own server for passkey metadata and account records in app-specific mode.

Overview

The SDK needs a server that stores, for each passkey, its credential ID, public key and display name, plus the record of the smart account the passkey created. By default the SDK uses https://api.justaname.id/wallet/v2/passkeys, but you can host your own.

Configuration

Point the SDK to your server's passkeys endpoint:

import { JAW, Mode } from '@jaw.id/core';
import { ReactUIHandler } from '@jaw.id/ui';
 
const jaw = JAW.create({
  apiKey: 'your-api-key',
  preference: {
    mode: Mode.AppSpecific,
    uiHandler: new ReactUIHandler(),
    serverUrl: 'https://your-server.example.com/passkeys',
  },
});

serverUrl is the full URL of the passkeys endpoint. The SDK sends requests to exactly that URL, and fetches account records from the accounts path next to it:

serverUrlPasskeys endpointAccounts endpoint
https://your-server.example.com/passkeyshttps://your-server.example.com/passkeyshttps://your-server.example.com/accounts
https://your-server.example.com/v1/passkeyshttps://your-server.example.com/v1/passkeyshttps://your-server.example.com/v1/accounts

Query strings in serverUrl are dropped.

Response Format

Every response is a JSON envelope:

{
  "statusCode": 200,
  "result": {
    "data": {},
    "error": null
  }
}

On success, data must not be null: the SDK treats a null data as a failure and reports error. On failure, return a 4xx/5xx status with data: null and a message in error.

Required Endpoints

POST /passkeys - Register a New Passkey

Registers a new passkey credential, and stores the record of the account it creates.

Headers:

  • Content-Type: application/json

Request Body:

{
  "credentialId": "string",
  "publicKey": "0x...",
  "displayName": "string"
}

Success Response (200/201): any non-null data, for example:

{
  "statusCode": 201,
  "result": {
    "data": "Passkey registered successfully",
    "error": null
  }
}

Behavior:

  • If the credential ID is already registered, change nothing and return success. Never overwrite the public key of an existing credential.
  • Compute the account record from the public key, as described in Computing the account record, and store it with the passkey in the same transaction.

GET /passkeys - Lookup Passkeys by Credential IDs

Retrieves passkey information for one or more credential IDs. Used when a user imports a passkey on a new device.

Query Parameters:

  • credentialIds (string[], repeatable): Array of credential IDs to lookup

Success Response (200):

{
  "statusCode": 200,
  "result": {
    "data": {
      "passkeys": [
        {
          "credentialId": "string",
          "publicKey": "0x...",
          "displayName": "string"
        }
      ]
    },
    "error": null
  }
}

Error Response (404):

{
  "statusCode": 404,
  "result": {
    "data": null,
    "error": "Passkeys not found"
  }
}

GET /accounts - Get the Account Record of a Credential

Returns the record of the smart account a passkey signs for: its address and what it was created from.

Query Parameters:

  • credentialId (string): The credential ID to look up

Success Response (200):

{
  "statusCode": 200,
  "result": {
    "data": {
      "address": "0x1f4201aED7443a59a3f849E6A07dbF438FFB6C62",
      "version": 1,
      "factory": "0x5803c076563C85799989d42Fc00292A8aE52fa9E",
      "owners": [
        "0x885d083da109f4ea0b626025ee5886d869f51817afad5a380f2b1fa9a35bccf8d52b9347c47495158ef458c338cdd15f7a902d357541d0e19d0b19e8f7ea0fd8",
        "0x000000000000000000000000f1b40e3d5701c04d86f7828f0eb367b9c90901d8"
      ],
      "nonce": "0"
    },
    "error": null
  }
}

Error Response (404): the credential is not registered.

Behavior:

  • The request carries only a credential ID. Look up the stored record; if the passkey was registered before you stored records, compute it from the stored public key and store it then.
  • Never accept an address or owners from a request. Anyone with your app's API key can call this endpoint, so a record a client could write is a record an attacker could write.

Computing the Account Record

Every account is created from its passkey's public key with this recipe (version 1):

FieldValue
factory0x5803c076563C85799989d42Fc00292A8aE52fa9E
owners[publicKey, 0x000000000000000000000000f1b40e3d5701c04d86f7828f0eb367b9c90901d8]
nonce0
addressCREATE2 from the factory, with the salt keccak256(abi.encode(owners, nonce)) and the init code hash 0x66e5c494c913cf25a974791c8b932da3b3e54d43bb638e7a6421c64db20696e4

The second owner is the permission manager, 0xf1b40E3D5701C04d86F7828f0EB367B9C90901D8, padded to 32 bytes. With viem:

import { encodeAbiParameters, getContractAddress, keccak256, pad, type Hex } from 'viem';
 
const FACTORY = '0x5803c076563C85799989d42Fc00292A8aE52fa9E';
const PERMISSIONS_MANAGER = '0xf1b40E3D5701C04d86F7828f0EB367B9C90901D8';
const INIT_CODE_HASH = '0x66e5c494c913cf25a974791c8b932da3b3e54d43bb638e7a6421c64db20696e4';
 
export function accountRecord(publicKey: Hex) {
  const owners = [publicKey.toLowerCase() as Hex, pad(PERMISSIONS_MANAGER).toLowerCase() as Hex];
  const nonce = 0n;
  const salt = keccak256(encodeAbiParameters([{ type: 'bytes[]' }, { type: 'uint256' }], [owners, nonce]));
  const address = getContractAddress({ opcode: 'CREATE2', from: FACTORY, salt, bytecodeHash: INIT_CODE_HASH });
 
  return { address, version: 1, factory: FACTORY, owners, nonce: nonce.toString() };
}

Check your implementation against the factory itself. For the public key 0x followed by 11 repeated 64 times, the address is 0x3f9D0947449d8dbAfDC6ACFE301593e97176780F:

cast call 0x5803c076563C85799989d42Fc00292A8aE52fa9E \
  'getAddress(bytes[],uint256)(address)' \
  '[<publicKey>,0x000000000000000000000000f1b40E3D5701C04d86F7828f0EB367B9C90901D8]' 0 \
  --rpc-url https://mainnet.base.org

Store the record once, when the passkey is registered, and serve it from then on. Do not recompute it on each request: the stored record is what keeps an account's address fixed if the recipe for new accounts ever changes.

How the SDK Uses the Record

  • The SDK accepts a record only if it is exactly the one the user's own public key creates. A record that does not match is ignored, so a misbehaving server cannot point a user at another account.
  • If the accounts endpoint is missing or fails, the SDK computes the record itself, so the endpoint is optional today. Implement it anyway: a future release that lets users rotate passkeys will rely on it, because a rotated passkey cannot compute its account's address from its own key.

Data Format Requirements

FieldFormatDescription
credentialIdBase64url stringStandard WebAuthn credential ID format
publicKeyHex string with 0x prefixThe passkey's P-256 public key: 64 bytes, x then y
displayNameStringHuman-readable name for the passkey
addressChecksummed addressThe smart account address
ownersArray of hex stringsOwner bytes the account was created with, in order
nonceDecimal stringThe nonce the account was created with

When to Self-Host

Consider running your own passkey server when:

  • Data sovereignty: You need full control over user passkey metadata
  • Custom logic: You want to add additional validation or processing
  • Development/staging: You need isolated environments for testing
  • Compliance: Your organization requires self-hosted infrastructure