Passwords

Read this page in the documentation

Passwords A password column is the one field where getting the write path wrong is invisible: the insert succeeds, the row looks right, and the damage only shows up in a breach. So this page is short, and every example on it is executed by tests/password-hashing.test.ts. The shape of it Hashing happens in a beforeCreate / beforeUpdate hook, so the plain text never reaches the database no matter which path wrote the row. You supply the hashing library; the ORM does not bundle one. @Secure is a class decorator naming a field, not a property decorator: @Secure('password') above the class, not @Secure() above the property. addModel() wires the hooks for you — there is no applySecureHooks() call to forget. (It still exists, and is idempotent, for a model you built another way.) Writing a user is then ordinary: And checking one at sign-in: Choosing the adapter An adapter is two functions, so any library works: Two are pre-shaped: Library | Adapter | Install | --- | --- | --- | bcrypt / bcryptjs | createBcryptAdapter(bcrypt, 12) | npm install bcrypt (native) or bcryptjs (pure JS) | argon2 | createArgon2Adapter(argon2) | npm install argon2 | The number is bcrypt's cost factor. 12 is a reasonable default in 2026; raise it as hardware improves, and note that it is deliberately slow — a login endpoint that hashes on every request is doing the right thing. A per-field adapter overrides the global one, which is how a legacy column and a new one can hash differently during a migration: Re-saving a hash does not hash it twice The hook skips a value that already looks like a hash — bcrypt's $2a$/$2b$/ $2y$, argon2's $argon2…, scrypt's $scrypt$. Without that, loading a user, changing their email and saving would hash the hash and lock them out. Turn it off with { skipIfHashed: false } only if you know your stored values do not look like hashes. Changing a password is just a write: Without decorators prorm.define() models take the same hook directly: The second argument is the field list. Hooks registered under a name chain rather than replace each other, and removeHook('beforeCreate', 'hash-password') takes one back off. What this does not do It does not hide the column. findAll() returns the hash like any other field. Exclude it — attributes: { exclude: ['password'] } — or use @CannotView so it never leaves the process by accident. It does not rate-limit sign-ins. Hashing cost slows an attacker per guess; it does not stop them guessing. It does not validate strength. That is a validation rule: @Len([12, 128]) and whatever policy you keep. It is not encryption. A hash is one-way and cannot be read back. For a value you need to recover — an API token you must present later — see the field-encryption tooling in Compliance. Related reading Decorators — the wider decorator set Validation — length and format rules on the same field Hooks — the lifecycle events these are built on Compliance — reversible field encryption, masking, audit