<- All posts
Writing

Why I built django-system-audit

Signal-based audit logging is architecturally broken for compliance systems. Here is what I built instead.

Most Django audit libraries rely heavily on signals.

Signals are great for detecting that something happened. But they struggle to answer a more important question: why did it happen?

While working on compliance-heavy systems — healthcare platforms with HIPAA requirements, fintech systems with SOC2 obligations — this caused real problems:

  • Duplicate audit logs from multiple signals firing on the same event
  • Confusing actor attribution — was this the API, the Admin panel, or a background job?
  • Missing request context like IP address or endpoint
  • Audit trails that were hard to trust when debugging incidents

The question that kept coming up during compliance reviews: “Who did this, from where, and why?” Signals couldn’t answer it.

The core architectural problem

Signals capture effects. When you save a model, a signal fires. But the signal doesn’t know what triggered the save — it just knows the save happened.

This means:

# A signal-based audit logger sees this:
# "Order #1234 was updated"

# What compliance actually needs:
# "Order #1234 was updated by user@example.com
#  via PUT /api/orders/1234/
#  from IP 192.168.1.1
#  because the payment status changed"

The difference isn’t cosmetic. In a compliance audit, “the record was updated” is useless. “The record was updated by this actor, through this interface, with this context” is what makes an audit trail legally defensible.

What I built instead

django-system-audit takes a different approach: explicit, intent-aware logging.

Instead of relying on signals, views and admin actions record audit events directly through a central service:

from system_audit import audit


class OrderViewSet(viewsets.ModelViewSet):
    def update(self, request, *args, **kwargs):
        response = super().update(request, *args, **kwargs)

        audit.log(
            actor=request.user,
            action="order.updated",
            target=self.get_object(),
            context={"ip": request.META.get("REMOTE_ADDR")},
        )

        return response

Signals still exist as a fallback — but they are no longer the primary mechanism. Views capture intent. Signals catch what slips through.

The result

Cleaner audit trails. Correct actor attribution. Full request context. And when a compliance team asks “who changed this record and why” — you have an actual answer.

The library is on PyPI:

pip install django-system-audit

GitHub: django-system-audit

If you are building in healthtech, fintech, or any compliance-sensitive domain — I would love to hear how you handle audit logging and whether this approach resonates.

RJ

Rajat Jog

Senior Backend Engineer from Pune, India. Specialises in Python, Django, and FastAPI. Builds HIPAA/SOC2-compliant systems and open-sources the hard parts.

<- Back to writing