User Model Extension Guide
Flask-More-Smorest provides flexible options for extending the User model to meet your application’s needs. This guide helps you choose the right approach and implement it correctly.
Tip
Quick Decision
Just adding fields? → Direct inheritance (Pattern 1)
Distinct user types? → Separate tables (Pattern 2)
Existing auth system? → Custom user context (Pattern 3)
External auth (OAuth, SAML)? → Custom user context (Pattern 3)
Pattern 1: Direct Inheritance (90% of Use Cases)
Use direct inheritance when you want to add fields to the built-in User model while keeping all authentication and permission functionality.
When to Use: - Adding profile fields (bio, phone, etc.) - Adding custom properties or methods - Most common use cases
Single Table Inheritance (default):
from flask_more_smorest.perms.models.defaults import User
from sqlalchemy.orm import Mapped, mapped_column
class CustomUser(User):
# Uses "user" table (single-table inheritance)
bio: Mapped[str | None] = mapped_column(db.String(500), nullable=True)
phone: Mapped[str | None] = mapped_column(db.String(20), nullable=True)
age: Mapped[int | None] = mapped_column(db.Integer, nullable=True)
@property
def is_adult(self) -> bool:
return self.age is not None and self.age >= 18
All User instances automatically include:
- Email authentication with password hashing
- Role management via User.roles relationship
- User settings via User.settings relationship
- JWT tokens via User.tokens relationship
- Permission methods (is_admin, is_superadmin, has_role)
Overriding Permission Methods:
class VerifiedUser(User):
is_verified: Mapped[bool] = mapped_column(db.Boolean, default=False)
def _can_write(self) -> bool:
"""Only verified users can edit their own profile."""
if self.is_current_user_admin():
return True
if self.id == get_current_user_id():
return self.is_verified
return super()._can_write()
Using Mixins for Common Features:
from flask_more_smorest.perms import ProfileMixin, TimestampMixin, SoftDeleteMixin
class FullUser(User, ProfileMixin, TimestampMixin, SoftDeleteMixin):
# Adds: first_name, last_name, display_name, avatar_url
# Adds: last_login_at, email_verified_at
# Adds: deleted_at, is_deleted, soft_delete(), restore()
pass
Pattern 2: Separate Tables (Distinct User Types)
Use separate tables when you have distinct types of users that need their own schema.
When to Use: - Different user types (employees, contractors, customers) - Avoiding table conflicts - Cleaner schema separation
Joined Table Inheritance:
class ContractorUser(User):
__tablename__ = "contractor_users"
id: Mapped[uuid.UUID] = mapped_column(
sa.Uuid(as_uuid=True),
db.ForeignKey("user.id"),
primary_key=True
)
contractor_id: Mapped[str] = mapped_column(db.String(50), unique=True)
hourly_rate: Mapped[float] = mapped_column(db.Float)
# Inherits all User functionality via FK relationship
No Inheritance (Completely Separate):
class CustomerProfile(BaseModel):
"""Separate profile table linked to User."""
__tablename__ = "customer_profiles"
user_id: Mapped[uuid.UUID] = mapped_column(
sa.Uuid(as_uuid=True),
db.ForeignKey("user.id"),
nullable=False
)
user: Mapped[User] = relationship("User")
preferences: Mapped[dict] = mapped_column(db.JSON, default={})
Pattern 3: Custom User Context (External Auth)
Use custom user context when you have an existing authentication system or use external providers.
When to Use: - Existing User/UserRole models in your app - OAuth, SAML, LDAP integration - Multi-tenant with distinct user models per tenant - Want complete control over User model
Registering Custom User Context:
Register your custom user class (and optionally a custom getter):
from flask_login import current_user
from flask_more_smorest.perms import init_fms
def get_flask_login_user():
return current_user if not current_user.is_anonymous else None
# Register - everything else derives from this!
init_fms(user=MyUser, get_current_user=get_flask_login_user)
Note
Auto-Loading of Defaults
When you call init_fms(user=MyUser), the system automatically
imports and registers the default models for UserRole, Token, Domain, and
UserSetting. No explicit import of default models is needed.
The system automatically provides:
get_current_user()- Calls your registered functionget_current_user_id()- Extractsidattribute from your useris_current_user_admin()- Checkshas_role('admin')is_current_user_superadmin()- Checkshas_role('superadmin')list_roles()- Returns user’s roles as strings (if implemented)
Type Safety:
Your custom User model should inherit from AbstractUser for type compatibility:
from flask_more_smorest.perms import AbstractUser, ROLE_ADMIN, ROLE_SUPERADMIN
import uuid
class MyUser(AbstractUser):
# Your specific implementation
pass
# Type checking works
user: MyUser = get_current_user()
if isinstance(user, AbstractUser):
print(f"User {user.email} is compliant")
Integration Examples:
Flask-Login:
from flask_login import current_user
from flask_more_smorest.perms import init_fms
def get_flask_login_user():
return current_user if not current_user.is_anonymous else None
init_fms(user=MyUser, get_current_user=get_flask_login_user)
Custom OAuth (Google, GitHub, etc.):
from authlib.integrations.flask_client import OAuth
from flask_more_smorest.perms import init_fms
oauth = OAuth()
def get_oauth_user():
from flask import session
user_id = session.get('user_id')
return MyUser.query.get(user_id) if user_id else None
init_fms(user=MyUser, get_current_user=get_oauth_user)
# Your User model needs has_role method:
class MyUser:
def has_role(self, role: str) -> bool:
return role in ['admin', 'superadmin']
def list_roles(self) -> list[str]:
return ['admin'] if self.is_admin else []
Multi-Tenant with Distinct User Models:
from flask import g
from flask_more_smorest.perms import init_fms
def get_tenant_user():
if not hasattr(g, 'tenant_id'):
return None
return get_tenant_user_model(g.tenant_id).get_current_user()
init_fms(user=get_tenant_user_model(g.tenant_id), get_current_user=get_tenant_user)
# Your User model needs has_role method:
class MyUser:
def has_role(self, role: str) -> bool:
if role == 'admin':
return self.is_tenant_admin()
if role == 'superadmin':
return self.is_platform_admin()
return False
def list_roles(self) -> list[str]:
roles = []
if self.is_tenant_admin():
roles.append('admin')
if self.is_platform_admin():
roles.append('superadmin')
return roles
Available Mixins
Flask-More-Smorest provides several mixins for common functionality.
ProfileMixin - User profile information:
from flask_more_smorest.perms import ProfileMixin
class ProfileUser(User, ProfileMixin):
# Adds: first_name, last_name, display_name, avatar_url
# Plus: full_name property
pass
TimestampMixin - Additional timestamps:
from flask_more_smorest.perms import TimestampMixin
class TimestampUser(User, TimestampMixin):
# Adds: last_login_at, email_verified_at
pass
SoftDeleteMixin - Soft delete support:
from flask_more_smorest.perms import SoftDeleteMixin
class SoftDeleteUser(User, SoftDeleteMixin):
# Adds: deleted_at, is_deleted, soft_delete(), restore()
pass
UserOwnershipMixin - User-owned resources:
from flask_more_smorest.perms import UserOwnershipMixin
class Note(UserOwnershipMixin, BasePermsModel):
# Adds: user_id, user relationship
# Automatic permission checks (owner can read/write)
content: Mapped[str] = mapped_column(db.Text)
HasUserMixin - Simple user reference:
from flask_more_smorest.perms import HasUserMixin
class Comment(HasUserMixin, BasePermsModel):
# Adds: user_id, user relationship
# Configurable via __user_field_name__, etc.
text: Mapped[str] = mapped_column(db.Text)
Common Use Cases
Multi-Tenant SaaS Application
class TenantUser(User):
tenant_id: Mapped[uuid.UUID] = mapped_column(db.ForeignKey('tenant.id'))
feature_flags: Mapped[dict] = mapped_column(db.JSON, default={})
def has_feature(self, feature: str) -> bool:
return self.feature_flags.get(feature, False)
def _can_read(self) -> bool:
"""Users can only read within their tenant."""
current = get_current_user()
return bool(current and (
current.tenant_id == self.tenant_id or current.is_admin
))
E-Commerce Platform
class CustomerUser(User, ProfileMixin, TimestampMixin):
customer_type: Mapped[str] = mapped_column(db.String(20), default="retail")
loyalty_points: Mapped[int] = mapped_column(db.Integer, default=0)
preferred_currency: Mapped[str] = mapped_column(db.String(3), default="USD")
@property
def loyalty_tier(self) -> str:
if self.loyalty_points >= 10000:
return "platinum"
elif self.loyalty_points >= 5000:
return "gold"
return "bronze"
Enterprise Application
class EmployeeUser(User, ProfileMixin, TimestampMixin):
employee_id: Mapped[str] = mapped_column(db.String(20), unique=True)
hire_date: Mapped[date] = mapped_column(db.Date)
position: Mapped[str] = mapped_column(db.String(100))
manager_id: Mapped[uuid.UUID | None] = mapped_column(db.ForeignKey('user.id'))
manager: Mapped["EmployeeUser | None"] = relationship(
"EmployeeUser", remote_side="EmployeeUser.id"
)
@property
def years_of_service(self) -> int:
return (date.today() - self.hire_date).days // 365
def _can_read(self) -> bool:
"""Employees can read own data and direct reports."""
current = get_current_user()
if not current:
return False
if current.id == self.id or current.is_admin:
return True
return self.manager_id == current.id
Troubleshooting
“Table ‘user’ is already defined” Error:
Cause: Multiple classes using single-table inheritance without proper config
Solution: Use
__tablename__for separate tables or ensure__table_args__ = {"extend_existing": True}is set
Custom role enums not working with permissions:
Cause: Hardcoded role checking in
UserRole._can_write()Solution: Now uses string matching for custom role names; ensure custom roles include “admin” or “superadmin” when appropriate.
“Working outside of request context” Error:
Cause: Calling
get_current_user()outside Flask request contextSolution: Use
with app.test_request_context():for testing, or catchRuntimeErrorgracefully
See also
- Permissions System
Learn about the permission system and how it works with user context.
- Custom User Context Integration
Detailed examples of external auth integration.