User Model
Why NestAuthUser only stores auth fields, and how to link your AppUser to it.
This is the most important page in the docs. Read it once and the rest falls into place.
The split
Nest Auth deliberately keeps two user concepts separate:
| Table | Owned by | Holds |
|---|---|---|
NestAuthUser (nest_auth_users) | The library | email, phone, passwordHash, isMfaEnabled, metadata, timestamps |
AppUser (your name, your table) | You | Anything else: firstName, lastName, gender, avatarUrl, dob, referredById, subscriptionTier, billing IDs… |
The two are linked by a single column on AppUser:
Why split them at all
- Library upgrades stay safe. When
@ackplus/nest-authadds a column to its user table, your business columns are untouched. - Security blast radius is small. Your domain code reads from
AppUserfor almost everything; the auth columns (password hash, MFA secret) live in a table most of your code never queries. - Multi-tenancy is cleaner. A single
NestAuthUsercan have oneAppUserper tenant, with different roles and profile data per tenant. - The library doesn't care about your domain. It doesn't need to know what a
subscriptionTieris.
When you don't need AppUser
If your "extra" data is genuinely auth-shaped — a couple of flags, a preference, a feature toggle — use the metadata: Record<string, any> JSON column on NestAuthUser directly:
Reach for an AppUser table when you start having relations (orders, posts, referrals), or when querying user data turns into a JSON traversal.
Creating the AppUser automatically on signup
The canonical pattern is an event listener. The library emits UserRegisteredEvent after a NestAuthUser is created — your listener reads any extra signup fields off the payload and writes the matching AppUser.
The signup payload is open-ended ([key: string]: any on ISignupRequest), so anything the frontend sends — firstName, marketingSource, inviteToken — lands on the event.
Worked example: referral codes
Frontend sends a referralCode along with the signup. The listener looks up the referrer and sets a referredBy relation on the new AppUser, then credits the referrer.
A full runnable version of this lives in the user-registered listener recipe.
Putting AppUser fields into the session payload
If your frontend wants firstName available on useUser() without a separate fetch, use the user.getSessionUserData hook. It runs once when a session is created and its return value is stored on the session row.
Now useUser() on the client returns those fields without an extra round trip.
Cleaning up on user deletion
Set the foreign key on AppUser to cascade so deleting a NestAuthUser deletes the matching AppUser:
If you'd rather soft-delete AppUser and keep its row for audit purposes, listen for the USER_DELETED event instead and run your own cleanup logic.
Migrating from a single user table
If you already have a project user table that mixes auth and business fields, the migration plan is:
- Add the
nest_auth_userstable (and the rest ofNestAuthEntities). - For each existing user, insert a row into
nest_auth_userscarrying justemail,phone,passwordHash, etc. Capture the newid. - Add an
authUserIdcolumn on your existing user table; backfill it with the new IDs. - Drop the auth columns from your user table — they now live in
nest_auth_users. - Rename the table to
AppUser(or whatever you call it) for clarity.
The migration recipe walks through it with SQL.
Atomic creation across NestAuthUser and AppUser
The event-listener pattern above runs after createUser commits, so a
listener failure leaves you with a NestAuthUser row but no AppUser
row. For portal-style "create user with roles and a tenant binding"
flows where partial state is broken, use a transaction: every
auth-side method on UserService and the NestAuthUser /
NestAuthUserAccess / NestAuthPlatformAccess entities accepts an
optional EntityManager so the entire flow rolls back as one unit.
See the Transactional user creation recipe for the canonical end-to-end pattern.
Related
- Events & Hooks — the full extension surface.
- Hooks Reference —
user.beforeCreate,user.afterCreate,user.getSessionUserData. - user-registered listener recipe.
- Transactional user creation recipe.
- Migrate existing users recipe.