n2k
n2k13h ago

Using the Better Auth component in "use node" context

Coming from a different database, I'm in the process of migrating over to Convex (cloud, not self-hosted), and using Better Auth Convex component (local install) as auth solution. Ideally I want to re-use the users's password hash, which are hashed using argon2id, this requires the node runtime instead of the default v8 runtime Convex uses. When adding "use node" at the top of the /convex/auth.ts where I the betterAuth instance with the password hash override like so:
"use node";

export const createAuth = (ctx: GenericCtx<DataModel>, { optionsOnly } = { optionsOnly: false }) => {
return betterAuth({
...
emailAndPassword: {
enabled: true,
minPasswordLength: 12,
password: {
hash: async (password) => await argon2.hash(password),
verify: async ({ hash, password }) => await argon2.verify(hash, password),
},
},
...
});
};
"use node";

export const createAuth = (ctx: GenericCtx<DataModel>, { optionsOnly } = { optionsOnly: false }) => {
return betterAuth({
...
emailAndPassword: {
enabled: true,
minPasswordLength: 12,
password: {
hash: async (password) => await argon2.hash(password),
verify: async ({ hash, password }) => await argon2.verify(hash, password),
},
},
...
});
};
this will causes the following errors (not only from node:crypto also node:util fs, path and os)
✘ [ERROR] Could not resolve "node:crypto"

node_modules/argon2/argon2.cjs:1:49:
1 │ const { randomBytes, timingSafeEqual } = require("node:crypto");
╵ ~~~~~~~~~~~~~

The package "node:crypto" wasn't found on the file system but is built into node. Are you trying
to bundle for node? You can use "platform: 'node'" to do that, which will remove this error.

It looks like you are using Node APIs from a file without the "use node" directive.
Split out actions using Node.js APIs like this into a new file only containing actions that uses "use node" so these actions will run in a Node.js environment.
For more information see https://docs.convex.dev/functions/runtimes#nodejs-runtime
✘ [ERROR] Could not resolve "node:crypto"

node_modules/argon2/argon2.cjs:1:49:
1 │ const { randomBytes, timingSafeEqual } = require("node:crypto");
╵ ~~~~~~~~~~~~~

The package "node:crypto" wasn't found on the file system but is built into node. Are you trying
to bundle for node? You can use "platform: 'node'" to do that, which will remove this error.

It looks like you are using Node APIs from a file without the "use node" directive.
Split out actions using Node.js APIs like this into a new file only containing actions that uses "use node" so these actions will run in a Node.js environment.
For more information see https://docs.convex.dev/functions/runtimes#nodejs-runtime
this essentially means I can't override the password hash?
10 Replies
Convex Bot
Convex Bot13h ago
Thanks for posting in <#1088161997662724167>. Reminder: If you have a Convex Pro account, use the Convex Dashboard to file support tickets. - Provide context: What are you trying to achieve, what is the end-user interaction, what are you seeing? (full error message, command output, etc.) - Use search.convex.dev to search Docs, Stack, and Discord all at once. - Additionally, you can post your questions in the Convex Community's <#1228095053885476985> channel to receive a response from AI. - Avoid tagging staff unless specifically instructed. Thank you!
Justin
Justin13h ago
I don't think you can use use node in /convex/auth.ts. That defines auth for your whole application, so it can't be isolated to the node-compatible runtime. Try creating a separate file with node convex actions for hashing and verifying passwords, then calling them via ctx.runAction in your auth file.
Sara
Sara13h ago
mmmmm, so like justin mentions, use node is made for you to use actions on, convex works with crypto without needing to import it at the top, maybe you could use that?
n2k
n2kOP12h ago
yeah that's what I figured, having "use node" at the top of /convex/auth.ts would basically mean that most Convex functions (doing auth checks) will need to run in node environment. this also isn't how the node runtime works for Convex, in the docs they explain that only actions will run in the node environment, and is not for queries or mutations. This is intended to be extract/separated into its own file with "use node" at the top, so that only those actions are run in the node environment. Making me come to the conclusion: I can't override the password hashing (at least in its current implementation) for Better Auth as Convex component. Or is it possible that I can run an action from within the /convex/auth.ts inside the betterAuth({...}) constructor? Hmm I'll try that it's not that I'm using/importing crypto myself, it's used by the package argon2 that I import into /convex/auth.ts
Sara
Sara12h ago
Yeah i understand that Yeah scratch that
Justin
Justin12h ago
Yes, you can run actions from within the auth.ts file. For example:
betterAuth({
...
emailAndPassword: {
enabled: true,
requireEmailVerification: false,
sendResetPassword: async ({ user, url }) => {
const actionCtx = ctx as ActionCtx;
await actionCtx.scheduler.runAfter(
0,
internal.email.nodeActions.sendEmail,
{
to: user.email,
type: "RESET_PWD",
props: {
resetUrl: url,
userEmail: user.email,
userName: user.name,
},
},
);
},
},
...
});
betterAuth({
...
emailAndPassword: {
enabled: true,
requireEmailVerification: false,
sendResetPassword: async ({ user, url }) => {
const actionCtx = ctx as ActionCtx;
await actionCtx.scheduler.runAfter(
0,
internal.email.nodeActions.sendEmail,
{
to: user.email,
type: "RESET_PWD",
props: {
resetUrl: url,
userEmail: user.email,
userName: user.name,
},
},
);
},
},
...
});
In this case, it's a scheduled action. But ActionCtx will also expose runAction.
n2k
n2kOP12h ago
I have created separate node functions for the hash and verify in /convex/password.ts like so:
"use node";

