1.1.3 User + OAuthAccount schema + password hashing
Estimate: 15m · Depends on: 1.0.2, 1.1.2
Add the user table and the oauth_account account-linking table to the Prisma schema, wire up the password-hashing helper, and create a clean repository layer so the rest of the codebase never touches password hashing or OAuth account state directly. Better-Auth (configured in 1.1.2) consumes this schema as its persistence layer.
Why two tables and not one: a single user can have both an email/password credential and linked OAuth accounts (via auto-linking on matching email). Storing OAuth account state as rows in a separate oauth_account table — keyed by (provider, providerAccountId) — is Better-Auth's recommended shape and keeps the user table free of provider-specific fields. The User's passwordHash is nullable: Google-only signups never set one.
Why hashing helper is its own concern: Password handling is a security footgun. By centralizing it in one file (/lib/auth/passwords.ts) with both hash() and verify(), the rest of the codebase can never accidentally compare a plaintext password — there's only one way to do it.
What you'll do: Edit /prisma/schema.prisma to add the User and OAuthAccount models (using Better-Auth's expected field names so the Prisma adapter just works), generate and apply a migration, write /lib/auth/passwords.ts using argon2 (preferred) or bcrypt (fallback if argon2 isn't viable), and provide /lib/users/repo.ts with createUser, findUserByEmail, verifyPassword, findOrCreateOAuthUser (used by 1.1.4 for Google sign-in auto-link), and linkOAuthAccount. Wire the Better-Auth instance from 1.1.2 to the Prisma adapter pointing at this schema.
Acceptance criteria
Usertable:id(uuid),email(unique, case-insensitive),passwordHash(nullable — null for Google-only signups),emailVerifiedAt(nullable; set automatically for Google signups since Google has already verified),name,image(nullable; populated from Google profile when available),createdAt,updatedAt.OAuthAccounttable:id,userId(FK → User),provider(e.g."google"),providerAccountId(the Google user ID),accessToken(nullable),refreshToken(nullable),expiresAt(nullable),createdAt,updatedAt. Composite unique on(provider, providerAccountId). Field names match Better-Auth's Prisma-adapter conventions so no custom mapping is required.- Migration created and applies cleanly on a fresh database.
/lib/auth/passwords.tsexposeshash(plain) → stringandverify(plain, hash) → boolean. Uses argon2id with sensible parameters (memoryCost: 19MB, timeCost: 2, parallelism: 1) or bcrypt cost 12 if argon2 isn't viable./lib/users/repo.tsexposescreateUser({ email, password, name? }),findUserByEmail(email),verifyPassword(email, plain),findOrCreateOAuthUser({ provider, providerAccountId, email, name?, image? })(auto-link semantics: if an existing user matches on email, link the OAuth account to them; otherwise create a new user withpasswordHash: nulland link),linkOAuthAccount({ userId, provider, providerAccountId, ... }). Email lookups are case-insensitive.- Better-Auth's Prisma adapter is wired in
/lib/auth/index.ts(from 1.1.2) to point at the new schema. - No raw
bcrypt/argon2imports anywhere else in the codebase (lint rule or just convention; spot-check). - Tests: hash-and-verify roundtrip; invalid password returns false; duplicate-email password creation fails with a typed error;
findOrCreateOAuthUserwith matching email links to existing user;findOrCreateOAuthUserwith no matching email creates a new user with nullpasswordHash.
Context refs
README.md— Prisma conventions in this repo/prisma/schema.prisma— existing schema (placeholder from 1.0.2)- OWASP password-storage cheat-sheet (URL, fetched at prompt-gen)