Skip to main content

Models Reference

All GoAuth data models live in the pkg/models package. Each model maps to a database table (via GORM tags) and has an associated repository interface for data access.

All entity IDs are UUIDs (v7). GoAuth does not use soft deletes.


Sentinel Error​

var ErrNotFound = errors.New("record not found")

Repository methods return models.ErrNotFound when the requested record does not exist. Check for it with:

if errors.Is(err, models.ErrNotFound) {
// record does not exist
}

This is a standard error, not a *types.GoAuthError. It is used at the storage layer, while *types.GoAuthError is used at the service/handler layer.


Core Models​

User​

Table: users

type User struct {
ID string `json:"id" gorm:"primaryKey"`
Name string `json:"name"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email" gorm:"uniqueIndex;not null"`
Username string `json:"username" gorm:"uniqueIndex"`
PasswordHash string `json:"-" gorm:"column:password;not null"`
Avatar string `json:"avatar"`
PhoneNumber string `json:"phone_number"`
Active bool `json:"active" gorm:"default:true"`
EmailVerified bool `json:"email_verified" gorm:"default:false"`
PhoneNumberVerified bool `json:"phone_number_verified" gorm:"default:false"`
IsSuperAdmin bool `json:"is_super_admin" gorm:"default:false;not null;index"`
TokenVersion int `json:"-" gorm:"default:0;not null"`
FailedLoginAttempts int `json:"-" gorm:"default:0;not null"`
LockedUntil *time.Time `json:"-" gorm:"index"`
CreatedAt time.Time `json:"created_at"`
LastLoginAt *time.Time `json:"last_login_at"`
UpdatedAt *time.Time `json:"updated_at"`
}

Hidden fields (never serialized to JSON):

FieldPurpose
PasswordHashBcrypt-hashed password. Stored in the password DB column.
TokenVersionIncremented to invalidate all existing stateless tokens for this user.
FailedLoginAttemptsCounter for account lockout after repeated failures.
LockedUntilTimestamp until which the account is locked. Nil means not locked.

UserRepository​

type UserRepository interface {
Create(ctx context.Context, user *User) error
FindByEmail(ctx context.Context, email string) (*User, error)
FindByUsername(ctx context.Context, username string) (*User, error)
FindByPhoneNumber(ctx context.Context, phoneNumber string) (*User, error)
FindByEmailOrUsername(ctx context.Context, emailOrUsername string) (*User, error)
List(ctx context.Context, opts UserListOpts) ([]*User, int64, error)
FindByID(ctx context.Context, id string) (*User, error)
Update(ctx context.Context, user *User) error
Delete(ctx context.Context, id string) error
IsAvailable(ctx context.Context, field, value string) (bool, error)
}

Token​

Table: tokens

Used for all verification and state tokens: email verification, phone verification, password reset, two-factor codes, magic links, and OAuth state parameters.

