Skip to main content

Architecture overview

Ave is built on three core principles:
  1. Passwordless authentication - WebAuthn passkeys replace passwords
  2. End-to-end encryption - Master keys never leave devices in plaintext
  3. Standards-based - OAuth 2.0, OIDC, and WebAuthn protocols
Ave Architecture

Components

Frontend (ave-web)

React + Vite application that handles:
  • User registration and login UI
  • WebAuthn credential creation/authentication
  • Client-side cryptography (master key generation, encryption)
  • OAuth authorization flows
  • Device management dashboard

Backend (ave-server)

Hono.js API running on Cloudflare Workers:
  • WebAuthn registration/authentication endpoints (/api/register, /api/login)
  • OAuth 2.0 token endpoints (/api/oauth/token, /api/oauth/authorize)
  • OIDC discovery (/.well-known/openid-configuration)
  • Session management with stateless JWT tokens
  • WebSocket server for real-time notifications

SDKs

@ave-id/sdk - TypeScript SDK with helpers for:
  • Building authorization URLs with PKCE
  • Exchanging codes for tokens
  • Refreshing access tokens
  • Token exchange (delegation)
  • Signing API integration
@ave-id/embed - Embeddable authentication UI:
  • Inline iframes
  • Modal sheets
  • Popup windows
  • PostMessage communication

End-to-end encryption

Master key generation

When a user creates an account, Ave generates a 256-bit master key in the browser using crypto.getRandomValues(). This key never leaves the device in plaintext.
// Simplified from ave-web/src/lib/crypto.ts
function generateMasterKey(): Uint8Array {
  const key = new Uint8Array(32);
  crypto.getRandomValues(key);
  return key;
}
The master key is never sent to the server. All encryption/decryption happens client-side in the browser.

Master key protection

The master key is protected by three independent methods:
Ave uses the PRF (Pseudo-Random Function) extension of WebAuthn to derive encryption keys from passkey authentication.
// During registration
const credential = await navigator.credentials.create({
  publicKey: {
    ...options,
    extensions: {
      prf: { eval: { first: salt } },
    },
  },
});

const prfOutput = credential.getClientExtensionResults().prf.results.first;
const encryptionKey = await deriveKey(prfOutput);
const encryptedMasterKey = await encrypt(masterKey, encryptionKey);
PRF provides a unique, deterministic output for each passkey. The encrypted master key is stored in localStorage, and decrypted when the user authenticates.

Data encryption

All user data (profile fields, settings, etc.) is encrypted with the master key before being sent to the server:
const encryptedData = await encryptWithMasterKey(masterKey, {
  displayName: "Jane Doe",
  email: "[email protected]",
  birthday: "1990-01-01",
});

// Server only receives encrypted blob
await fetch("/api/identities", {
  method: "POST",
  body: JSON.stringify({ encryptedData }),
});
The server has zero knowledge of user data. It can only store and retrieve encrypted blobs.

Authentication flows

Registration flow

1

User enters handle

Frontend checks if handle is available via /api/register/start
2

WebAuthn registration

Browser prompts for biometric/PIN to create a passkey. A temporary user ID is generated for the WebAuthn credential.
const options = await generateRegistrationOptions({
  rpName: "Ave",
  rpID: "aveid.net",
  userName: handle,
  userID: tempUserId,
  authenticatorSelection: {
    residentKey: "required",
    userVerification: "required",
  },
  extensions: {
    prf: { eval: { first: salt } },
  },
});
3

Master key generation

Frontend generates a 256-bit master key and encrypts it with PRF output, security question answers, and trust codes.
4

Complete registration

Frontend sends to /api/register/complete:
  • WebAuthn credential (public key, credential ID)
  • Identity data (encrypted)
  • Hashed security answers
  • Encrypted master key backups
  • Trust codes (hashed)
  • Device information
Backend creates:
  • User account
  • Identity record
  • Passkey record
  • Device record
  • Session token
5

User logged in

Session token is set in an HTTP-only cookie. User is redirected to the dashboard.

Passkey login flow

1

User enters handle

Frontend calls /api/login/start to get WebAuthn challenge options.
2

WebAuthn authentication

Browser prompts for biometric/PIN. The passkey signs the challenge.
const credential = await navigator.credentials.get({
  publicKey: {
    ...options,
    extensions: {
      prf: { eval: { first: salt } },
    },
  },
});
3

Decrypt master key

Frontend uses PRF output to decrypt the master key from localStorage.
4

Complete login

Frontend sends signed challenge to /api/login/complete. Backend:
  • Verifies WebAuthn signature
  • Creates session token
  • Updates device last seen timestamp

Multi-device login flow

When a user wants to log in on a new device without a passkey:
1

Request approval

New device generates an ephemeral ECDH key pair (P-256 curve) and sends public key to /api/login/request-approval.
const keyPair = await crypto.subtle.generateKey(
  { name: "ECDH", namedCurve: "P-256" },
  true,
  ["deriveKey"]
);

const publicKeyJwk = await crypto.subtle.exportKey("jwk", keyPair.publicKey);
Backend stores the login request with the requester’s public key.
2

Notify trusted device

Backend sends WebSocket notification to all active devices for this user. Trusted device sees the login request in real-time.
3

User approves

Trusted device:
  • Loads master key from localStorage
  • Generates its own ephemeral ECDH key pair
  • Derives shared secret using requester’s public key
  • Encrypts master key with shared secret
  • Sends encrypted master key to /api/login/approve
