Components
ElvixAppPasskeys

ElvixAppPasskeys

Per-app passkey manager. Lists and lets the user add or remove WebAuthn passkeys scoped to ONE application — the credential can only sign them in to this app. Account-level passkeys (added on `/account/security`) work everywhere and are NOT shown here.

Passkeys for Acme

Phishing-proof. These passkeys can only sign you in to Acme. Account-level passkeys you added on /account/security work here too and are managed there.

app/account/apps/[appId]/page.tsx
import {
ElvixAppPasskeys,
ElvixCard,
ElvixProvider,
} from "@elvix.is/sdk/react";
// /account/apps/[appId]/page.tsx — host renders one ElvixAppPasskeys
// per app pane. The provider applies the app's brand color via CSS
// vars so the add button + row icons match the app brand chord.
export default async function AccountAppPage({ params }: { params: { appId: string } }) {
const { appId } = await params;
// ... fetch the app + brand ...
return (
<ElvixProvider theme="light" brand={appBrand} baseUrl="">
<ElvixAppPasskeys
appId={appId}
appName={app.name}
onResult={(r) => {
if (r.ok && r.kind === "added") console.log("registered");
if (r.ok && r.kind === "removed") console.log("removed", r.passkeyId);
}}
/>
</ElvixProvider>
);
}

Props

PropTypeRequiredDefaultDescription
appIdstringoptionalfrom <ElvixProvider> bootstrapDatabase id of the application this card lists passkeys for. Falls back to the app id surfaced by <ElvixProvider clientId>'s bootstrap envelope.
appNamestringoptionalfrom <ElvixProvider> bootstrapDisplay name shown in the heading, subtitle, and "Add a passkey for X" CTA. Overrides the bootstrap-derived name (handy on first-party elvix pages that already have the app row in scope).
onResult(r: ElvixAppPasskeysResult) => voidoptionalnoneTerminal callback. Fires for both add ({ ok: true, kind: "added" }) and remove ({ ok: true, kind: "removed", passkeyId }) success states, plus typed failures. ResponseDto-shape.
onAdded() => voidoptionalnoneShortcut fired ONLY when a passkey successfully registers. Hosts that need to call router.refresh() for their own server-rendered list wire this.
onRemoved(passkeyId: string) => voidoptionalnoneShortcut fired ONLY when a passkey is successfully removed. Symmetric with onAdded.
classNamestringoptional""Extra classes merged onto the outer elvix-card.
width / height / etc.size propsoptionalautoStandard <Elvix*> size knobs. See `ElvixSizeProps`.

App-scoped vs account-level. Passkeys with applicationId=null (added on /account/security) sign the user in to every app they belong to. Passkeys registered through <ElvixAppPasskeys> are bound to one app's id by the elvix register/start challenge — picking the credential on another app's sign-in screen returns credential_not_for_app. The WebAuthn RP (elvix.is) is identical for both flavors; the scope is an elvix-level policy filter.

Brand chord. The add button and row icons paint from var(--elvix-primary) installed by <ElvixProvider brand>. Mount the provider with the per-app brand colors (e.g. via the elvix resolveBrandColors helper on first-party pages) so the card feels like part of that app, not the elvix shell.

Endpoints used. GET /api/account/apps/<appId>/passkeys for the list, DELETE /api/account/apps/<appId>/passkeys?passkeyId=<id> for removal, and the standard /api/auth/passkey/register/start + /finish with an extra applicationId body field for registration. All three require the signed-in account session and a non-deleted membership in the named app.

Cross-origin support. Same architecture as the rest of the SDK: the cross-origin interceptor attaches Authorization: Bearer <token> and rewrites /api/account/... calls to https://elvix.is/... automatically when the host is on a customer origin. Customers who whitelist their origin in elvix's Console and mount <ElvixProvider> get the full per-app passkey UX without any extra wiring.

Result shape

``ts type ElvixAppPasskeysResult = | { ok: true; kind: "added" } | { ok: true; kind: "removed"; passkeyId: string } | { ok: false; error: string; message?: string }; ``

Related