Audit · Guides
Send events to Audit
Events wait in the outbox in your own database until Audit confirms it has them. The scheduler sends them every minute, in order.
How the outbox works
Audit::record() never calls Audit. It adds a row to the outbox table, in your transaction, and returns. php artisan audit:ship sends the outbox: up to 500 events in a request, oldest first, and up to 20 requests in a run. It forgets a row only once Audit has answered that it kept the event.
So recording works the same whether Audit is up, slow or down, and no event is lost on the way.
Run the scheduler
The SDK adds audit:ship to Laravel's scheduler, every minute, on one server. The scheduler has to run:
php artisan schedule:workOr with cron, the way Laravel's docs set it up: * * * * * php artisan schedule:run in the app's folder. Without it, events wait in the outbox and nothing arrives.
An event leaves once it is two seconds old (ship.settle_seconds). Rows are sent in the order of their ids, and a transaction that started first may commit a moment after one that started later: the pause keeps a late commit from being overtaken.
In order, and once
Audit adds the events of a batch to the project's chain in the batch's order, each with its number. It answers for each one, and the SDK acts on the answer:
| Audit says | The SDK |
|---|---|
Kept, or kept already (duplicate) | Forgets the outbox row. |
| Refused, with a reason | Keeps the row aside with the reason, logs an error and never sends it again as it is. The events after it go on. |
413, too big | Sends the same events again in half the batch. |
401, 429, any other error, or no answer | Stops. Everything waits, and the next run starts again from the same oldest event. |
Every event has its own id, so a batch sent again after an answer that never arrived is kept once: the events Audit has already come back as duplicate, with their number.
See what waits
audit:ship says what it sent: Sent 12 event(s). 0 waiting. When it stops, it says why, ends with an error, and the events wait for the next run. To look without sending:
$ php artisan audit:ship --status
3 event(s) waiting, the oldest since 2026-11-01 09:14:02.120 UTC.
Last try: The service is busy (429).
1 refused by the service:
customer.viewed: occurred_at is in the future: check the host's clock.The SDK also logs a warning to your app's log each time a run stops with events waiting, and an error for each event Audit refuses.
Refused events
Audit refuses an event it can't take as it is, with its reason: a time more than 5 minutes ahead of its clock, say, or an email address sent past the SDK. The SDK keeps that row aside in the outbox and never sends it again as it is. The events after it go on.
Once you have fixed the cause, put the refused events back in line, in their old place:
php artisan audit:ship --retry-refusedDon't delete refused rows
When Audit can't be reached
When Audit is down, busy (429), doesn't know the secret (401) or answers with any other error, the run stops at once. Everything waits, and the next run starts again from the same oldest event, so nothing overtakes. When Audit is back, the outbox empties a batch at a time, oldest first.
Without MYBEAST_AUDIT_URL or a well-formed MYBEAST_AUDIT_SECRET, nothing is sent and events wait as well: a server whose .env isn't complete yet loses nothing.
Several apps, one log
- Several servers of one app share its database and so its outbox. One shipper is enough, and two would send the same events: the scheduler runs
audit:shipon one server only, and the SDK takes a lock as well. Both need a cache your servers share, such as Redis or the database. - Several apps each have their own outbox and ship it with the project's secret, into the project's one log. Give each app a server secret of its own, so one can be revoked alone.
- A developer's checkout sets
MYBEAST_AUDIT_ENABLED=false, so it writes nothing into the project's log. Tests useAudit::fake().