Hashing
@rudderjs/hash is the framework's password hashing facade. It ships with bcrypt (pure JavaScript, default) and argon2 (native, opt-in) drivers, exposes a small Hash facade, and integrates with @rudderjs/auth for password verification on login.
Setup
pnpm add @rudderjs/hashFor argon2, also install the native package:
pnpm add argon2// config/hash.ts
import type { HashConfig } from '@rudderjs/hash'
export default {
driver: 'bcrypt',
bcrypt: { rounds: 12 },
argon2: { memory: 65536, time: 3, threads: 4 },
} satisfies HashConfigThe provider is auto-discovered. When @rudderjs/auth is also installed, HashProvider must boot before AuthProvider — auto-discovery orders this correctly via the infrastructure stage.
The Hash facade
import { Hash } from '@rudderjs/hash'
const hashed = await Hash.make('secret-password')
const ok = await Hash.check('secret-password', hashed) // true
if (Hash.needsRehash(hashed)) {
// Cost parameters changed since this hash was created
const fresh = await Hash.make('secret-password')
// persist fresh
}| Method | Returns | Description |
|---|---|---|
Hash.make(value) |
Promise<string> |
Hash a plain-text value |
Hash.check(value, hashed) |
Promise<boolean> |
Verify a plain-text value against a hash |
Hash.needsRehash(hashed) |
boolean |
Sync check: does the hash use the current cost parameters? |
needsRehash() parses the hash format directly without performing any cryptographic work, so it's safe to call on every login. The typical pattern is: verify on login, rehash if needsRehash() returns true, persist the new hash.
Drivers
Bcrypt (default)
Uses bcryptjs — pure JavaScript, no native compilation. The rounds parameter is the cost factor; each increment doubles hashing time.
import { BcryptDriver } from '@rudderjs/hash'
const driver = new BcryptDriver({ rounds: 14 })12 is a reasonable default in 2026. 14+ for high-value targets.
Argon2
Uses the native argon2 package with argon2id. Argon2 is the OWASP-recommended algorithm for new applications.
import { Argon2Driver } from '@rudderjs/hash'
const driver = new Argon2Driver({
memory: 65536, // 64 MB
time: 3, // iterations
threads: 4, // parallelism
})The driver records each parameter in the encoded hash, so needsRehash() can compare them against the current config without re-hashing.
Picking a driver
| Bcrypt | Argon2 | |
|---|---|---|
| Native dep | No (bcryptjs) |
Yes (argon2) |
| Memory hardness | No | Yes (configurable) |
| OWASP recommendation (2026) | Acceptable | Preferred for new apps |
| Performance on edge runtimes | Works | Native binding limits portability |
Use bcrypt unless you specifically need argon2's memory hardness — e.g. high-value password databases or compliance requirements. The driver is swappable: change driver: 'argon2' and call Hash.needsRehash() on login to migrate hashes lazily.
Custom drivers
Implement HashDriver to plug in scrypt, PBKDF2, or a remote hashing service:
import type { HashDriver } from '@rudderjs/hash'
import { HashRegistry } from '@rudderjs/hash'
class ScryptDriver implements HashDriver {
async make(value: string): Promise<string> { /* ... */ }
async check(value: string, hashed: string): Promise<boolean> { /* ... */ }
needsRehash(hashed: string): boolean { /* ... */ }
}
HashRegistry.set(new ScryptDriver())When to use hashing vs. encryption
- Hashing is one-way. Use it for data you need to verify but never read back — passwords, API keys, file fingerprints.
- Encryption is reversible. Use it for data you need to read back — encrypted columns, signed cookies. See Encryption.
Pitfalls
- Provider order.
HashProvidermust boot beforeAuthProvider— auth depends on the hash driver for password verification. Auto-discovery handles this; manual orderings need it spelled out. - Argon2 native build failures. Some platforms (Cloudflare Workers, Deno without
--allow-ffi) can't load native modules. Use bcrypt for portable deployments. - Increasing rounds in production. Raise the cost factor and ship — old hashes still validate, and
Hash.needsRehash()flags them for migration on next login.