Skip to content

Add passkey authentication support - #1

Open
NicoM77 wants to merge 74 commits into
mainfrom
feat/passkey-authentication-support
Open

Add passkey authentication support#1
NicoM77 wants to merge 74 commits into
mainfrom
feat/passkey-authentication-support

Conversation

@NicoM77

@NicoM77 NicoM77 commented Apr 28, 2026

Copy link
Copy Markdown
Owner

No description provided.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds passkey (WebAuthn) authentication support to the existing Better Auth–based auth system, including database persistence and UI surfaces for managing credentials.

Changes:

  • Add @better-auth/passkey dependency and wire passkey plugins into server (betterAuth) and client (createAuthClient).
  • Introduce Drizzle schema + migration for a new passkey table and relations to usersTable.
  • Add Settings UI for listing/adding/renaming/deleting passkeys and enable passkey autofill sign-in on the login page.

Reviewed changes

Copilot reviewed 10 out of 11 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
package.json Adds passkey package dependency.
bun.lock Locks passkey + transitive deps and bumps Better Auth core version.
app/server/lib/auth.ts Enables the Better Auth passkey plugin with RP config derived from BASE_URL.
app/server/db/schema.ts Adds passkey table schema.
app/server/db/relations.ts Adds user↔passkey relations.
app/drizzle/20260428161759_productive_namor/snapshot.json Captures updated schema snapshot including passkey table.
app/drizzle/20260428161759_productive_namor/migration.sql Creates passkey table and indexes.
app/client/modules/settings/routes/settings.tsx Adds Passkeys section to Settings page.
app/client/modules/settings/components/passkeys-section.tsx New UI for passkey CRUD operations.
app/client/modules/auth/routes/login.tsx Attempts conditional WebAuthn autofill sign-in on login.
app/client/lib/auth-client.ts Registers passkey client plugin.

);
--> statement-breakpoint
CREATE INDEX `passkey_userId_idx` ON `passkey` (`user_id`);--> statement-breakpoint
CREATE INDEX `passkey_credentialID_idx` ON `passkey` (`credential_id`); No newline at end of file

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

credential_id is indexed but not unique. Since credential IDs are expected to be unique in WebAuthn, add a UNIQUE constraint/index here to prevent duplicates and ambiguous credential lookups.

Suggested change
CREATE INDEX `passkey_credentialID_idx` ON `passkey` (`credential_id`);
CREATE UNIQUE INDEX `passkey_credentialID_idx` ON `passkey` (`credential_id`);

Copilot uses AI. Check for mistakes.
Comment on lines +87 to +100
const { error } = await authClient.$fetch("/passkey/update-passkey", {
method: "POST",
body: { id: renameTarget.id, name },
});
setIsRenaming(false);
if (error) {
logger.error(error);
toast.error("Failed to rename passkey", { description: error.message });
return;
}
toast.success("Passkey renamed");
setRenameTarget(null);
setRenameValue("");
await refetch();

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleRename doesn’t guard the authClient.$fetch call with try/finally (or try/catch). If the request throws (network error, non-JSON response, etc.), isRenaming will remain true and the dialog will be stuck in a loading state. Wrap the request in try/finally and ensure setIsRenaming(false) runs even on exceptions (and optionally surface a toast).

Suggested change
const { error } = await authClient.$fetch("/passkey/update-passkey", {
method: "POST",
body: { id: renameTarget.id, name },
});
setIsRenaming(false);
if (error) {
logger.error(error);
toast.error("Failed to rename passkey", { description: error.message });
return;
}
toast.success("Passkey renamed");
setRenameTarget(null);
setRenameValue("");
await refetch();
try {
const { error } = await authClient.$fetch("/passkey/update-passkey", {
method: "POST",
body: { id: renameTarget.id, name },
});
if (error) {
logger.error(error);
toast.error("Failed to rename passkey", { description: error.message });
return;
}
toast.success("Passkey renamed");
setRenameTarget(null);
setRenameValue("");
await refetch();
} catch (err) {
logger.error(err);
toast.error("Failed to rename passkey", {
description: err instanceof Error ? err.message : "Unknown error",
});
} finally {
setIsRenaming(false);
}

