ZCore LogoZCore
Quick learn

Step 4 - Defining Models & Permissions

Create standard SQLAlchemy 2.0 models and learn how ZCore auto-generates permission keys.

ZCore uses standard SQLAlchemy 2.0 declarative models. It does not force you to learn a new ORM syntax. Let's define our Task model.

Write the Model

Open tasks/models.py and define the model:

# tasks/models.py
import uuid
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column
from zcore import Base, SoftDeleteMixin

class Task(Base, SoftDeleteMixin):
    __tablename__ = "tasks"

    id: Mapped[uuid.UUID] = mapped_column(
        primary_key=True,
        default=uuid.uuid4,
    )
    title: Mapped[str] = mapped_column(String(255))
    is_completed: Mapped[bool] = mapped_column(default=False)
    assignee_email: Mapped[str | None] = mapped_column(String(255), nullable=True)

Create an Alembic Migration

ZCore does not use Base.metadata.create_all() as a replacement for database migrations.

For a real application, schema changes should be tracked through Alembic migrations. This gives you a versioned database schema that can be reproduced across development, staging, and production environments.

If your project does not have Alembic initialized yet, initialize it from the project root:

alembic init alembic

Configure Alembic to use your database connection and make sure its env.py can access the application's SQLAlchemy metadata.

Then generate a migration from the Task model:

alembic revision --autogenerate -m "create tasks table"

Alembic will generate a migration inside the alembic/versions/ directory.

Review the generated migration before applying it. Autogenerated migrations should always be treated as a starting point rather than blindly trusted.

Finally, apply the migration:

alembic upgrade head

Why Alembic? Base.metadata.create_all() is useful for quick experiments, but it does not provide versioned schema changes, upgrade paths, downgrade paths, or a reliable migration history. Alembic is the appropriate tool for managing a production database schema.

After applying the migration, you can inspect the current database revision with:

alembic current

And inspect the complete migration history with:

alembic history

When you later modify the Task model, create a new migration instead of modifying the database manually:

alembic revision --autogenerate -m "add task field"

Then apply the new migration:

alembic upgrade head

This keeps your database schema synchronized with your application's models through an explicit, version-controlled migration history.

The Magic of model.actions()

Under the hood: The Base class you inherited from provides a special classmethod called actions().

If you call Task.actions(), it returns an Actions dataclass mapping standard operations and lookups to permission strings based on your table name:

  • CREATE: "tasks:create"
  • VIEW: "tasks:view"
  • LISTVIEW: "tasks:listview"
  • LOOKUP: "tasks:lookup"
  • UPDATE: "tasks:update"
  • DELETE: "tasks:delete"

We will use these strings later to secure our endpoints automatically.

Your database schema is ready. Next, we will define the Pydantic schemas (with security rules) for this model.

On this page