The account activity journal: what we record, and what never reaches it
Every action on a Mailclutch account is now written to an append-only journal — sign-ins, invitations, project deletions, share links. This is the shape of a line, why it is written inside your transaction, and the allowlist that keeps secrets out of it.
Your account settings now have a journal. It lists what happened, when, from which address, and who did it: sign-ins and failed sign-ins, password changes, two-factor being turned on, invitations sent and accepted, projects created and emptied, SMTP credentials rotated, share links created and revoked, webhooks added and deleted. This is the part of the work worth writing down, because most of the decisions in it are the ones that make an audit trail either trustworthy or decorative.
Append-only, and it outlives what it describes
A row is written once. There is no update path and no delete path — the model has no updated_at at all — so a line cannot be quietly corrected after the fact.
The harder problem is that a trail describes things that get deleted. Delete an organization and the rows about it are still the record of what happened to it. So each line links the actor and the organization and stores their name and email as text at the time of writing. The foreign keys null out when the target goes; the words stay. Reading a two-year-old row never depends on the row it points at still existing.
Public identifiers are hashed on the way out. The table's auto-increment id is a global counter shared by every tenant, and a counter you can watch is a business metric you did not mean to publish, so it never leaves the server as-is.
Written inside your transaction, on purpose
The obvious design is to queue the write. We do not: the logger is synchronous and joins the caller's transaction.
Two reasons. The IP address and the user agent only exist inside the request, so a queued job would have to carry them along and would be recording what a worker was told rather than what a server saw. And an audit trail that a failed job can silently drop is worse than no trail, because you will trust it.
The consequence is deliberate and worth stating plainly: if the action rolls back, so does its line. We do not audit what did not happen. An invitation whose transaction failed leaves no trace of having been sent, because it was not.
The allowlist
Each line can carry a small bag of properties — which project, which role, which domain. That bag is written verbatim, which is exactly why the rule around it is absolute: callers pass an explicit list of keys, never the request body and never the validated input. Passwords, webhook signing secrets, plain-text API tokens, recovery codes and generated SMTP credentials have no path into it.
"Redaction" would be the wrong shape here. A redactor is a filter you have to keep ahead of a growing list of field names, and it fails open — the day someone adds smtp_password_confirmation, the filter has never heard of it. An allowlist fails closed: a new field is absent until someone writes it down.
The vocabulary is frozen
Event types read as domain.action — auth.login, share.inbox_revoked, domain.verification_failed — the same shape as the payloads our webhooks send, so the two never drift into different spellings of the same event.
Once a value has been written to the database it is never renamed and never removed, even when the feature behind it is gone. An old row still has to be readable; a missing case would throw on the cast and take the whole page down with it. "There are no rows in the local database" proves nothing about production, so retired types stay in the enum with a note.
What it is for
The journal is per account today. The organization-wide audit log — the same rows, read by an owner across every member — builds directly on it and is the next thing we ship.
In the meantime it already answers the questions you actually ask after something goes wrong: when did that share link get created, who removed that member, and was that sign-in from Lisbon me. That last one is the session list and the new-device alert doing their half of the job, and the journal keeping the record.
The events are also the vocabulary you will see in the API. If you are wiring Mailclutch into your own tooling, start from the API reference.