Copilot uses AI. Check for mistakes.
}
toast.success("Passkey deleted");
setDeleteTarget(null);
await refetch();

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Manual refetch() here is doing explicit invalidation on the client. The repo relies on automatic invalidation after mutations (AGENTS.md:64-67). Prefer using the standard mutation pattern so the list refreshes automatically without calling refetch().

Copilot uses AI. Check for mistakes.
Comment on lines +160 to +169
<Button
variant="outline"
size="sm"
onClick={() => {
setRenameTarget(p);
setRenameValue(p.name ?? "");
}}
>
<Pencil className="h-4 w-4" />
</Button>

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rename action button is icon-only, but it doesn’t provide an accessible label. Add an aria-label (and optionally a tooltip/title) so screen readers can announce what the control does.

Copilot uses AI. Check for mistakes.
Comment on lines +170 to +172
<Button variant="destructive" size="sm" onClick={() => setDeleteTarget(p)}>
<Trash2 className="h-4 w-4" />
</Button>

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The delete action button is icon-only and currently has no accessible label. Add an aria-label (and optionally a tooltip/title) to make the action discoverable to assistive technologies.

Copilot uses AI. Check for mistakes.
Comment on lines +106 to +118
const { error } = await authClient.$fetch("/passkey/delete-passkey", {
method: "POST",
body: { id: deleteTarget.id },
});
setIsDeleting(false);
if (error) {
logger.error(error);
toast.error("Failed to delete passkey", { description: error.message });
return;
}
toast.success("Passkey deleted");
setDeleteTarget(null);
await refetch();

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleDelete has the same issue as rename: if authClient.$fetch throws, isDeleting never gets reset and the confirmation dialog remains disabled. Use try/finally (and/or try/catch) around the request so state is always restored.

Suggested change
const { error } = await authClient.$fetch("/passkey/delete-passkey", {
method: "POST",
body: { id: deleteTarget.id },
});
setIsDeleting(false);
if (error) {
logger.error(error);
toast.error("Failed to delete passkey", { description: error.message });
return;
}
toast.success("Passkey deleted");
setDeleteTarget(null);
await refetch();
try {
const { error } = await authClient.$fetch("/passkey/delete-passkey", {
method: "POST",
body: { id: deleteTarget.id },
});
if (error) {
logger.error(error);
toast.error("Failed to delete passkey", { description: error.message });
return;
}
toast.success("Passkey deleted");
setDeleteTarget(null);
await refetch();
} catch (err) {
logger.error(err);
toast.error("Failed to delete passkey", {
description: err instanceof Error ? err.message : "Unknown error",
});
} finally {
setIsDeleting(false);
}

Copilot uses AI. Check for mistakes.
toast.success("Passkey added");
setAddDialogOpen(false);
setNewPasskeyName("");
await refetch();

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This manual refetch() is implementing client-side invalidation logic. Per repo guidance, invalidation is handled automatically after mutations (see AGENTS.md:64-67). Prefer using the existing mutation/invalidation pattern (e.g., a typed passkey mutation hook/client method that triggers the automatic invalidation) instead of explicitly refetching here.

Copilot uses AI. Check for mistakes.
toast.success("Passkey renamed");
setRenameTarget(null);
setRenameValue("");
await refetch();

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Manual refetch() here adds client-side invalidation logic. The repo’s convention is to rely on the automatic invalidation setup after mutations (AGENTS.md:64-67). Consider switching this rename flow to the standard mutation path so the cache updates without explicit refetching.

