Skip to content

User Models

Think of a model as a blueprint for your database table. Just like an architect's blueprint defines what a building will look like, a model defines what data you'll store and how it's structured. In Edgy, models are Python classes that automatically become database tables.

Ravyn provides pre-built User models so you don't have to reinvent the wheel for authentication.

What You'll Learn

  • Using Ravyn's built-in User models
  • Creating and managing users
  • Password hashing and validation
  • Leveraging settings for cleaner code
  • Best practices for user authentication

Quick Start

from ravyn.contrib.auth.edgy.models import User
from ravyn import RavynSettings

# Create a user
user = await User.query.create_user(
    email="user@example.com",
    password="securepassword123",
    first_name="John"
)

# Verify password
is_valid = user.check_password("securepassword123")  # True

User Models Overview

Ravyn provides two user models for Edgy integration:

  1. AbstractUser - Base class with all user fields and functionality
  2. User - Ready-to-use subclass of AbstractUser

You can use User directly or extend AbstractUser for custom fields.


Basic Usage

Using the Default User Model

from datetime import datetime
from enum import Enum

from edgy import Database, Registry, fields

from ravyn.contrib.auth.edgy.base_user import User as BaseUser

database = Database("<YOUR-SQL-QUERY_STRING")
models = Registry(database=database)


class UserType(Enum):
    ADMIN = "admin"
    USER = "user"
    OTHER = "other"


class User(BaseUser):
    """
    Inherits from the BaseUser all the fields and adds extra unique ones.
    """

    date_of_birth: datetime = fields.DateField()
    is_verified: bool = fields.BooleanField(default=False)
    role: UserType = fields.ChoiceField(
        UserType,
        max_length=255,
        null=False,
        default=UserType.USER,
    )

    class Meta:
        registry = models

    def __str__(self):
        return f"{self.email} - {self.role}"


async with models:
    # Create the db and tables
    # Don't use this in production! Use Alembic or any tool to manage
    # The migrations for you
    await models.create_all()

    await User.query.create(is_active=False)

    user = await User.query.get(id=1)
    print(user)
    # User(id=1)

This gives you a complete user authentication system with minimal code.

User Model Fields

The User model includes these Django-inspired fields:

Field Type Description
first_name CharField User's first name
last_name CharField User's last name
username CharField Unique username
email EmailField User's email address
password CharField Hashed password
last_login DateTimeField Last login timestamp
is_active BooleanField Account active status
is_staff BooleanField Staff access flag
is_superuser BooleanField Superuser access flag

Leveraging Settings

Instead of repeating database configuration, use Ravyn settings to centralize your setup:

from typing import TYPE_CHECKING

from functools import cached_property
from edgy import Registry

from ravyn.conf.global_settings import RavynSettings

if TYPE_CHECKING:
    from edgy import EdgySettings


class AppSettings(RavynSettings):
    # this strategy works only when there is a single set of models (no clashing model names, no redefinitions)
    # otherwise have a look in ravyn tests how it is solved
    @cached_property
    def registry(self) -> Registry:
        return Registry("<YOUR-SQL-QUERY-STRING")

    # optional, in case we want a centralized place
    @cached_property
    def edgy_settings(self) -> "EdgySettings":
        from edgy import EdgySettings

        return EdgySettings(preloads=["myproject.models"])
from datetime import datetime
from enum import Enum

from edgy import fields

from ravyn.conf import settings
from ravyn.contrib.auth.edgy.base_user import User as BaseUser

models = settings.registry


class UserType(Enum):
    ADMIN = "admin"
    USER = "user"
    OTHER = "other"


class User(BaseUser):
    """
    Inherits from the BaseUser all the fields and adds extra unique ones.
    """

    date_of_birth: datetime = fields.DateField()
    is_verified: bool = fields.BooleanField(default=False)
    role: UserType = fields.ChoiceField(
        UserType,
        max_length=255,
        null=False,
        default=UserType.USER,
    )

    class Meta:
        registry = models

    def __str__(self):
        return f"{self.email} - {self.role}"
import os
import sys

from ravyn import Ravyn, Include


def build_path():
    """
    Builds the path of the project and project root.
    """  #
    SITE_ROOT = os.path.dirname(os.path.realpath(__file__))

    if SITE_ROOT not in sys.path:
        sys.path.append(SITE_ROOT)
        # in case of an application model with apps
        sys.path.append(os.path.join(SITE_ROOT, "apps"))


def disable_edgy_settings_load():
    os.environ["EDGY_SETTINGS_MODULE"] = ""


def get_application():
    """
    Encapsulating in methods can be useful for controlling the import order but is optional.
    """
    # first call build_path
    build_path()
    # this is optional, for rewiring edgy settings to ravyn settings
    disable_edgy_settings_load()  # disable any settings load
    # import edgy now
    from edgy import Instance, monkay
    from ravyn.conf import settings

    monkay.settings = lambda: settings.edgy_settings  # rewire
    monkay.evaluate_settings_once(ignore_import_errors=False)  # import manually

    # now the project is in the search path and we can import
    registry = settings.registry

    app = registry.asgi(
        Ravyn(
            routes=[Include(namespace="my_project.urls")],
        )
    )

    monkay.set_instance(Instance(registry=registry, app=app))
    return app


app = get_application()

This approach lets you:

  • Access database configuration anywhere in your codebase
  • Avoid repeating yourself
  • Easily switch between environments
  • Share configuration across multiple apps

User Management Functions

Warning

The following examples assume you're using settings as described above.

create_user

Create a regular user with automatic password hashing:

from pydantic import EmailStr

from edgy import monkay

from .accounts.models import User


