At first, the requirement sounded almost trivial:
Employees need a way to submit anonymous reports.
Build a form. Store the report. Give HR an admin panel.
Done.
Except it wasn't.
Once we started breaking the requirement down, a simple reporting form became an authorization problem, then a privacy problem, then a governance problem.
And most of the difficult parts had very little to do with the form itself.
The requirement
In Brazil, Law No. 14,457/2022 introduced several requirements for companies that maintain a CIPA — the country's Internal Commission for Accident and Harassment Prevention.
One of them is particularly interesting from a software perspective.
Companies need procedures for receiving and following up on reports, investigating what happened, and potentially applying sanctions while guaranteeing the anonymity of the person submitting the report.
That's a legal requirement.
But someone eventually has to turn it into software.
And that's where the ambiguity starts.
Who is allowed to see a report?
Imagine the first version of the application.
You have:
- a public reporting form;
- an authenticated dashboard;
- attachments;
- comments;
- report statuses;
- an audit history.
Pretty standard internal software.
Then this report arrives:
My manager has been harassing me.
Now suppose that manager also has administrative access to the reporting system.
Suddenly, the permission model is wrong.
It doesn't matter that the reporter's name is hidden if the person being reported can open the case, see its contents, or infer where it came from.
So we get a new rule:
Someone mentioned in a report should not be able to manage that report.
That sounds obvious after you hear it.
It isn't necessarily obvious when you design the first database schema.
admin stops being useful pretty quickly
A lot of internal software begins with something like:
user
admin
For this kind of system, that abstraction falls apart almost immediately.
You might have:
- HR;
- compliance;
- company directors;
- external investigators;
- organization administrators;
- members of the CIPA.
Different roles can see different things.
But even that isn't enough.
Because access can depend on the content of the report itself.
Alice may normally be allowed to see every report.
Unless Alice is mentioned in this one.
So authorization becomes something closer to:
can_access =
has_permission
&& !is_involved_in_report
Which means this is no longer just classic RBAC.
The authorization decision now depends on context.
And once you go down that path, a lot of other questions show up.
Who determines that someone is involved?
Can another administrator override the restriction?
What happens if every person with the appropriate role is mentioned?
Who can see that access was revoked?
Should the database enforce this, or should the application?
A requirement that originally sounded like "add an anonymous form" is now shaping the authorization model.
Anonymous to whom?
"Anonymous" is another word that sounds simple until you have to define it.
The form not asking for a name is one thing.
The company administrator being unable to identify the reporter is another.
The infrastructure itself not retaining data that could later be correlated with that person is something else entirely.
A perfectly anonymous-looking form can still sit behind:
- IP logs;
- analytics scripts;
- authentication middleware;
- session identifiers;
- reverse proxy logs;
- third-party monitoring tools.
None of those systems are inherently bad.
Most are things we would add to a normal application without thinking much about them.
But privacy-sensitive products force you to ask a different question:
Do we actually need to collect this information?
Sometimes the safest data is simply data you never stored.
Then comes the messaging problem
Reports are rarely complete.
Someone submits something like:
My manager threatened me after the meeting.
For an investigation, that may not be enough.
Which meeting?
When?
Was anyone else there?
Is there a message or document related to it?
Normally, the obvious solution is:
Send the user an email.
Except you don't know who the user is.
And ideally you don't want to know.
So the identity model needs to change.
Instead of attaching the conversation to a person, you can attach it to the report itself.
Conceptually:
report_id
access_token
The reporter keeps a credential.
They can return later, see replies, provide additional information, and follow the status of the case.
The investigator talks to the holder of that credential without knowing who that person is.
Nothing particularly exotic is happening technically.
But a domain requirement completely changes how a very ordinary feature — messaging — has to work.
Trust is another part of the architecture
There's also a problem code alone can't solve.
You can build everything correctly.
You can avoid storing IP addresses.
You can minimize logs.
You can encrypt sensitive fields.
You can implement contextual access policies.
You can automatically remove someone from a case when they're involved in it.
And an employee can still look at the Submit report button and think:
Is this actually anonymous?
That's rational.
The person using the system isn't reading the source code.
They're not inspecting proxy configuration.
They're not reviewing database policies.
They're being asked to trust the system.
So technical decisions eventually become UX decisions.
Compare:
Your report is anonymous.
with:
We do not ask for your identity or store your IP address.
The second statement is more specific.
It explains something the system actually does.
That sort of explainability matters more than I originally expected.
The law defines an outcome, not an architecture
This is probably the part I find most interesting.
Brazilian Law No. 14,457/2022 doesn't tell developers to use PostgreSQL.
It doesn't say:
use row-level security
don't log IP addresses
implement contextual RBAC
require two administrators
use magic-link authentication
It defines obligations and expected outcomes.
The implementation is left to the organization building the system.
That happens a lot in B2B software.
A requirement arrives as:
We need to comply with X.
You start decomposing it.
And underneath that sentence you find decisions involving:
- architecture;
- permissions;
- security;
- privacy;
- data retention;
- UX;
- auditability;
- governance.
The regulation describes the destination.
Engineering has to figure out the road.
The form is probably the least interesting part
This:
<textarea></textarea>
<button>Submit report</button>
is easy.
The hard questions come afterward.
Who receives the report?
Who must never receive it?
Who audits access?
How do you communicate with someone you can't identify?
What should you log?
What should you intentionally not log?
How do you handle conflicts of interest?
And how do you explain all of this to the user well enough that they trust the system?
That's when something that originally looked like "an HR form" becomes a much more interesting engineering problem.
I'm particularly curious about one part of this:
If access to a record depends on whether the current user is mentioned inside that record, where would you enforce that rule?
Application layer?
Database policies?
A dedicated authorization layer?
Something else?
A note on this article
I work as CTO and co-founder at Sigilo, where we build privacy-first reporting and organizational feedback systems.
The examples and technical decisions discussed here come from problems we've encountered while designing these workflows.
I used AI assistance to help structure and refine the English version of this article. The technical reasoning, product experience, and final review are my own.
Top comments (0)