Copilot uses AI. Check for mistakes.
Comment thread app/server/db/schema.ts
Comment on lines +441 to +442
credentialID: text("credential_id").notNull(),
counter: integer("counter").notNull(),

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

credentialID is not constrained to be unique. WebAuthn credential IDs should be globally unique; allowing duplicates can cause ambiguous lookups during assertion and potentially authenticate the wrong account if duplicates ever occur. Add a uniqueness constraint (e.g., unique index on credentialID, or on (userId, credentialID) depending on the expected model) and reflect it in the generated migration.

Copilot uses AI. Check for mistakes.
renovate Bot and others added 4 commits April 29, 2026 21:12
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* feat: pre/post backup webhooks

* fix(hooks): run post when cancelled

* refactor(webhooks): headers as array

* refactor: pr feedback

* refactor: simplify hooks ceremonies

* chore: pr feedbacks

* chore: re-gen migration
* feat: make webhook calls trusted only

* fix: pr feedbacks
@NicoM77
NicoM77 force-pushed the feat/passkey-authentication-support branch from c2677ee to a79937d Compare May 1, 2026 09:38
nicotsx and others added 10 commits May 1, 2026 18:07
* Add repository column sorting

* Make status title correctly centered o nsmaller screens

* Add volumes column sorting

* refactor: use tanstack table for filtering and sorting

* feat: make notifications sortable

* chore: pr feedbacks

---------

Co-authored-by: Antoine Jeanselme <67123340+ajeanselme@users.noreply.github.com>
Co-authored-by: Nicolas Meienberger <github@thisprops.com>
* fix(notifications): persist delivery health status

* fix: pr feedback double update
@NicoM77
NicoM77 force-pushed the feat/passkey-authentication-support branch from a79937d to 660e1b4 Compare May 4, 2026 09:25
nicotsx and others added 11 commits May 4, 2026 17:05
…cotsx#853)

* fix(notifications): enforce allowlist for custom Shoutrrr targets

* fix(notifications): enforce allowlist for email notification targets
* Initial plan

* docs: update Docker image tags to v0.36

Closes nicotsx#856

Agent-Logs-Url: https://github.com/nicotsx/zerobyte/sessions/685fd718-282c-4843-b5bb-082bf8ed0571

Co-authored-by: nicotsx <47644445+nicotsx@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: nicotsx <47644445+nicotsx@users.noreply.github.com>
* feat(agents): create agent registry and service

* fix: mark agent offline only if the session was removed properly

* refactor: centralize agent backup lifecycle state

* refactor: simplify session management

* refactor: move effect / async boundary in one place

* chore: regen migration

* refactor: improve error handling

* chore: pr feedback
…sx#772)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
nicotsx and others added 26 commits May 18, 2026 21:35
* refactor(mutext): persist repository locks in database

* fix: clean up promoted repository lock on queued abort

* fix: throttle repository lock cleanup during polling
* feat(auth): allow skipping forced recovery key download

* refactor: move from session storage to cookie
)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…x#777)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* chore(deps): update dependency bun to v1.3.14

* chore: update bun base docker image

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Nicolas Meienberger <github@thisprops.com>
* fix(deps): update dependency content-disposition to v2

* refactor(content-disposition): use new named export

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Nicolas Meienberger <github@thisprops.com>
)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…tsx#905)

* Initial plan

* docs: update docker compose image tags to v0.37

Agent-Logs-Url: https://github.com/nicotsx/zerobyte/sessions/067f22a5-8223-4fd3-8e9c-c50f6ab2cc6e

Co-authored-by: nicotsx <47644445+nicotsx@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: nicotsx <47644445+nicotsx@users.noreply.github.com>
@nicotsx
nicotsx force-pushed the feat/passkey-authentication-support branch from 51cba3b to a291eaf Compare May 21, 2026 06:03
@nicotsx
nicotsx force-pushed the feat/passkey-authentication-support branch from ca0f2e8 to 97c70ea Compare May 21, 2026 17:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants