Skip to main content

Users Schema Deep Dive

Overview

The users module is the core identity layer of the Ever Works Template. It encompasses the base users table (NextAuth-compatible), client_profiles for extended profile data, authentication-related tables (accounts, sessions, authenticators, verificationTokens, passwordResetTokens), and the RBAC system (roles, permissions, rolePermissions, userRoles).

Source file: template/lib/db/schema.ts Relations file: template/lib/db/migrations/relations.ts


Table: users

The core authentication table, designed for compatibility with NextAuth (Auth.js).

Columns

ColumnDB NameTypeNullableDefaultConstraints
ididtextNocrypto.randomUUID()Primary Key
emailemailtextYes-Unique
imageimagetextYes-Profile avatar URL
emailVerifiedemailVerifiedtimestampYes-Date email was verified
passwordHashpassword_hashtextYes-Hashed password
createdAtcreated_attimestampNonow()-
updatedAtupdated_attimestampNonow()-
deletedAtdeleted_attimestampYes-Soft delete

Indexes

NameColumnsType
users_email_uniqueemailUnique
users_created_at_idxcreatedAtB-tree

TypeScript Types

export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;

Table: client_profiles

Extended profile for application users (clients). Stores personal details, account settings, moderation state, and location preferences.

Columns

ColumnDB NameTypeNullableDefaultConstraints
ididtextNocrypto.randomUUID()Primary Key
userIduserIdtextNo-FK -> users.id (CASCADE)
emailemailtextNo--
namenametextNo--
displayNamedisplay_nametextYes--
usernameusernametextYes-Unique
biobiotextYes--
jobTitlejob_titletextYes--
companycompanytextYes--
industryindustrytextYes--
phonephonetextYes--
websitewebsitetextYes--
locationlocationtextYes--
avataravatartextYes--
accountTypeaccount_typetext (enum)Yes'individual'individual, business, enterprise
statusstatustext (enum)Yes'active'active, inactive, suspended, banned, trial
planplantext (enum)Yes'free'free, standard, premium
timezonetimezonetextYes'UTC'-
languagelanguagetextYes'en'-
countrycountrytextYes--
currencycurrencytextYes'USD'-
defaultLatitudedefault_latitudedoublePrecisionYes-"Near Me" fallback
defaultLongitudedefault_longitudedoublePrecisionYes-"Near Me" fallback
defaultCitydefault_citytextYes--
defaultCountrydefault_countrytextYes--
locationPrivacylocation_privacytextYes'private'-
twoFactorEnabledtwo_factor_enabledbooleanYesfalse-
emailVerifiedemail_verifiedbooleanYesfalse-
totalSubmissionstotal_submissionsintegerYes0-
notesnotestextYes-Admin notes
tagstagstextYes--
warningCountwarning_countintegerYes0Moderation
suspendedAtsuspended_attimestamp (tz)Yes-Moderation
bannedAtbanned_attimestamp (tz)Yes-Moderation
createdAtcreated_attimestampNonow()-
updatedAtupdated_attimestampNonow()-

Indexes

NameColumnsType
client_profile_user_id_unique_idxuserIdUnique
client_profile_email_idxemailB-tree
client_profile_status_idxstatusB-tree
client_profile_plan_idxplanB-tree
client_profile_account_type_idxaccountTypeB-tree
client_profile_username_idxusernameB-tree
client_profile_created_at_idxcreatedAtB-tree
client_profiles_username_uniqueusernameUnique

TypeScript Types

export type ClientProfile = typeof clientProfiles.$inferSelect;
export type NewClientProfile = typeof clientProfiles.$inferInsert;
export type ClientProfileWithUser = ClientProfile & {
user: typeof users.$inferSelect;
};

Table: accounts

OAuth provider accounts linked to users. Follows the NextAuth adapter pattern.

Columns

ColumnDB NameTypeNullableDefaultConstraints
userIduserIdtextNo-FK -> users.id (CASCADE)
typetypetextNo-AdapterAccountType
providerprovidertextNo-Compound PK part
providerAccountIdproviderAccountIdtextNo-Compound PK part
emailemailtextYes-Client auth
passwordHashpassword_hashtextYes-Client auth
refresh_tokenrefresh_tokentextYes-OAuth
access_tokenaccess_tokentextYes-OAuth
expires_atexpires_atintegerYes-OAuth
token_typetoken_typetextYes-OAuth
scopescopetextYes-OAuth
id_tokenid_tokentextYes-OAuth
session_statesession_statetextYes-OAuth

Primary Key

Composite: (provider, providerAccountId)

Indexes

NameColumnsType
accounts_email_idxemailB-tree
accounts_provider_idxproviderB-tree

Table: sessions

Active user sessions for NextAuth.

Columns

ColumnDB NameTypeNullableDefaultConstraints
sessionTokensessionTokentextNo-Primary Key
userIduserIdtextNo-FK -> users.id (CASCADE)
expiresexpirestimestampNo--

