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.
User table: 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.OAuthAccount table: 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./lib/auth/passwords.ts exposes hash(plain) → string and verify(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.ts exposes createUser({ 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 with passwordHash: null and link), linkOAuthAccount({ userId, provider, providerAccountId, ... }). Email lookups are case-insensitive./lib/auth/index.ts (from 1.1.2) to point at the new schema.bcrypt/argon2 imports anywhere else in the codebase (lint rule or just convention; spot-check).findOrCreateOAuthUser with matching email links to existing user; findOrCreateOAuthUser with no matching email creates a new user with null passwordHash.README.md — Prisma conventions in this repo/prisma/schema.prisma — existing schema (placeholder from 1.0.2)