At first, an anonymous reporting system looks like a fairly ordinary CRUD application.
Someone submits a report.
An administrator receives it.
Evidence gets attached.
The case moves through a few statuses.
Eventually, someone closes it.
You can model that with roles:
reporter
investigator
admin
And permissions:
reporter -> create report, read own case
investigator -> read cases, add notes
admin -> read everything, manage users
Simple.
Until the first sensitive case arrives.
Imagine an employee reports their own manager.
That manager happens to be an administrator.
Should admin still mean:
can_read_all_cases = true
Obviously not.
And that's where the authorization model starts getting much more interesting.
The problem is no longer:
What role does this user have?
It becomes:
What is this user's relationship with this specific case?
Role-based access is only the first layer
RBAC works well when permissions mostly follow organizational roles.
A billing employee sees billing information.
Support sees tickets.
Administrators configure the system.
Sensitive investigations introduce another dimension: context.
A user can be perfectly authorized to investigate reports in general and still be forbidden from accessing one particular report.
For example:
can_access_case =
has_investigator_role
&& !is_subject_of_case
&& !has_conflict_of_interest
That is already different from standard RBAC.
Now imagine another situation.
An HR director is allowed to access investigations.
But the case mentions their spouse.
Or their direct report.
Or their own department.
The role didn't change.
The context did.
This is why systems that deal with sensitive cases often need some combination of:
- role-based permissions;
- case-level permissions;
- explicit exclusions;
- conflict-of-interest rules;
- temporary assignments;
- auditable overrides.
"Admin" stops being a sufficient security model very quickly.
Case access and identity access should not be the same permission
Another mistake is treating a report as one indivisible object.
Suppose the report contains:
case
├── allegation
├── attachments
├── reporter identity
├── internal notes
├── interviews
├── investigation findings
└── resolution
Does everyone who can access the case need to see every field?
Usually not.
An investigator may need to read the allegation but not know the reporter's identity.
Someone reviewing evidence may need attachments but not internal interview notes.
A compliance lead may need to approve the final decision without seeing unnecessary personal information.
So instead of:
can_view_case
you start needing permissions closer to:
can_view_report
can_view_reporter_identity
can_view_evidence
can_view_internal_notes
can_add_evidence
can_interview
can_assign_case
can_close_case
This matters for privacy, but it also changes your domain model.
The reporter's identity should not just be another field conveniently returned by:
GET /cases/:id
If identity deserves stronger protection, model it that way.
Different storage.
Different access policy.
Different audit trail.
Possibly different encryption boundaries.
The data model should reflect the sensitivity model.
Anonymity is not a boolean
Developers are often tempted to model anonymity like this:
{
"anonymous": true
}
But anonymity is not really a property of the report.
It is a property of the entire system around the report.
You can remove the name field and still collect:
- IP addresses;
- analytics identifiers;
- session metadata;
- authentication information;
- reverse-proxy logs;
- device fingerprints;
- email notifications containing sensitive context.
The UI may tell the user:
Your report is anonymous.
Meanwhile the infrastructure may know exactly where it came from.
That doesn't necessarily mean those systems are malicious.
It often means nobody modeled anonymity as an architectural requirement.
For sensitive reporting, a better question is:
Which pieces of information could allow us to identify this person, and do we actually need them?
That's data minimization as an engineering decision, not just a privacy-policy sentence.
Anonymous follow-up creates another design problem
If you don't know who the reporter is, how do you ask them another question?
You still need conversations like:
Can you tell us approximately when this happened?
or:
Do you still have the original file?
Traditional messaging assumes identity.
Anonymous reporting can't.
One pattern is to issue an opaque credential when the report is created:
report_id
access_token
The reporter stores that credential and uses it to return later.
Something conceptually like:
POST /reports
201 Created
{
"report_id": "RPT-8FH2K",
"access_token": "<opaque-secret>"
}
The token is not an account.
It doesn't need an email address.
It simply proves that whoever holds it is allowed to access the reporter-facing side of that specific case.
That lets the system provide bidirectional communication without forcing identity.
But then more questions appear:
- Can the token be recovered?
- Can administrators see it?
- Should it be stored hashed?
- Can it be rotated?
- What happens if it is leaked?
- How long does it remain valid?
The apparently simple requirement "allow anonymous follow-up" becomes an authentication design problem.
Evidence needs history, not just storage
Attachments are another deceptively simple feature.
The product requirement may begin as:
Users can upload evidence.
So you create:
attachments
and store files somewhere.
But once those files may influence an investigation, the system needs to answer more questions.
Where did this file come from?
When was it uploaded?
Was it replaced?
Who downloaded it?
Did anyone modify its metadata?
Was it deleted?
If it was deleted, why?
A useful evidence record might look more like:
Evidence
--------
id
case_id
original_filename
storage_key
content_hash
submitted_at
submitted_by
source_type
status
And then every important operation becomes an event:
EVIDENCE_UPLOADED
EVIDENCE_VIEWED
EVIDENCE_DOWNLOADED
EVIDENCE_MARKED_IRRELEVANT
EVIDENCE_RESTRICTED
Notice what is missing:
EVIDENCE_SILENTLY_DISAPPEARED
For sensitive systems, destructive CRUD operations deserve suspicion.
Sometimes the better model is not:
DELETE FROM evidence
WHERE id = ?;
but something closer to:
status = discarded
discard_reason = duplicate
discarded_by = investigator_42
discarded_at = ...
The original remains protected according to retention rules, while the investigation history remains understandable.
Whether you need immutable storage, hashing, signed records or stronger chain-of-custody controls depends on the system.
But auditability should be designed before you need it.
Your audit log is security infrastructure
Most applications have logs.
That doesn't automatically mean they have an audit trail.
An application log might say:
GET /api/cases/381 200
An investigation audit trail should answer something more useful:
actor: user_827
action: viewed_reporter_identity
case: case_381
timestamp: ...
reason: assigned_investigator
Those are different things.
For highly sensitive operations, I want to know:
- who performed the action;
- which case it affected;
- what kind of data was accessed;
- when it happened;
- whether the action was authorized;
- when relevant, why it happened.
And the audit system itself needs protection.
If administrators can rewrite the audit trail, it isn't much of an audit trail.
Investigation state is richer than open/closed
Another place where domain complexity leaks into the software is case status.
The naive model:
open
closed
doesn't tell you much.
So maybe you add:
new
triage
investigating
resolved
closed
Better.
But eventually you discover that workflow state and investigation outcome are different concepts.
A case might be:
workflow_status = closed
while its outcome is:
outcome = inconclusive
That matters because an inconclusive report is not the same thing as a false report.
A more expressive model separates them:
workflow_status:
new
triage
investigating
awaiting_information
decision_pending
closed
outcome:
substantiated
partially_substantiated
unsubstantiated
inconclusive
Why care?
Because software shapes behavior.
If your UI forces investigators to choose only:
true
false
you are embedding an oversimplified policy into the product.
Your enum is making an organizational decision.
Conflicts of interest belong in the domain model
This is another thing I initially thought belonged entirely to company policy.
It doesn't.
Suppose Alice is normally an investigator.
A report arrives involving Alice.
The system should not rely on another administrator remembering:
Oh, we probably shouldn't assign that one to Alice.
The conflict should be representable.
For example:
CaseParticipant
---------------
case_id
user_id
relationship
relationships:
reporter
subject
witness
investigator
reviewer
conflicted
Now authorization can use domain information.
Conceptually:
def can_access_case(user, case):
if not user.has_permission("investigate"):
return False
if case.has_conflict(user):
return False
return case.is_assigned_to(user)
Real systems will be more complex.
But the important point is this:
conflict of interest stops being a note in a policy PDF and becomes something the application can enforce.
That's a much stronger guarantee.
Overrides should be possible — and uncomfortable
Eventually someone will need emergency access.
Maybe every normal investigator is conflicted.
Maybe a legal hold requires access.
Maybe an incident needs escalation.
Hard-coding "never" into authorization rules can make a system unusable.
But the opposite is also dangerous:
super_admin = god_mode
A useful pattern is controlled break-glass access.
For example:
override requires:
- elevated permission
- explicit reason
- re-authentication
- audit event
- optional second approval
You are not making the action impossible.
You are making it deliberate and visible.
That difference matters.
Sensitive workflows make generic admin panels dangerous
A lot of SaaS products are built around the idea that the customer administrator owns everything.
For many products, that's reasonable.
For sensitive reporting systems, it can be exactly the wrong assumption.
The person who bought the software may not be entitled to inspect every report.
The company owner may be the subject of a report.
The account administrator may work in the department being investigated.
So tenant ownership and data access cannot automatically mean the same thing.
This is one of those cases where traditional B2B SaaS assumptions start breaking.
The customer owns the workspace.
That does not necessarily mean every customer administrator should be able to read every piece of information inside it.
The hardest requirements often appear after POST /reports
When we first think about a reporting system, it's natural to focus on intake.
The form.
Validation.
File uploads.
Anonymous submission.
Those are visible.
They are easy to put on a roadmap.
But most of the difficult engineering decisions happen afterward:
submit
↓
triage
↓
conflict detection
↓
assignment
↓
evidence collection
↓
restricted interviews
↓
decision
↓
follow-up
↓
retention
Every arrow hides authorization decisions.
Every box contains potentially sensitive data.
And every shortcut can affect whether users trust the system.
That's why I no longer think of anonymous reporting as a form with a back office.
It's a context-sensitive authorization system wrapped around an investigation workflow.
The form is just where the data enters.
A few questions I now ask much earlier
If I were designing another sensitive workflow from scratch, I'd ask these before choosing the database schema:
Can someone be authorized globally but forbidden from one specific case?
Is identity access separate from case access?
Can sensitive information disappear without leaving a trace?
Can users communicate without revealing their identity?
Can the system represent conflicts of interest?
What happens when normal authorization rules need an emergency override?
Do our statuses reflect the real domain, or just make the UI convenient?
If those questions arrive late, changing the architecture becomes much more painful.
If they arrive early, they shape a much better system.
AI assistance disclosure: AI was used during drafting and editing. The technical concepts and product decisions were reviewed by the author before publication.
Conceptual illustration of a sensitive reporting workflow. AI-generated.
Top comments (0)