const sharedSecret = await crypto.subtle.deriveKey(
  { name: "ECDH", public: requesterPublicKey },
  trustedDevicePrivateKey,
  { name: "AES-GCM", length: 256 },
  false,
  ["encrypt"]
);

const encryptedMasterKey = await crypto.subtle.encrypt(
  { name: "AES-GCM", iv },
  sharedSecret,
  masterKey
);
4

New device receives master key

New device:
  • Receives encrypted master key via WebSocket
  • Derives same shared secret using trusted device’s ephemeral public key
  • Decrypts master key
  • Stores in localStorage
  • Logs in automatically
The ephemeral ECDH keys are never stored - they’re generated on-demand and discarded after use. The backend never sees the shared secret or plaintext master key.

OAuth 2.0 integration

Authorization flow

1

App redirects to Ave

const url = buildAuthorizeUrl({
  clientId: "app_123",
  redirectUri: "https://yourapp.com/callback",
}, {
  scope: ["openid", "profile", "email"],
  codeChallenge: challenge,
  codeChallengeMethod: "S256",
});
// Redirects to: https://aveid.net/signin?client_id=...
2

User authenticates

User logs in with passkey and selects which identity to share with the app.
3

User authorizes app

Ave shows app information (name, description, website) and requested permissions. User swipes to approve.
4

Redirect with code

Ave redirects back to app with authorization code:
https://yourapp.com/callback?code=abc123&state=xyz
5

Exchange code for tokens

App backend exchanges code for tokens:
const tokens = await exchangeCodeServer(
  {
    clientId: "app_123",
    clientSecret: process.env.AVE_SECRET,
    redirectUri: "https://yourapp.com/callback",
  },
  { code: "abc123" }
);
Backend verifies PKCE code_verifier (if used) and returns:
  • access_token - JWT with user/identity info
  • id_token - OIDC identity token
  • refresh_token - Long-lived token (if offline_access requested)

Token structure

Access tokens are Ed25519-signed JWTs with the following claims:
{
  "sub": "user_abc123",
  "iss": "https://api.aveid.net",
  "aud": "app_xyz789",
  "iat": 1234567890,
  "exp": 1234571490,
  "scope": "openid profile email",
  "identity_id": "identity_def456",
  "handle": "janedoe",
  "name": "Jane Doe",
  "email": "[email protected]",
  "avatar_url": "https://cdn.aveid.net/avatars/..."
}
Verify tokens using the public key from /.well-known/jwks.json:
import * as jose from "jose";

const jwks = await jose.createRemoteJWKSet(
  new URL("https://api.aveid.net/.well-known/jwks.json")
);

const { payload } = await jose.jwtVerify(accessToken, jwks, {
  issuer: "https://api.aveid.net",
  audience: "your_client_id",
});

Security features

Session management

Sessions are stateless JWT tokens signed with Ed25519. They contain:
  • User ID
  • Device ID
  • Issued at / Expires at timestamps
  • Session fingerprint (hashed)
Sessions are stored in HTTP-only, secure, SameSite=Lax cookies.
Session tokens are short-lived (default: 7 days) and automatically refreshed on activity.

Device tracking

Each device has a unique fingerprint stored in localStorage:
const fingerprint = `${navigator.userAgent}_${screen.width}x${screen.height}_${timezone}`;
Devices are tracked with:
  • Device type (phone, computer, tablet)
  • Browser and OS information
  • Last seen timestamp
  • IP address (logged in activity)

Activity logging

All security-relevant actions are logged:
  • Account creation
  • Login/logout events
  • Passkey added/removed
  • Trust codes regenerated
  • Device revoked
  • OAuth authorizations
Logs include:
  • Timestamp
  • Action type
  • Device ID
  • IP address
  • User agent
  • Severity (info, warning, danger)

Rate limiting

Ave implements rate limiting on sensitive endpoints:
  • Login attempts: 5 per minute per IP
  • Registration: 3 per hour per IP
  • Trust code recovery: 3 per hour per account
  • Security question attempts: 5 per hour per account
Rate limits are enforced at the Cloudflare Workers level using Durable Objects for distributed state.

OIDC discovery

Ave exposes standard OIDC discovery endpoints:
curl https://api.aveid.net/.well-known/openid-configuration
{
  "issuer": "https://api.aveid.net",
  "authorization_endpoint": "https://api.aveid.net/api/oauth/authorize",
  "token_endpoint": "https://api.aveid.net/api/oauth/token",
  "userinfo_endpoint": "https://api.aveid.net/api/oauth/userinfo",
  "jwks_uri": "https://api.aveid.net/.well-known/jwks.json",
  "scopes_supported": ["openid", "profile", "email", "offline_access"],
  "response_types_supported": ["code"],
  "grant_types_supported": [
    "authorization_code",
    "refresh_token",
    "urn:ietf:params:oauth:grant-type:token-exchange"
  ],
  "subject_types_supported": ["public"],
  "id_token_signing_alg_values_supported": ["EdDSA"],
  "token_endpoint_auth_methods_supported": ["client_secret_post", "none"]
}

Next steps

Signing API

Request cryptographic signatures from users

Delegation

Implement token exchange and connector apps

Self-hosting

Deploy your own Ave instance

Testing guide

Complete testing walkthrough with all flows

Build docs developers (and LLMs) love