Skip to content

Conversation

@lorenzocorallo
Copy link
Member

No description provided.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 26, 2026

Walkthrough

Adds passkey authentication support by introducing a new SQL migration that creates an auth_passkey table with foreign keys and indexes, adds the @better-auth/passkey package dependency, registers the passkey plugin in the auth configuration, and refactors auth schema exports from plural to singular naming conventions.

Changes

Cohort / File(s) Summary
Database Migrations
drizzle/0006_uneven_lord_hawal.sql, drizzle/meta/0006_snapshot.json, drizzle/meta/_journal.json
Creates auth_passkey table with columns (id, name, public_key, user_id, credential_id, counter, device_type, backed_up, transports, created_at, aaguid) and foreign key to auth_users. Adds indexes on auth_passkey(user_id), auth_passkey(credential_id), and indexes on existing tables (auth_accounts(user_id), auth_sessions(user_id), auth_verifications(identifier)). Sets created_at and updated_at to NOT NULL in auth_verifications.
Package Dependencies
package.json
Adds @better-auth/passkey@^1.4.17 dependency.
Auth Schema & Configuration
src/auth/index.ts, src/db/schema/auth/auth.ts
Imports and registers passkey plugin with rpID and rpName. Refactors schema exports from plural (users, sessions, accounts, verifications) to singular (user, session, account, verification). Introduces passkey table with relations and adds relation exports (userRelations, sessionRelations, accountRelations, passkeyRelations). Updates foreign key references to use singular naming and adds explicit onDelete: "cascade" behavior.
Schema Reference Updates
src/db/schema/tg/link.ts, src/routers/tg/link.ts
Updates imports and foreign key references from plural users to singular user to align with auth schema naming convention.
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title accurately reflects the main objective: adding passkey authentication plugin support to the authentication system.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@package.json`:
- Line 41: Update the better-auth dependency in package.json to match the
`@better-auth/passkey` peer requirement: change "better-auth" from ^1.4.15 to
^1.4.17 so it aligns with "@better-auth/passkey": "^1.4.17" and avoids a
peerDependency mismatch; ensure both dependency entries (better-auth and
`@better-auth/passkey`) use the same major.minor.patch range (^1.4.17) and run
npm/yarn install to validate.

In `@src/auth/index.ts`:
- Around line 23-29: The drizzle adapter schema in src/auth/index.ts is missing
the passkey table mapping needed by `@better-auth/passkey`; update the
configuration passed to drizzleAdapter (the database: drizzleAdapter(db, {
provider: "pg", schema: { ... } }) call) to include a passkey property, e.g. add
passkey: SCHEMA.AUTH.passkey alongside user, account, session, and verification
so the adapter knows about the passkey table.
- Around line 18-21: Replace the hard-coded rpID in the passkey(...) call with a
derived hostname from the configured base URL: read env.PUBLIC_URL, parse it
(e.g., via new URL(env.PUBLIC_URL).hostname) and use that value as rpID (with a
safe fallback to the current request origin or a default if parsing fails).
Update the passkey(...) invocation in src/auth/index.ts so rpID is computed at
runtime from env.PUBLIC_URL rather than the literal "polinetwork.org", keeping
rpName as-is.
🧹 Nitpick comments (3)
drizzle/0006_uneven_lord_hawal.sql (1)

1-13: Consider making created_at NOT NULL for consistency.

The created_at column (line 11) is nullable, while other auth tables (auth_users, auth_sessions, auth_accounts) define created_at as NOT NULL. If there's no specific reason for passkeys to have a nullable creation timestamp, consider adding the NOT NULL constraint for consistency.

Suggested fix
-	"created_at" timestamp,
+	"created_at" timestamp NOT NULL,
src/db/schema/auth/auth.ts (2)

85-103: Table naming inconsistency: "passkey" (singular) vs other tables using plural.

Other tables use plural names ("users", "sessions", "accounts", "verifications"), but this table uses singular "passkey". Consider using "passkeys" for consistency across the auth schema.

Suggested fix
 export const passkey = createTable.auth(
-  "passkey",
+  "passkeys",
   {

Note: This would require updating the SQL migration to use auth_passkeys as well.


99-100: Consider making createdAt non-nullable to align with other auth tables.

The passkey table has a nullable createdAt, while all other auth tables (user, session, account, verification) use .notNull(). The absence of updatedAt follows the better-auth passkey plugin schema (which does not include this field), so that's intentional. However, the nullable createdAt inconsistency should be addressed:

Suggested fix
   transports: text("transports"),
-  createdAt: timestamp("created_at"),
+  createdAt: timestamp("created_at").notNull(),
   aaguid: text("aaguid"),

},
"dependencies": {
"@azure/identity": "^4.13.0",
"@better-auth/passkey": "^1.4.17",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

@better-auth/passkey peerDependencies better-auth compatibility

💡 Result:

@better-auth/passkey is version-pinned to better-auth via peerDependencies, so you generally need to keep the versions in lockstep.

For example, @better-auth/passkey@1.4.15 declares these peer deps (must be satisfied by your app, not installed automatically):

  • better-auth: 1.4.15
  • @better-auth/core: 1.4.15
  • @better-auth/utils: 0.3.0
  • @better-fetch/fetch: 1.1.21
  • better-call: 1.1.8
  • nanostores: ^1.0.1 [1]

Compatibility rule of thumb: use the same version number for better-auth, @better-auth/core, and @better-auth/passkey (e.g., all 1.4.15). If your project is on a newer better-auth (Snyk reports 1.4.17 as latest for better-auth), you’ll likely need the matching @better-auth/passkey release (or you’ll hit peer-dep conflicts). [2]

Where to check your exact version’s peer deps: the plugin’s published package.json on jsDelivr for that specific version. [1]

Sources: [1] [2]


Align better-auth version with @better-auth/passkey.

@better-auth/passkey is version-pinned to better-auth via peerDependencies and requires matching versions. Adding @better-auth/passkey ^1.4.17 while better-auth is ^1.4.15 risks a peer dependency mismatch—if better-auth resolves to 1.4.15, the passkey package will fail due to incompatible peer dependencies. Update better-auth to ^1.4.17 to ensure version alignment.

🤖 Prompt for AI Agents
In `@package.json` at line 41, Update the better-auth dependency in package.json
to match the `@better-auth/passkey` peer requirement: change "better-auth" from
^1.4.15 to ^1.4.17 so it aligns with "@better-auth/passkey": "^1.4.17" and
avoids a peerDependency mismatch; ensure both dependency entries (better-auth
and `@better-auth/passkey`) use the same major.minor.patch range (^1.4.17) and run
npm/yarn install to validate.

Comment on lines +18 to +21
passkey({
rpID: "polinetwork.org",
rpName: "PoliNetwork APS",
}),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's read the src/auth/index.ts file to see the full context
cat -n src/auth/index.ts | head -50

Repository: PoliNetworkOrg/backend

Length of output: 1888


🏁 Script executed:

# Check if env.PUBLIC_URL is defined and used elsewhere
rg "PUBLIC_URL" -A 2 -B 2

Repository: PoliNetworkOrg/backend

Length of output: 533


🏁 Script executed:

# Look for `@better-auth` package info and configuration patterns
cat -n package.json | grep -A 5 -B 5 "better-auth"

Repository: PoliNetworkOrg/backend

Length of output: 782


🏁 Script executed:

# Search for any other passkey or rpID configuration in the codebase
rg "rpID|rpName" -A 2 -B 2

Repository: PoliNetworkOrg/backend

Length of output: 280


🌐 Web query:

@better-auth passkey rpID configuration requirements

💡 Result:

For Better Auth’s @better-auth/passkey plugin, rpID must follow standard WebAuthn “Relying Party ID” rules:

  • Set rpID to a domain name that matches your auth server’s origin domain (the public site/domain users are authenticating to). localhost is allowed for local development. [1]
  • It must be an “effective domain” (registrable domain) or a subdomain of it: you may drop labels from the left, e.g. www.example.comwww.example.com or example.com. [1]
  • It cannot be a public suffix / eTLD (e.g., com is invalid). [1]
  • Related config: set origin to the exact origin where your Better Auth server is hosted (scheme + host + optional port), with no trailing slash. [1]

Sources: Better Auth Passkey plugin docs. [1]


🌐 Web query:

WebAuthn rpID effective domain matching requirement

💡 Result:

In WebAuthn, the RP ID (rpId) must domain-match the calling page’s origin as follows:

  • The RP ID is a valid domain string (host name only — no scheme, port, or path). [1]
  • By default, the RP ID is the caller origin’s effective domain. [1]
  • If you override it, the RP ID must be either:
    • exactly equal to the origin’s effective domain, or
    • a registrable domain suffix of the origin’s effective domain (i.e., you can broaden scope to a parent registrable domain, but you can’t choose a sibling or an unrelated domain). [1]
  • The origin’s scheme must be https (port is unrestricted). [1]
  • During authentication, the authenticator/browser ensures the ceremony uses the same rpId the credential was registered under. [1][2]

Example (from the spec): for origin https://login.example.com:1337, valid RP IDs are login.example.com (default) and example.com, but not m.login.example.com and not com. [1]

Sources:
[1] W3C WebAuthn Level 2 Recommendation (RP ID / effective domain + registrable-suffix rules) (w3.org)
[2] MDN: PublicKeyCredentialRequestOptions.rpId (must match credential’s rpId; defaults to origin domain) (developer.mozilla.org)

Citations:


🏁 Script executed:

# Check if there's any documentation or comments about passkey/rpID in the repo
rg "passkey|rpID|rpName" -B 5 -A 5 --type ts --type js

Repository: PoliNetworkOrg/backend

Length of output: 3073


🏁 Script executed:

# Check the betterAuth configuration docs or types
rg "baseURL|trustedOrigins" src/auth/index.ts -B 3 -A 3

Repository: PoliNetworkOrg/backend

Length of output: 239


🏁 Script executed:

# Look for any test files or configuration examples
find . -name "*.ts" -o -name "*.js" | xargs grep -l "passkey" 2>/dev/null | head -10

Repository: PoliNetworkOrg/backend

Length of output: 114


Derive rpID from baseURL to prevent WebAuthn registration failures across environments.

Hard-coding rpID to "polinetwork.org" conflicts with the configurable baseURL (set to env.PUBLIC_URL). Per WebAuthn spec, rpID must match the origin's effective domain—if PUBLIC_URL points to localhost, a staging domain, or any other host during development/preview, the hard-coded rpID will cause passkey registration to fail. Extract the hostname from env.PUBLIC_URL to make rpID match the active origin.

Example fix
+const rpID = new URL(env.PUBLIC_URL).hostname
+
 passkey({
-  rpID: "polinetwork.org",
+  rpID,
   rpName: "PoliNetwork APS",
 }),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
passkey({
rpID: "polinetwork.org",
rpName: "PoliNetwork APS",
}),
const rpID = new URL(env.PUBLIC_URL).hostname
passkey({
rpID,
rpName: "PoliNetwork APS",
}),
🤖 Prompt for AI Agents
In `@src/auth/index.ts` around lines 18 - 21, Replace the hard-coded rpID in the
passkey(...) call with a derived hostname from the configured base URL: read
env.PUBLIC_URL, parse it (e.g., via new URL(env.PUBLIC_URL).hostname) and use
that value as rpID (with a safe fallback to the current request origin or a
default if parsing fails). Update the passkey(...) invocation in
src/auth/index.ts so rpID is computed at runtime from env.PUBLIC_URL rather than
the literal "polinetwork.org", keeping rpName as-is.

Comment on lines 23 to +29
database: drizzleAdapter(db, {
provider: "pg",
schema: {
user: SCHEMA.AUTH.users,
account: SCHEMA.AUTH.accounts,
session: SCHEMA.AUTH.sessions,
verification: SCHEMA.AUTH.verifications,
user: SCHEMA.AUTH.user,
account: SCHEMA.AUTH.account,
session: SCHEMA.AUTH.session,
verification: SCHEMA.AUTH.verification,
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

@better-auth passkey drizzle adapter schema configuration

💡 Result:

To use @better-auth/passkey with the Drizzle adapter, you mainly need (1) the passkey plugin enabled in your Better Auth config and (2) a Drizzle schema that includes the required passkey table, passed into drizzleAdapter.

1) Configure Better Auth + Passkey + Drizzle adapter (with schema)

// auth.ts
import { betterAuth } from "better-auth";
import { passkey } from "@better-auth/passkey";
import { drizzleAdapter } from "better-auth/adapters/drizzle";

import { db } from "./drizzle";
import * as schema from "./schema"; // must export `passkey` table too

export const auth = betterAuth({
  plugins: [passkey()],
  database: drizzleAdapter(db, {
    provider: "pg", // or "sqlite" / "mysql"
    schema,
  }),
});

If your Drizzle tables use different names (e.g., users instead of user), you must map them in the adapter schema (or change Better Auth modelName). [1]

2) Generate/migrate the schema (recommended)

Better Auth can generate the needed Drizzle schema (including plugin tables like passkey) based on your config/plugins:

  • npx @better-auth/cli@latest generate (generate schema)
  • or npx @better-auth/cli migrate (apply migrations) [1] [2]

3) Ensure the Passkey table exists in your DB/schema

The passkey plugin requires a passkey table (with fields like publicKey, userId, credentialID, etc.). If you don’t use the CLI, you must create it yourself in Drizzle. [2]

Sources: Drizzle adapter docs (schema + mapping) [1]; Passkey plugin docs (required table + migration/generate) [2].


🏁 Script executed:

rg "SCHEMA\.AUTH\." src/ -A 2 -B 2 | head -50

Repository: PoliNetworkOrg/backend

Length of output: 735


🏁 Script executed:

rg "export.*SCHEMA|const SCHEMA" src/ -B 2 -A 10 | head -80

Repository: PoliNetworkOrg/backend

Length of output: 1647


🏁 Script executed:

rg "authSchema|auth.*schema" src/db/ -B 5 -A 15

Repository: PoliNetworkOrg/backend

Length of output: 840


🏁 Script executed:

cat src/db/schema/auth.ts

Repository: PoliNetworkOrg/backend

Length of output: 119


🏁 Script executed:

fd "auth" src/db/schema/

Repository: PoliNetworkOrg/backend

Length of output: 112


🏁 Script executed:

ls -la src/db/schema/auth/ && echo "---" && wc -l src/db/schema/auth/*.ts

Repository: PoliNetworkOrg/backend

Length of output: 371


🏁 Script executed:

cat src/db/schema/auth/index.ts && echo "---" && cat src/db/schema/auth/auth.ts

Repository: PoliNetworkOrg/backend

Length of output: 4134


Add passkey table mapping to drizzle adapter schema.

The @better-auth/passkey plugin requires an explicit passkey table mapping in the drizzle adapter configuration. The passkey table exists in your schema definition but is missing from the adapter setup in src/auth/index.ts.

Fix required
   schema: {
     user: SCHEMA.AUTH.user,
     account: SCHEMA.AUTH.account,
     session: SCHEMA.AUTH.session,
     verification: SCHEMA.AUTH.verification,
+    passkey: SCHEMA.AUTH.passkey,
   },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
database: drizzleAdapter(db, {
provider: "pg",
schema: {
user: SCHEMA.AUTH.users,
account: SCHEMA.AUTH.accounts,
session: SCHEMA.AUTH.sessions,
verification: SCHEMA.AUTH.verifications,
user: SCHEMA.AUTH.user,
account: SCHEMA.AUTH.account,
session: SCHEMA.AUTH.session,
verification: SCHEMA.AUTH.verification,
database: drizzleAdapter(db, {
provider: "pg",
schema: {
user: SCHEMA.AUTH.user,
account: SCHEMA.AUTH.account,
session: SCHEMA.AUTH.session,
verification: SCHEMA.AUTH.verification,
passkey: SCHEMA.AUTH.passkey,
},
🤖 Prompt for AI Agents
In `@src/auth/index.ts` around lines 23 - 29, The drizzle adapter schema in
src/auth/index.ts is missing the passkey table mapping needed by
`@better-auth/passkey`; update the configuration passed to drizzleAdapter (the
database: drizzleAdapter(db, { provider: "pg", schema: { ... } }) call) to
include a passkey property, e.g. add passkey: SCHEMA.AUTH.passkey alongside
user, account, session, and verification so the adapter knows about the passkey
table.

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.

2 participants