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.
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.
| Model | Table | What writes it |
|---|---|---|
CredentialModel | credential | The account itself — name, email, password hash, access level, language, time zone and avatar. It is the one model most apps extend. |
CredentialDeviceModel | credential_device | The player IDs a credential is reachable on, registered when the app adds a device and read by the notification queue. |
CredentialRefreshTokenModel | credential_refresh_token | The refresh tokens handed out at sign-in, so a session renews without asking for the password again. |
CredentialResetModel | credential_reset | The pending password resets, one row per request, with the code that was emailed and the time it was asked for. |
CredentialSpamModel | credential_spam | The 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.
| Model | Table | What writes it |
|---|---|---|
SettingsModel | settings | One 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. |
MigrationsModel | migrations | The 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.
| Model | Table | What writes it |
|---|---|---|
EmailContentModel | email_content | The 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. |
EmailQueueModel | email_queue | The queue — every email waiting to go out and every one already sent, with the result of each attempt kept beside it. |
EmailWhiteListModel | email_white_list | The 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.
| Model | Table | What writes it |
|---|---|---|
LogSessionModel | log_session | One row per sign-in, with the address, the device and whether the session is still open. The other logs point back at it. |
LogActionModel | log_action | What each credential did, as a module and an action, tied to the session it happened in. |
LogErrorModel | log_error | The errors raised, grouped by where they came from, so the same one repeating raises a count instead of filling the table. |
LogQueryModel | log_query | The statements slower than DB_LOG_TIME, grouped with their timings — the slow query log. |
LogDeviceModel | log_device | The 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.
| Model | Table | What writes it |
|---|---|---|
NotificationContentModel | notification_content | The title and message of every push, one row per code and language, loaded on each migration the same way the email copy is. |
NotificationQueueModel | notification_queue | The 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:
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.
| Rule | Why |
|---|---|
| The class name has to match | The 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 own | It 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 are | The 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.
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.