async def create_user(
    first_name: str, last_name: str, username: str, email: EmailStr, password: str
) -> User:
    """
    Creates a user in the database.
    """
    registry = monkay.instance.registry
    async with registry:
        User = registry.get_model("User")
        user = await User.query.create_user(
            username=username,
            password=password,
            email=email,
            first_name=first_name,
            last_name=last_name,
        )
        return user

What happens behind the scenes:

  • Password is automatically hashed using bcrypt
  • User is created in the database
  • Returns the User instance

create_superuser

Create an admin user with elevated privileges:

from pydantic import EmailStr
from edgy import monkay

from .accounts.models import User


async def create_superuser(
    first_name: str, last_name: str, username: str, email: EmailStr, password: str
) -> User:
    """
    Creates a superuser in the database.
    """
    registry = monkay.instance.registry
    async with registry:
        User = registry.get_model("User")
        user = await User.query.create_superuser(
            username=username,
            password=password,
            email=email,
            first_name=first_name,
            last_name=last_name,
        )
        return user

Automatically sets: - is_staff = True - is_superuser = True - is_active = True

check_password

Verify a user's password:

from pydantic import EmailStr

from edgy import monkay


# Check if password is valid or correct
async def check_password(email: EmailStr, password: str) -> bool:
    """
    Check if the password of a user is correct.
    """
    registry = monkay.instance.registry
    async with registry:
        User = registry.get_model("User")
        user: User = await User.query.get(email=email)

        is_valid_password = await user.check_password(password)
        return is_valid_password

Security features:

  • Compares against hashed password
  • Constant-time comparison (prevents timing attacks)
  • Returns boolean (True/False)

set_password

Change a user's password:

from pydantic import EmailStr
from edgy import monkay


# Update password
async def set_password(email: EmailStr, password: str) -> None:
    """
    Set the password of a user is correct.
    """
    registry = monkay.instance.registry
    async with registry:
        User = registry.get_model("User")
        user: User = await User.query.get(email=email)

        await user.set_password(password)

What happens:

  • New password is hashed
  • Old password is replaced
  • User instance is updated (remember to save!)

Password Hashing

Ravyn uses secure password hashing out of the box.

Default Hashers

@property
def password_hashers(self) -> list[str]:
    return [
        "ravyn.contrib.auth.hashers.BcryptPasswordHasher",
    ]

Ravyn uses passlib under the hood for secure password hashing.

Custom Hashers

Override the password_hashers property in your settings:

from typing import List

from ravyn import RavynSettings
from ravyn.contrib.auth.hashers import BcryptPasswordHasher


class CustomHasher(BcryptPasswordHasher):
    """
    All the hashers inherit from BasePasswordHasher
    """

    salt_entropy = 3000


class MySettings(RavynSettings):
    @property
    def password_hashers(self) -> List[str]:
        return ["myapp.hashers.CustomHasher"]

Common Pitfalls & Fixes

Pitfall 1: Forgetting to Save After set_password

Problem: Password change doesn't persist.

# Wrong - password not saved
user.set_password("newpassword")
# User logs out, can't log back in!

Solution: Always save after setting password:

# Correct
user.set_password("newpassword")
await user.save()

Pitfall 2: Storing Plain Text Passwords

Problem: Manually setting password field.

# Wrong - plain text password!
user = await User.query.create(
    email="user@example.com",
    password="plaintext123"  # Not hashed!
)

Solution: Use create_user or set_password:

# Correct
user = await User.query.create_user(
    email="user@example.com",
    password="plaintext123"  # Automatically hashed
)

Pitfall 3: Not Using Unique Constraints

Problem: Duplicate emails or usernames.

Solution: Add unique constraints in your model:

class CustomUser(AbstractUser):
    email = fields.EmailField(unique=True)
    username = fields.CharField(max_length=150, unique=True)

    class Meta:
        registry = registry

Best Practices

1. Use create_user for User Creation

# Good - automatic password hashing
user = await User.query.create_user(
    email="user@example.com",
    password="secure123"
)

2. Validate Before Creating

# Good - validate input first
from pydantic import BaseModel, EmailStr

class UserCreate(BaseModel):
    email: EmailStr
    password: str

    @validator('password')
    def password_strength(cls, v):
        if len(v) < 8:
            raise ValueError('Password must be at least 8 characters')
        return v

# Then create user
data = UserCreate(email="user@example.com", password="securepass123")
user = await User.query.create_user(**data.dict())

3. Use Settings for Registry

# Good - centralized configuration
from ravyn.conf import settings

class User(AbstractUser):
    class Meta:
        registry = settings.edgy_registry

Database Migrations

Edgy provides built-in migration commands that use Alembic under the hood:

# Install Edgy (includes Alembic)
pip install edgy

# Initialize migrations
edgy init

# Create migration
edgy makemigrations

# Apply migrations
edgy migrate

Edgy's migration system supports auto-discovery and can be configured using the --app flag or EDGY_DEFAULT_APP environment variable.

For advanced migration scenarios, you can also use Alembic directly. See Edgy Migrations for complete documentation.

Initialize Migrations

Auto-Discovery:

$ edgy init

Edgy automatically discovers your application in src/main.py following its search pattern.

Using --app:

$ edgy --app src.main init

Using Environment Variable:

$ export EDGY_DEFAULT_APP=src.main
$ edgy init

Create Migrations

Auto-Discovery:

$ edgy makemigrations

Using --app:

$ edgy --app src.main makemigrations

Using Environment Variable:

$ export EDGY_DEFAULT_APP=src.main
$ edgy makemigrations

Note

As of version 0.23.0, the import path must point to a module where the Instance object triggers automatic registration.

Using Preloads

Instead of --app or EDGY_DEFAULT_APP, use the preloads setting in your configuration to specify an import path. When an instance is set in a preloaded file, auto-discovery is skipped.

Edgy provides detailed migration guidance in its documentation.


Learn More


Next Steps