VSA Monolith Framework

XAuth mirror

A mirror is a copy of some console tables inside your database, so lists and joins stay local. This page is the contract it must keep, and the commands that prove and repair it.

The contract

The console database is the source of truth. A mirror table holds exactly the rows the console grants this app, with exactly the console's values, in exactly the columns you declared. No more, no less.

WordMeaningEnforced by
rowsthe app's tenant scope — never wider, never narrowerthe engine's scope rules
valuesevery field equals the console's, not only the ones with a fresh timestampthe content hash
columnsthe columns you declared, narrowed by the project's grant — nothing else exists in the tableschema check, drift
exactlya difference is detected, reported and repaired, never toleratedverify, self-repair

A mirror is for display and joins. It is never where a permission is decided — that goes to AuthGate.

What verify proves

Each table is compared with the console through four independent fingerprints:

FingerprintCatchesCannot see
row countinserts, deletesedits
id checksuma delete paired with an insertedits
newest updated_atedits that bump the stampedits that don't
content hashany value difference, on either sidenothing in the mirrored columns

The first three are not enough. A writer that changes a row without touching updated_at — a last-login stamp is the live example — changes none of them: the differential pull never fetches that row and the digest says "in sync". The same blindness hides a row someone edited locally. The content hash is an order-independent hash of every mirrored column of every row, computed with the same SQL on both sides.

Self-repair

The differential pull is an optimisation; the fingerprints are the truth. After a differential, xauth:mirror:sync re-checks them, and if the table still differs it prints escalating to a full refresh and repairs it in the same run. The refresh swaps inside one transaction, so readers never see a half-filled table.

When a table reports drift

Rows are synced, but the table is not an exact copy, and the command exits non-zero. A column the manifest does not mirror is never written again and keeps its old values for good — extra data that looks like data.

FindingMeaningDo
columns_extrathe local table has columns outside the manifestadd them to the manifest, or rebuild
columns_retypeda column's type differs from the console's, or a varchar is narrowerrebuild
rebuild one table
php artisan xauth:mirror:sync --replace-tables --table=um_branches

That drops the table and recreates it from the console's definition, so it asks first (--force skips the question). It is never automatic: a column outside the manifest may be read by your code, and only you know whether to mirror it or drop it.

One app, several databases

An app with more than one database (a core database plus an HR one) declares one profile per target in config/xauth_client.php. The top-level mirrors keys are the default profile; every other target goes under profiles.

config/xauth_client.php
'mirrors' => [
    'tables'   => [ /* the default profile: the app's own connection */ ],
    'profiles' => [
        'activehr' => [
            'connection' => 'activehr',   // required: never falls back to the default
            'namespace'  => 'App\\Models\\ActiveHr\\Xauth',
            'path'       => app_path('Models/ActiveHr/Xauth'),
            'tables'     => [ /* same shape as above */ ],
        ],
    ],
],

A profile must name its connection. Falling back to the default would write one product's mirror into another's database, so it throws instead.

Commands

CommandWhat it does
xauth:mirror:syncBrings every profile in line, every table. --profile=<name> picks one database
xauth:mirror:verifyCompares all four fingerprints for every profile. Exits non-zero if anything differs — for cron and CI
xauth:mirror:statusPer profile and table: local vs console rows, watermark, last sync
xauth:mirror:sync --dry-run --schema-onlyWhat the schema phase would do, including drift. Writes nothing
xauth:mirror:sync --full --table=um_xForce a full refresh of one table

Each profile is isolated: a database that is down fails its own pass, the rest still run, and the command exits non-zero.

What keeps it correct

TriggerRole
the scheduled xauth:mirror:syncThe guarantee. It must exist
the console's change callbackSpeed only. It carries ids, never data, and is signed
identity write-throughRefreshes a user right after the app wrote it
a manual syncAn engineer's repair

Where each customer runs on its own host, the console's callback URL may contain {subs_code}; it is filled in per customer and each one is told only its own changes.

Rules that must always hold

  • Never authorise from the mirror.
  • Never write a mirror table by hand and never add columns to it. Keep your own data in your own table with a foreign key to the mirrored id.
  • Never hide a failing command's output. It names the cause and the exact fix.
  • A named profile always has an explicit connection.
  • Never make correctness depend on a callback or a queue. The scheduled pull is the guarantee.