type Token struct {
ID string `json:"id" gorm:"primaryKey"`
UserID string `json:"user_id" gorm:"not null;index"`
Type string `json:"type" gorm:"not null;index"`
Token string `json:"token" gorm:"uniqueIndex;not null"`
Code string `json:"code,omitempty" gorm:"index"`
Email string `json:"email,omitempty" gorm:"index"`
PhoneNumber string `json:"phone_number,omitempty" gorm:"index"`
ExpiresAt time.Time `json:"expires_at" gorm:"not null;index"`
Used bool `json:"used" gorm:"default:false"`
UsedAt *time.Time `json:"used_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
}

Token type constants:

ConstantValueUsed By
TokenTypeEmailVerification"email_verification"Core module -- email verification flow
TokenTypePhoneVerification"phone_verification"Core module -- phone verification flow
TokenTypePasswordReset"password_reset"Core module -- password reset flow
TokenTypeTwoFactorCode"two_factor_code"Two-factor module -- code-based 2FA
TokenTypeMagicLink"magic_link"Magic link module -- passwordless auth
TokenTypeOAuthState"oauth_state"OAuth module -- CSRF state parameter

TokenRepository​

type TokenRepository interface {
Create(ctx context.Context, token *Token) error
FindByToken(ctx context.Context, token string) (*Token, error)
FindByUserID(ctx context.Context, userID string) ([]*Token, error)
FindByCode(ctx context.Context, code, tokenType string) (*Token, error)
FindByEmailAndType(ctx context.Context, email, tokenType string) (*Token, error)
FindByPhoneAndType(ctx context.Context, phone, tokenType string) (*Token, error)
MarkAsUsed(ctx context.Context, id string) error
Delete(ctx context.Context, token string) error
DeleteByIDAndType(ctx context.Context, id string, tokenType string) error
DeleteByUserID(ctx context.Context, userID string) error
DeleteExpired(ctx context.Context) (int64, error)
}

Session Module Models​

Session​

Table: sessions

Represents an active user session. Only created when the session module is registered (mutually exclusive with the stateless module).

type Session struct {
ID string `json:"id" gorm:"primaryKey"`
UserID string `json:"user_id" gorm:"not null;index"`
RefreshToken string `json:"refresh_token" gorm:"uniqueIndex;not null"`
RefreshTokenExpiresAt time.Time `json:"refresh_token_expires_at" gorm:"not null;index"`
ExpiresAt time.Time `json:"expires_at" gorm:"not null;index"`
UserAgent string `json:"user_agent"`
IPAddress string `json:"ip_address"`
ReplacedBy string `json:"replaced_by"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

The ReplacedBy field tracks refresh token rotation: when a session is refreshed, the old session record stores the ID of the new session that replaced it.

SessionRepository​

type SessionRepository interface {
Create(ctx context.Context, session *Session) error
FindByID(ctx context.Context, id string) (*Session, error)
FindByToken(ctx context.Context, token string) (*Session, error)
FindByUserID(ctx context.Context, userID string, opts SessionListOpts) ([]*Session, int64, error)
Update(ctx context.Context, session *Session) error
Delete(ctx context.Context, id string) error
DeleteByToken(ctx context.Context, token string) error
DeleteByUserID(ctx context.Context, userID string) error
DeleteExpired(ctx context.Context) (int64, error)
}

Stateless Module Models​

Blacklist​

Table: (not yet defined)

Placeholder model for the stateless module's token blacklist. Currently an empty struct and interface, reserved for future blacklist-based token revocation.

type Blacklist struct{}

type BlacklistRepository interface{}

OAuth Module Models​

Account​

Table: accounts

Represents an OAuth/OIDC provider link for a user. A single user can have multiple accounts (e.g. Google + GitHub). This model is only created when the OAuth module is used.

type Account struct {
ID string `json:"id" gorm:"primaryKey"`
UserID string `json:"user_id" gorm:"not null;index"`
Provider string `json:"provider" gorm:"not null;index"`
ProviderAccountID string `json:"provider_account_id" gorm:"not null"`
Type string `json:"type" gorm:"not null"`
AccessToken string `json:"-" gorm:"type:text"`
RefreshToken string `json:"-" gorm:"type:text"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
TokenType string `json:"token_type,omitempty"`
Scope string `json:"scope,omitempty"`
IDToken string `json:"-" gorm:"type:text"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

Account type constants:

ConstantValueDescription
AccountTypeOAuth"oauth"Standard OAuth 2.0 provider
AccountTypeOIDC"oidc"OpenID Connect provider
AccountTypeCredentials"credentials"Password-based authentication

Hidden fields (never serialized to JSON):

FieldPurpose
AccessTokenProvider access token, encrypted at rest (AES-256-GCM).
RefreshTokenProvider refresh token, encrypted at rest.
IDTokenOIDC ID token, stored for reference.

AccountRepository​

type AccountRepository interface {
Create(ctx context.Context, account *Account) error
FindByID(ctx context.Context, id string) (*Account, error)
FindByProviderAndAccountID(ctx context.Context, provider, providerAccountID string) (*Account, error)
FindByUserID(ctx context.Context, userID string) ([]*Account, error)
FindByUserIDAndProvider(ctx context.Context, userID, provider string) (*Account, error)
Update(ctx context.Context, account *Account) error
Delete(ctx context.Context, id string) error
DeleteByUserIDAndProvider(ctx context.Context, userID, provider string) error
CountByUserID(ctx context.Context, userID string) (int64, error)
}

Two-Factor Module Models​

TwoFactor​

Table: two_factors

Stores the 2FA configuration for a user. Each user has at most one TwoFactor record.

type TwoFactor struct {
ID string `json:"id" gorm:"primaryKey"`
UserID string `json:"user_id" gorm:"uniqueIndex;not null"`
Secret string `json:"-" gorm:"not null"`
Enabled bool `json:"enabled" gorm:"default:false"`
Verified bool `json:"verified" gorm:"default:false"`
Method string `json:"method" gorm:"default:'totp'"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

Hidden fields:

FieldPurpose
SecretTOTP secret key, encrypted in storage (AES-256-GCM).

The Method field indicates the 2FA method: "totp", "sms", or "email".

The Verified field is false until the user successfully confirms their first TOTP code, proving they have set up their authenticator app correctly.

TwoFactorRepository​

type TwoFactorRepository interface {
Create(ctx context.Context, tf *TwoFactor) error
GetByUserID(ctx context.Context, userID string) (*TwoFactor, error)
Update(ctx context.Context, tf *TwoFactor) error
Delete(ctx context.Context, userID string) error
}

BackupCode​

Table: backup_codes

Recovery codes for 2FA. Users receive a set of backup codes when enabling 2FA; each code can be used exactly once.

type BackupCode struct {
ID string `json:"id" gorm:"primaryKey"`
UserID string `json:"user_id" gorm:"index;not null"`
Code string `json:"-" gorm:"not null"`
Used bool `json:"used" gorm:"default:false"`
UsedAt *time.Time `json:"used_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
}

Hidden fields:

FieldPurpose
CodeThe backup code, hashed with bcrypt. Never exposed in API responses.

BackupCodeRepository​

type BackupCodeRepository interface {
CreateBatch(ctx context.Context, codes []*BackupCode) error
GetByUserID(ctx context.Context, userID string) ([]*BackupCode, error)
GetUnusedByUserID(ctx context.Context, userID string) ([]*BackupCode, error)
MarkUsed(ctx context.Context, id string) error
DeleteByUserID(ctx context.Context, userID string) error
}

Audit Module Models​

AuditLog​

Table: audit_logs

Cross-cutting concern used by all modules (core, admin, audit, organization) to track system actions.

type AuditLog struct {
ID string `json:"id" gorm:"primaryKey"`
Action string `json:"action" gorm:"not null;index"`
ActorID string `json:"actor_id" gorm:"not null;index"`
ActorType string `json:"actor_type" gorm:"default:'user'"`
TargetID *string `json:"target_id,omitempty" gorm:"index"`
TargetType *string `json:"target_type,omitempty"`
Details string `json:"details" gorm:"type:text"`
Metadata string `json:"metadata,omitempty" gorm:"type:jsonb"`
Severity string `json:"severity" gorm:"default:'info';index"`
IPAddress string `json:"ip_address"`
UserAgent string `json:"user_agent"`
CreatedAt time.Time `json:"created_at" gorm:"index"`
}

Field details:

FieldDescriptionExample values
ActionDot-notation action identifier"auth.login", "admin.user.delete"
ActorIDUUID of the user who performed the action
ActorTypeType of actor"user", "admin", "system"
TargetIDUUID of the affected resource (optional)
TargetTypeType of the affected resource"user", "organization", "resource"
DetailsHuman-readable description
MetadataJSON blob with contextual data{"ip": "...", "user_agent": "...", "device": "..."}
SeverityLog severity level"info", "warning", "critical"

AuditLogRepository​

type AuditLogRepository interface {
Create(ctx context.Context, log *AuditLog) error
FindByActorID(ctx context.Context, actorID string, opts AuditLogListOpts) ([]*AuditLog, int64, error)
FindByTargetID(ctx context.Context, targetID string, opts AuditLogListOpts) ([]*AuditLog, int64, error)
FindByAction(ctx context.Context, action string, opts AuditLogListOpts) ([]*AuditLog, int64, error)
FindBySeverity(ctx context.Context, severity string, opts AuditLogListOpts) ([]*AuditLog, int64, error)
FindByOrganizationID(ctx context.Context, orgID string, opts AuditLogListOpts) ([]*AuditLog, int64, error)
List(ctx context.Context, opts AuditLogListOpts) ([]*AuditLog, int64, error)
DeleteOlderThan(ctx context.Context, before time.Time) error
DeleteByActionOlderThan(ctx context.Context, action string, before time.Time) error
}

Organization Module Models​

Organization​

Table: organizations (inferred from convention; no explicit TableName() override)

type Organization struct {
ID string `json:"id" gorm:"primaryKey;type:varchar(36)"`
Name string `json:"name" gorm:"type:varchar(255);not null"`
Slug string `json:"slug" gorm:"type:varchar(255);uniqueIndex;not null"`
OwnerID string `json:"owner_id" gorm:"type:varchar(36);not null"`
LogoURL string `json:"logo_url,omitempty" gorm:"type:varchar(512)"`
Metadata string `json:"metadata,omitempty" gorm:"type:text"`
Active bool `json:"active" gorm:"default:true"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
}

OrganizationRepository​

type OrganizationRepository interface {
Create(ctx context.Context, org *Organization) error
FindByID(ctx context.Context, id string) (*Organization, error)
FindBySlug(ctx context.Context, slug string) (*Organization, error)
FindByOwnerID(ctx context.Context, ownerID string) ([]*Organization, error)
List(ctx context.Context, opts OrganizationListOpts) ([]*Organization, int64, error)
Update(ctx context.Context, org *Organization) error
Delete(ctx context.Context, id string) error
IsSlugAvailable(ctx context.Context, slug string) (bool, error)
}

OrganizationMember​

Table: organization_members (inferred from convention)

Represents a user's membership in an organization. The composite unique index on (org_id, user_id) prevents duplicate memberships.

type OrganizationMember struct {
ID string `json:"id" gorm:"primaryKey;type:varchar(36)"`
OrgID string `json:"org_id" gorm:"type:varchar(36);not null;uniqueIndex:idx_org_member"`
UserID string `json:"user_id" gorm:"type:varchar(36);not null;uniqueIndex:idx_org_member"`
Role string `json:"role" gorm:"type:varchar(50);not null;default:'member'"`
JoinedAt time.Time `json:"joined_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
}

OrganizationMemberRepository​

type OrganizationMemberRepository interface {
Create(ctx context.Context, member *OrganizationMember) error
FindByOrgAndUser(ctx context.Context, orgID, userID string) (*OrganizationMember, error)
ListByOrg(ctx context.Context, orgID string, opts MemberListOpts) ([]*OrganizationMember, int64, error)
ListByUser(ctx context.Context, userID string) ([]*OrganizationMember, error)
Update(ctx context.Context, member *OrganizationMember) error
Delete(ctx context.Context, id string) error
DeleteByOrgAndUser(ctx context.Context, orgID, userID string) error
CountByOrg(ctx context.Context, orgID string) (int64, error)
}

OrgInvitation​

Table: org_invitations

Represents a pending invitation to join an organization.

type OrgInvitation struct {
ID string `json:"id" gorm:"primaryKey;type:varchar(36)"`
OrgID string `json:"org_id" gorm:"type:varchar(36);not null"`
Email string `json:"email" gorm:"type:varchar(255);not null"`
Role string `json:"role" gorm:"type:varchar(50);not null;default:'member'"`
InviterID string `json:"inviter_id" gorm:"type:varchar(36);not null"`
Token string `json:"-" gorm:"type:varchar(255);uniqueIndex;not null"`
Status string `json:"status" gorm:"type:varchar(20);not null;default:'pending'"`
ExpiresAt time.Time `json:"expires_at"`
CreatedAt time.Time `json:"created_at"`
AcceptedAt *time.Time `json:"accepted_at,omitempty"`
}

OrgInvitationRepository​

type OrgInvitationRepository interface {
Create(ctx context.Context, invitation *OrgInvitation) error
FindByID(ctx context.Context, id string) (*OrgInvitation, error)
FindByToken(ctx context.Context, token string) (*OrgInvitation, error)
FindByOrgAndEmail(ctx context.Context, orgID, email string) (*OrgInvitation, error)
ListByOrg(ctx context.Context, orgID string, opts OrgInvitationListOpts) ([]*OrgInvitation, int64, error)
ListPendingByEmail(ctx context.Context, email string) ([]*OrgInvitation, error)
Update(ctx context.Context, invitation *OrgInvitation) error
Delete(ctx context.Context, id string) error
DeleteExpired(ctx context.Context) error
}

Invitation (standalone)​

Table: invitations

Represents a standalone platform invitation (not org-scoped).

type Invitation struct {
ID string `json:"id" gorm:"primaryKey;type:varchar(36)"`
Email string `json:"email" gorm:"type:varchar(255);not null"`
Purpose string `json:"purpose" gorm:"type:varchar(100);not null;default:'platform'"`
InviterID string `json:"inviter_id" gorm:"type:varchar(36);not null"`
Token string `json:"-" gorm:"type:varchar(255);uniqueIndex;not null"`
Status string `json:"status" gorm:"type:varchar(20);not null;default:'pending'"`
Metadata string `json:"metadata,omitempty" gorm:"type:text"`
ExpiresAt time.Time `json:"expires_at"`
CreatedAt time.Time `json:"created_at"`
AcceptedAt *time.Time `json:"accepted_at,omitempty"`
}

Invitation status constants (shared between standalone and org invitations):

ConstantValue
InvitationStatusPending"pending"
InvitationStatusAccepted"accepted"
InvitationStatusDeclined"declined"
InvitationStatusExpired"expired"

InvitationRepository​

type InvitationRepository interface {
Create(ctx context.Context, invitation *Invitation) error
FindByID(ctx context.Context, id string) (*Invitation, error)
FindByToken(ctx context.Context, token string) (*Invitation, error)
FindPendingByEmail(ctx context.Context, email, purpose string) (*Invitation, error)
ListByInviter(ctx context.Context, inviterID string, opts InvitationListOpts) ([]*Invitation, int64, error)
ListPendingByEmail(ctx context.Context, email string) ([]*Invitation, error)
Update(ctx context.Context, invitation *Invitation) error
Delete(ctx context.Context, id string) error
DeleteExpired(ctx context.Context) error
}

Listing Options​

All list/search repository methods accept a per-entity options struct that embeds the base ListingOpts. These control pagination and sorting.

ListingOpts (Base)​

type ListingOpts struct {
Offset int
Limit int
SortField string
SortDir string // "asc" or "desc"
}

Defaults (from DefaultListingOpts()): Offset=0, Limit=20, SortField="created_at", SortDir="desc".

The Normalize(maxLimit, allowedSortFields) method clamps values to safe ranges: limit to [1, maxLimit], offset to [0, +inf), sort direction to "asc" or "desc", and sort field to a member of the allowlist (falling back to "created_at").

Per-Entity Options​

TypeEmbedsExtra FieldsAllowed Sort Fields
UserListOptsListingOptsQuery string -- search name/email/usernamecreated_at, email, username, name
SessionListOptsListingOpts(none)created_at, expires_at, ip_address
AuditLogListOptsListingOpts(none)created_at, action, severity, actor_id
OrganizationListOptsListingOptsOwnerID string, Query stringcreated_at, name
MemberListOptsListingOptsRole stringjoined_at, role
InvitationListOptsListingOptsStatus stringcreated_at, expires_at, status

Each per-entity type has its own Normalize(maxLimit int) method that delegates to ListingOpts.Normalize with the correct sort field allowlist for that entity.