Framework
Database

Framework Models

The framework declares seventeen models of its own, and every app inherits all of them. They are ordinary models — the same attributes, the same generated schema — so a migration creates their tables beside yours and you read them through the same generated classes.

This page is about what each table is for. For the columns themselves, the Schema JSON page renders every one of them live from the published schema, so it can never fall behind the models.

They live in a Model folder under the module that owns them — src/Auth/Model, src/Core/Model and so on — and are found by discovery exactly like yours. There is no switch to turn one off: a migration creates all seventeen.

Credentials

Five tables back authentication and credentials. Only the first has an ID of its own; the rest are keyed by the credential they belong to.

ModelTableWhat writes it
CredentialModelcredentialThe account itself — name, email, password hash, access level, language, time zone and avatar. It is the one model most apps extend.
CredentialDeviceModelcredential_deviceThe player IDs a credential is reachable on, registered when the app adds a device and read by the notification queue.
CredentialRefreshTokenModelcredential_refresh_tokenThe refresh tokens handed out at sign-in, so a session renews without asking for the password again.
CredentialResetModelcredential_resetThe pending password resets, one row per request, with the code that was emailed and the time it was asked for.
CredentialSpamModelcredential_spamThe addresses that asked for too much too quickly, which is how a reset or a sign-in is refused before it reaches the rest.

Settings & migrations

Two tables the framework keeps for itself. Both are written by a discovery migration rather than by anything in your code.

ModelTableWhat writes it
SettingsModelsettingsOne row per setting, keyed by section and variable, with the value as text and the type beside it so it is read back as the right one. Every migrate syncs it with the settings you registered.
MigrationsModelmigrationsThe tracking table — the name and title of every data migration that has run, which is what makes each one apply exactly once.

Emails

Three tables behind emails, covering the copy, the sending and the safety net that keeps a staging server from writing to real people.

ModelTableWhat writes it
EmailContentModelemail_contentThe subject and body of every email, one row per code and language, loaded from your templates on each migration so the copy can be edited in the database afterwards.
EmailQueueModelemail_queueThe queue — every email waiting to go out and every one already sent, with the result of each attempt kept beside it.
EmailWhiteListModelemail_white_listThe addresses still written to while the app is not in production, which is what the white list checks before anything leaves.

Logs

Five tables written by logging. They are the ones that grow, and the only ones the framework prunes on its own — see retention.

ModelTableWhat writes it
LogSessionModellog_sessionOne row per sign-in, with the address, the device and whether the session is still open. The other logs point back at it.
LogActionModellog_actionWhat each credential did, as a module and an action, tied to the session it happened in.
LogErrorModellog_errorThe errors raised, grouped by where they came from, so the same one repeating raises a count instead of filling the table.
LogQueryModellog_queryThe statements slower than DB_LOG_TIME, grouped with their timings — the slow query log.
LogDeviceModellog_deviceThe changes to the devices of a credential, as each one is added or removed.

Notifications

Two tables behind notifications, shaped like the email pair — one for the copy, one for the queue.

ModelTableWhat writes it
NotificationContentModelnotification_contentThe title and message of every push, one row per code and language, loaded on each migration the same way the email copy is.
NotificationQueueModelnotification_queueThe queue — every push waiting and every one sent, with the devices it went to and the result that came back.

Extending a framework model

None of the seventeen are closed. To add columns to one of their tables, declare a class of the same name in your app that extends it:

src/Auth/Model/CredentialModel.php
namespace App\Auth\Model;

use Framework\Auth\Model\CredentialModel as FrameworkCredentialModel;
use Framework\Database\Model\Field;
use Framework\Database\Model\Requested;

class CredentialModel extends FrameworkCredentialModel {

    #[Field, Requested]
    public string $nickname = "";

    #[Field(isKey: true), Requested]
    public int $companyID = 0;
}

Your class replaces the framework's entirely: one model, one table, one set of generated classes. The columns are read base first, so the framework's come first and yours are appended after them.

RuleWhy
The class name has to matchThe name without Model is the table name, so a class called something else is a second table rather than the same one.
No #[Model] attribute of your ownIt is read from the class you extend, so the options on it — hasTimestamps, canDelete and the rest — stay the framework's. Your class adds fields, not options.
The generated classes stay where the framework's areThe path and namespace come from the class you extend, so CredentialSchema and its siblings are written once, from your model.

A migration sees the one merged model, so your columns are added and kept. Nothing drops them.

More than columns

Every attribute a model understands works here, not only #[Field]. That is what makes extending worth doing: the framework's table gains the joins, the counts and the request fields your app needs, and the generated schema, entity and request carry them from then on.

src/Auth/Model/CredentialModel.php
namespace App\Auth\Model;

use App\Store\Model\StoreModel;
use App\Team\Model\TeamModel;
use App\Team\Model\TeamMemberModel;

use Framework\Auth\Model\CredentialModel as BaseCredentialModel;
use Framework\Database\Model\Count;
use Framework\Database\Model\Relation;
use Framework\Database\Model\Requested;
use Framework\Database\Model\SubRequest;

class CredentialModel extends BaseCredentialModel {

    // A field of the request that is never a column
    #[Requested]
    public string $idToken = "";

    // How many stores the team of this credential owns
    #[Count(
        modelName:      StoreModel::class,
        otherModelName: TeamMemberModel::class,
        fieldName:      "teamID",
    )]
    public int $storeCount = 0;

    // The membership row, joined on the user the credential is signed in as
    #[Relation(
        fieldNames:   [ "teamID", "access", "isOnline", "status" ],
        relationJoin: "TeamMember.memberID",
        ownerJoin:    "Credential.currentUser",
    )]
    public ?TeamMemberModel $member = null;

    /** @var list<string> */
    #[SubRequest(
        modelName: TeamModel::class,
        idName:    "credentialID",
        fieldName: "teamID",
        valueName: "name",
        query:     "status = Active",
    )]
    public array $teams = [];
}

None of those four add a column, so the credential table is untouched by them — they change what the schema selects and what the entity carries. Only #[Field] reaches the migration.

They are resolved against the models of your app, which is why this works: a #[Relation], a #[Count] or a #[SubRequest] declared here may point at any of your own models, even though the class it is written on belongs to the framework.