Table: authenticators

WebAuthn/FIDO2 authenticator credentials.

Columns

ColumnDB NameTypeNullableDefaultConstraints
credentialIDcredentialIDtextNo-Unique
userIduserIdtextNo-FK -> users.id (CASCADE)
providerAccountIdproviderAccountIdtextNo--
credentialPublicKeycredentialPublicKeytextNo--
countercounterintegerNo--
credentialDeviceTypecredentialDeviceTypetextNo--
credentialBackedUpcredentialBackedUpbooleanNo--
transportstransportstextYes--

Primary Key

Composite: (userId, credentialID)


RBAC Tables

Table: roles

ColumnDB NameTypeNullableDefaultConstraints
ididtextNo-Primary Key
namenametextNo-Unique
descriptiondescriptiontextYes--
isAdminis_adminbooleanNofalse-
statusstatustext (enum)Yes'active'active, inactive
created_bycreated_bytextYes'system'-
createdAtcreated_attimestampNonow()-
updatedAtupdated_attimestampNonow()-
deletedAtdeleted_attimestampYes-Soft delete

Indexes: roles_status_idx, roles_is_admin_idx, roles_created_at_idx

Table: permissions

ColumnDB NameTypeNullableDefaultConstraints
ididtextNocrypto.randomUUID()Primary Key
keykeytextNo-Unique
descriptiondescriptiontextYes--
createdAtcreated_attimestampNonow()-
updatedAtupdated_attimestampNonow()-

Table: role_permissions (junction)

ColumnDB NameTypeNullableDefaultConstraints
roleIdrole_idtextNo-FK -> roles.id (CASCADE)
permissionIdpermission_idtextNo-FK -> permissions.id (CASCADE)
createdAtcreated_attimestampNonow()-

Primary Key: Composite (roleId, permissionId) Indexes: role_permissions_role_idx, role_permissions_permission_idx, role_permissions_created_at_idx

Table: user_roles (junction)

ColumnDB NameTypeNullableDefaultConstraints
userIduser_idtextNo-FK -> users.id (CASCADE)
roleIdrole_idtextNo-FK -> roles.id (CASCADE)
createdAtcreated_attimestampNonow()-

Primary Key: Composite (userId, roleId) Indexes: user_roles_user_idx, user_roles_role_idx, user_roles_created_at_idx


Relations Diagram


Token Tables

Table: verificationTokens

ColumnTypeConstraints
identifiertextComposite PK part
emailtextNOT NULL
tokentextComposite PK part
expirestimestampNOT NULL

Primary Key: Composite (identifier, token)

Table: passwordResetTokens

ColumnTypeConstraints
idtextPrimary Key (crypto.randomUUID())
emailtextNOT NULL
tokentextNOT NULL, Unique
expirestimestampNOT NULL

Activity Tracking

Table: activityLogs

ColumnDB NameTypeNullableDefaultConstraints
ididserialNoauto-incrementPrimary Key
userIduserIdtextYes-FK -> users.id (CASCADE)
clientIdclientIdtextYes-FK -> client_profiles.id (CASCADE)
actionactiontextNo--
timestamptimestamptimestampNonow()-
ipAddressip_addressvarchar(45)Yes-IPv4/IPv6

Indexes: activity_logs_user_idx, activity_logs_timestamp_idx, activity_logs_action_idx

Activity Type Enum

export enum ActivityType {
SIGN_UP = 'SIGN_UP',
SIGN_IN = 'SIGN_IN',
SIGN_OUT = 'SIGN_OUT',
VERIFY_EMAIL = 'VERIFY_EMAIL',
UPDATE_PASSWORD = 'UPDATE_PASSWORD',
DELETE_ACCOUNT = 'DELETE_ACCOUNT',
UPDATE_ACCOUNT = 'UPDATE_ACCOUNT',
UPDATE_TWENTY_CRM_CONFIG = 'UPDATE_TWENTY_CRM_CONFIG'
}

Query Examples

Get user with profile

import { db } from '@/lib/db/drizzle';
import { users, clientProfiles } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';

const result = await db
.select()
.from(users)
.leftJoin(clientProfiles, eq(users.id, clientProfiles.userId))
.where(eq(users.id, userId));

Check user roles

import { userRoles, roles } from '@/lib/db/schema';

const userRoleList = await db
.select({ roleName: roles.name, isAdmin: roles.isAdmin })
.from(userRoles)
.innerJoin(roles, eq(userRoles.roleId, roles.id))
.where(eq(userRoles.userId, userId));

Log an activity

import { activityLogs, ActivityType } from '@/lib/db/schema';

await db.insert(activityLogs).values({
userId,
action: ActivityType.SIGN_IN,
ipAddress: request.headers.get('x-forwarded-for'),
});