There is a question every internal app eventually has to answer: who changed this?
It usually comes up at the worst possible moment. A price changed. A shipment moved. An approval status flipped from pending to complete. A customer record looks different than it did yesterday. The business does not need a theory. It needs an answer: what happened, when did it happen, and who did it?
If the system cannot answer that, people stop trusting it. They go back to screenshots, email chains, spreadsheet copies, and hallway memory. That is the beginning of the end for an internal tool.
I have written a lot about replacing fragile spreadsheets and Access databases with real applications. Clean data models matter. Workflow states matter. Deployments and schema changes matter. But audit trails are the part that turns an app from “this stores data” into “this can be trusted when the data matters.”
## Audit Trails Are Not About Blame
The worst way to sell audit history is as a surveillance feature. Nobody wants to feel like the software is waiting to catch them making a mistake.
The right framing is support and trust. Audit trails help a team reconstruct reality. They make it possible to answer questions without guessing. They protect users from being blamed for things they did not do. They make accidental changes recoverable. And they give managers confidence that the system is not a black box.
When someone asks, “why did this order ship late?” the answer should not depend on whoever remembers the most recent hallway conversation. It should be visible in the system.
## What an Audit Trail Should Capture
For most internal business apps, the audit record does not need to be clever. It needs to be consistent.
At minimum, I want to know:
- Who made the change
- When the change happened
- What record was changed
- What action was performed: created, updated, deleted, approved, rejected, exported, assigned
- What changed: old value, new value, or a before/after snapshot
- Why, when the process needs a comment or reason code
That sounds basic because it is. The value is not in a fancy logging platform. The value is in being able to answer business questions with confidence.
## Do Not Log Everything Blindly
The easiest audit trail to build is also the least useful: dump every request body into a table and call it done. That creates noise, risk, and eventually a storage problem. Worse, it can capture things you should not retain, like passwords, tokens, private notes, or sensitive personal data.
Good audit trails are intentional. Log the parts of the system where history matters:
- Status changes in workflows
- Approvals and rejections
- Money, price, cost, and quantity fields
- Customer, vendor, inventory, or compliance records
- Permission and role changes
- Deletes, voids, cancellations, and overrides
Not every typo fix deserves a courtroom transcript. But anything that changes a decision, a shipment, a payment, a report, or a compliance outcome should leave a trail.
## A Simple Data Model Works
Most of the time I start with one table. Something like AuditLog with fields for user, action, entity type, entity ID, old values, new values, timestamp, and optional metadata.
In Prisma terms, the model might look conceptually like this:
model AuditLog {
id Int @id @default(autoincrement())
userId Int?
action String
entityType String
entityId String
oldValues Json?
newValues Json?
reason String?
createdAt DateTime @default(now())
@@index([entityType, entityId])
@@index([userId])
@@index([createdAt])
}
The exact fields depend on the app, but the shape is boring on purpose. You want audit data to be easy to write and easy to query. When a user opens an order, part, customer, or approval request, the history panel should be a straightforward query: show me every audit event for this entity, newest first.
## Put Audit Writes Near the Business Action
Audit logging should sit next to the action that matters, not sprinkled randomly through button handlers. If the app has a service function that approves a request, that function should both update the request and create the audit entry. If the code changes inventory quantity, that same unit of work should write the history.
Ideally, the business change and the audit entry happen in the same transaction. With Prisma, that usually means using $transaction so you do not end up with a changed record and no history, or a history entry for a change that failed.
This is another reason I try to keep business logic out of the UI. If important behavior is spread across pages and form handlers, audit trails become inconsistent. If the important behavior lives in a clean application layer, logging becomes part of the rule instead of an afterthought.
## Make History Visible to Users
An audit trail buried in the database is useful to developers, but the real payoff comes when users can see it.
A simple history tab can prevent a dozen support questions. Show the timestamp, the person, the action, and a readable summary: “Aaron changed status from Pending to Approved” or “Jessica updated quantity from 12 to 10.” Keep the raw JSON for developers, but give users the story in human language.
This is where audit history changes the tone of the app. Users stop treating it like a fragile form and start treating it like the shared record of what happened.
## Deletes Deserve Special Care
Deletes are where audit trails earn their keep. In many business systems, I avoid hard deletes for important records. I prefer soft deletes, void states, cancellation states, or archival flags. The record remains, the workflow reflects what happened, and the audit trail explains why.
If something truly has to be deleted, log enough context before it disappears. Record the entity type, ID, display name, and the fields someone would need to understand what was removed. A delete entry that only says “record 42 deleted” is better than nothing, but not by much.
## Retention and Privacy Matter
Audit logs can become sensitive data. They can reveal work patterns, customer information, internal decisions, pricing, and mistakes. That means they deserve the same care as the rest of the application data.
Do not log secrets. Do not log password fields. Be careful with personal data. Decide how long audit records should live. Some industries need years of history; some internal tools only need enough to support operational troubleshooting. The right answer depends on the business, but “forever, because nobody thought about it” is not a policy.
## Start Small, But Start Early
You do not need an enterprise event-sourcing system to get value from audit history. Start with the high-risk actions. Approvals. Status transitions. Deletes. Permission changes. The fields people argue about later.
The mistake is waiting until after the first incident. By then the question has already been asked, the data is already missing, and the best you can do is promise the next incident will be easier to investigate.
Internal apps are supposed to reduce uncertainty. Audit trails are one of the simplest ways to do that. They make the system explain itself. They turn “I think” into “here is what happened.” And that is the difference between an app people use because they have to and an app people trust because it has earned it.