import argon2 from "argon2";
import { v } from "convex/values";
import { internalAction } from "@/convex/_generated/server";

export const hash = internalAction({
args: { password: v.string() },
returns: v.string(),
handler: async (_, { password }) => await argon2.hash(password),
});

export const verify = internalAction({
args: { hash: v.string(), password: v.string() },
returns: v.boolean(),
handler: async (_, { hash, password }) => await argon2.verify(hash, password),
});
"use node";

import argon2 from "argon2";
import { v } from "convex/values";
import { internalAction } from "@/convex/_generated/server";

export const hash = internalAction({
args: { password: v.string() },
returns: v.string(),
handler: async (_, { password }) => await argon2.hash(password),
});

export const verify = internalAction({
args: { hash: v.string(), password: v.string() },
returns: v.boolean(),
handler: async (_, { hash, password }) => await argon2.verify(hash, password),
});
but during auto deployment of functions (after saving the file) I get this error:
✖ Error: Unable to start push to https://[REDACTED].convex.cloud
✖ Error fetching POST https://[REDACTED].convex.cloud/api/deploy2/start_push 400 Bad Request: InvalidModules: Hit an error while pushing:
Loading the pushed modules encountered the following
error:
Uncaught Failed to analyze password.js: No native build was found for platform=linux arch=arm64 runtime=node abi=127 uv=1 armv=8 libc=glibc node=22.20.0
loaded from: /var/task

at v.resolve.v.path [as path] (../node_modules/node-gyp-build/node-gyp-build.js:59:62)
at load (../node_modules/node-gyp-build/node-gyp-build.js:22:0)
at <anonymous> (../node_modules/argon2/argon2.cjs:14:25)
at <anonymous> (convex:/user/password.js:13:31)
at <anonymous> (../convex/password.ts:2:19)
✖ Error: Unable to start push to https://[REDACTED].convex.cloud
✖ Error fetching POST https://[REDACTED].convex.cloud/api/deploy2/start_push 400 Bad Request: InvalidModules: Hit an error while pushing:
Loading the pushed modules encountered the following
error:
Uncaught Failed to analyze password.js: No native build was found for platform=linux arch=arm64 runtime=node abi=127 uv=1 armv=8 libc=glibc node=22.20.0
loaded from: /var/task

at v.resolve.v.path [as path] (../node_modules/node-gyp-build/node-gyp-build.js:59:62)
at load (../node_modules/node-gyp-build/node-gyp-build.js:22:0)
at <anonymous> (../node_modules/argon2/argon2.cjs:14:25)
at <anonymous> (convex:/user/password.js:13:31)
at <anonymous> (../convex/password.ts:2:19)
I guess there's a mismatch / misconfiguration for the node version? 🤔 could it be related to my convex.json, where I set the version to 22, so it matches the version i have on vercel?
{
"$schema": "https://raw.githubusercontent.com/get-convex/convex-backend/refs/heads/main/npm-packages/convex/schemas/convex.schema.json",
"codegen": {
"fileType": "ts"
},
"node": {
"nodeVersion": "22"
}
}
{
"$schema": "https://raw.githubusercontent.com/get-convex/convex-backend/refs/heads/main/npm-packages/convex/schemas/convex.schema.json",
"codegen": {
"fileType": "ts"
},
"node": {
"nodeVersion": "22"
}
}
man, I feel i'm in the depths of Convex 😅 brb lunch
Justin
Justin12h ago
I wonder if you need to mark argon2 as an external package? https://docs.convex.dev/functions/bundling#external-packages
Bundling | Convex Developer Hub
How Convex bundles and optimizes your function code
n2k
n2kOP10h ago
YES! That was the last missing piece! Thanks @Justin 🙌 Or well, at least the upload of the functions work, and no TS errors exist 😉 so for completeness, the resulting code for betterAuth config is as follows (see below), this is using the internalActions from /convex/password.ts (see above), and the convex.json setting "externalPackages": ["argon2"]
import { type GenericCtx } from "@convex-dev/better-auth";
import { requireActionCtx } from "@convex-dev/better-auth/utils";
import { betterAuth } from "better-auth";
import type { DataModel } from "./_generated/dataModel";

export const createAuth = (ctx: GenericCtx<DataModel>, { optionsOnly } = { optionsOnly: false }) => {
return betterAuth({
...
emailAndPassword: {
enabled: true,
minPasswordLength: 12,
password: {
hash: async (password) => await requireActionCtx(ctx).runAction(internal.password.hash, { password }),
verify: async ({ hash, password }) => await requireActionCtx(ctx).runAction(internal.password.verify, { hash, password }),
},
},
...
})
);
import { type GenericCtx } from "@convex-dev/better-auth";
import { requireActionCtx } from "@convex-dev/better-auth/utils";
import { betterAuth } from "better-auth";
import type { DataModel } from "./_generated/dataModel";

export const createAuth = (ctx: GenericCtx<DataModel>, { optionsOnly } = { optionsOnly: false }) => {
return betterAuth({
...
emailAndPassword: {
enabled: true,
minPasswordLength: 12,
password: {
hash: async (password) => await requireActionCtx(ctx).runAction(internal.password.hash, { password }),
verify: async ({ hash, password }) => await requireActionCtx(ctx).runAction(internal.password.verify, { hash, password }),
},
},
...
})
);
Oh, this makes me feel great about this. There's a learning curve to Convex, but so far no hard blockers yet! Thanks again 😄

Did you find this page helpful?