Every large organization says the same thing:
"We want feedback from the team."
And then what happens?
Ideas get lost in email threads
"We should fix this..." messages disappear in Teams chats
Nobody knows what's approved, what's in progress, and what died in a meeting
That was the reality.
As a Senior Manager leading QA/Engineering, I was seeing the same pattern:
Smart people sharing good ideas
No single place to capture them
No workflow to review them
No visibility into decisions
So I stopped complaining about the process and built a product.
That product became Feedback Box (Voxa) — an internal platform for capturing, triaging, voting on, and approving ideas with auditability, governance, and clarity.
This is how it went from "we should have a form" to a fully structured internal product.
Starting Point: A Messy, Informal Feedback Culture
Here's the thing: the problem was not "we don't get feedback".
We had too much feedback, in the wrong format, through the wrong channels.
Patterns I saw:
Ideas buried inside meeting notes
"Quick asks" lost in Teams
No distinction between genuine product ideas, one-off complaints, and big structural improvements
And because nothing was structured:
No consistent intake
No system of record
No way to track lifecycle
No way to show leadership a clean pipeline
So I defined the core question:
"What would it look like if feedback flowed through the organization like a proper product backlog, with ownership, SLAs, and data?"
That question became the starting spec.
Turning Vague Complaints Into a Product Concept
Before I wrote a single line of code, I treated this like a real product.
2.1. Mapping the real actors
I identified the actual roles in the system, not just job titles:
Contributor
anyone who can submit feedback
Reviewer / Screener
validates: is this real, actionable feedback?
Recommender
senior leaders whose endorsement matters
Approver
people who can say "we are doing this"
Administrator
manages roles, titles, departments, and access
Then I mapped their questions:
Where do I submit an idea?
What happened to my suggestion?
Who approved or rejected this?
Why was it rejected?
Which ideas are actually in progress?
If a feature didn't answer one of these questions, it was noise.
2.2. Non-negotiable constraints
From there, I set some hard constraints:
No anonymous chaos
anonymous supported initially, but system must work if turned off
Role-based access
no magic admin emails in env vars; everything as data
Single source of truth
one DB, one feedback record, full history
Auditable decisions
every approval, rejection, recommendation needs traceability
SSO-ready
architecture ready for Microsoft SSO from day one
From that, the product snapped into shape:
Key Insight:
Voxa is not a form. It's a governed pipeline for feedback.
System Architecture at a Glance
I designed the system like something I would be happy to defend in an architecture review.
3.1. High-level view
Frontend
Next.js (App Router) + TypeScript + Tailwind CSS + Headless UI
Backend
Node.js + Express + Sequelize
Database
PostgreSQL (Supabase for dev → internal/Postgres for prod)
Auth
Email/password with future SSO hook points
Hosting
Internal Windows Server, reverse proxy (clean URL)
Nodemailer with strongly typed templates
Storage
Cloudflare Images / Azure Blob for profile photos
The important part: I separated concerns as if this were a product we might one day externalize.
3.2. Backend layering
The backend follows a simple but strict layering:
Routes / Controllers
HTTP layer, validation, and response shaping
Services
business logic, workflows, role/permission checks
Repositories (Sequelize models)
handle persistence and queries
Shared utilities
error handling, JWT, email, logging
No business logic in controllers.
No raw SQL in random files.
Everything that matters to the business lives in services, which is where the "Director-level" decisions are embedded:
Who can approve?
Who can recommend?
What happens when feedback is rejected?
Who gets notified?
Designing Roles, Permissions, and Workflows People Actually Follow
Most internal tools die because the permission model is bolted on later.
I started there.
4.1. Explicit role and permission model
Instead of hardcoding checks like:
…I designed a proper RBAC (Role-Based Access Control) system:
RRoles
PPermissions (examples)
VIEW_ALL_FEEDBACKSUBMIT_FEEDBACKADD_VOTEADD_COMMENTEDIT_OWN_COMMENTDELETE_OWN_COMMENTDELETE_OTHERS_COMMENTMANAGE_USERSASSIGN_ROLESRECOMMEND_FEEDBACKAPPROVE_FEEDBACKAnd then the glue tables:
User ↔ UserRole ↔ Role
Role ↔ RolePermission ↔ Permission
This allows:
Multiple roles per user
Easy permission toggling via UI (SuperUser page)
Frontend to render features based on permission strings
Once this was in place, the UI could simply ask:
No magic. No duplication.
Director lens:
This is not just "auth". It's governance. It lets leadership decide how strict the system should be without asking for code changes.
4.2. Feedback lifecycle workflow
Then I designed the lifecycle:
Submitted
User fills the feedback drawer, chooses priority, privacy, and submits
System stores it as status = 'pending'
Screening / Moderation
Reviewers see all pending feedback in a dedicated Admin view
They can Accept, Reject, or Request Clarification (future)
On reject, a reason is mandatory and emailed back to the submitter
Recommendation
For accepted feedback, users with RECOMMEND_FEEDBACK permission can mark: Recommended or Not recommended
The UI shows a visual recommender list with icons and state
Approval
Approvers see a Feedback Approval dashboard that only shows items: accepted with all required recommendations
Approve → status = 'approved' or 'completed' (configurable)
Completion / Archive
Once implemented or decided, feedback moves to Completed
The main dashboard switches between Active Feedback and Completed Feedback
The result is an opinionated, transparent pipeline.
Frontend Decisions: Why Drawers, Not Popups
The frontend was not an afterthought. I designed it very intentionally.
5.1. Why Tailwind and Headless UI
I chose:
Next.js App Router
modern routing, server components where appropriate, and good DX
Tailwind CSS
consistent design language, easy to mirror complex mockups, and fast iteration
Headless UI
accessible primitives for dialogs, drawers, and dropdowns without fighting CSS
This allowed me to:
Build pixel-perfect layouts
Keep behavior and styling in sync
Move fast while keeping a clean component structure
5.2. The feedback drawer pattern
I didn't want a tiny modal in the middle of the screen.
I designed a slide-in drawer for creating and viewing feedback:
Full-height on desktop, mobile-optimized
Purple header with title and close
Single-column form
Priority dropdown
Privacy toggle
Large description area
Reasons for drawer over popup:
People often paste long, thoughtful feedback
They might reference other apps while typing
The drawer feels like a "workspace", not a pop quiz
Dismiss behavior is clear and deliberate
The drawer also includes:
Real-time validation
Cancel handling logic for unsaved changes
Success modal on submit
5.3. Consistent visual language across pages
I kept one consistent visual design system:
Registration form (single-column, modern layout)
Dashboard with profile card + feedback list
Admin tables (Pending Users, User Management, Titles, Permissions)
Drawers for: Viewing feedback, Approving feedback, Approving users, Editing users
Everything follows the same principles:
Clear typography
Generous spacing
Obvious actions
No "clever" UI that confuses people
Backend Structure: Tables, Relationships, Approvals, Notifications
Now the fun part.
6.1. Core entities
At the heart of the system:
User
name, email, password hash
titleId, departmentId, managerId
profilePhotoUrl
approved flag
resetToken / resetTokenExpiry
Role / Permission
As described above, plus join tables
Feedback
title, description, priority
privacy (public / private)
status (pending, accepted, approved, completed, rejected)
submittedById, timestamps
FeedbackComment
feedbackId, userId, content
soft-delete flags or hard delete based on policy
FeedbackVote
feedbackId, userId
unique constraint to prevent double-voting
FeedbackRecommendation
feedbackId, recommenderId
status (recommended, not_recommended)
timestamp
AuditLog
entityType, entityId, action
actorId, payload snapshot
(planned / partially implemented)
Title / Department
Simple reference tables
surfaced in UI for registration, admin management, analytics
6.2. Approval and recommendation logic in code
The service layer encodes the rules:
Only users with RECOMMEND_FEEDBACK can create/update recommendations
Only users with APPROVE_FEEDBACK can approve
A feedback item only appears on the Approval page if status is accepted and all required recommenders have acted
Rejection requires:
Mandatory reason
Email to submitter with original title, reason, and next steps
6.3. Email notification system
I set up a simple but structured email layer:
Nodemailer transport
configured via env variables
Central emailTemplates.js
new feedback submitted, feedback accepted, feedback rejected, new comment/vote, feedback approved
Parameterized templates
{{userName}}, {{feedbackTitle}}, {{feedbackLink}}, {{reason}}
How QA Thinking Shaped Every Decision
Feedback Box wasn't just "built". It was engineered like a system that would be audited.
7.1. Data integrity by design
Examples:
Unique constraints
One vote per user per feedback
One active reset token per user
Foreign keys
You can't have a feedback assigned to a non-existent user
Status transitions
You can't approve feedback that was never accepted
You can't recommend feedback that doesn't exist
By encoding this in the DB and service layer, we reduce:
"Impossible" UI states
Manual clean-up
Data drift between environments
7.2. Edge-case-driven UI
I designed and coded for actual failure modes:
User tries to log in but is not approved
No ambiguous error. A styled modal tells them: You're registered, You're pending approval, What to expect next
User without ADD_VOTE permission
Vote button simply doesn't render. No "you are not authorized" toast on click; just a clean experience
Comments
"Edit" only appears if you have EDIT_OWN_COMMENT or DELETE_OTHERS_COMMENT when appropriate. Delete action opens a confirmation modal. Backend still enforces rules
7.3. Testability
Because of my QA background, I made sure the system is easy to test:
Clear, consistent API shapes
Predictable status codes and error formats
Simple headers for auth (JWT)
Deterministic role/permission behavior
The whole system is designed so that:
You can automate end-to-end flows
You can regression test role behavior
You can simulate edge cases without hacks
Registration, Security, and Identity
This part was critical, because the platform deals with internal people and sometimes sensitive commentary.
8.1. Controlled registration
Registration captures:
Full name
Title
Department
Manager (with avatar)
Profile photo upload (Cloudflare Images)
Password (hashed, never stored in plain text)
Once submitted:
User record created with approved = false
Admins with MANAGE_USERS permission see them under Pending Users
Approval happens via a UserApprovalDrawer, where roles can be assigned
8.2. Login and pending state
On login:
Credentials validated
If user approved: JWT generated and stored → Redirect to dashboard
If not approved: No dashboard access → Clear error via modal dialog
This makes the approval gate a first-class citizen, not a side flag.
8.3. Forgot / Reset password
I implemented:
POST /forgot-passwordGenerates secure resetToken
Sets resetTokenExpiry
Emails the user a time-bound link
POST /reset-passwordValidates token
Checks expiry
Updates password and clears token
This protects:
Users who forget credentials
System from endless admin manual resets
Security standards for internal tooling
Admin UX: Managing a Living System, Not a Static App
Admins often get the worst UI. I went the opposite direction.
9.1. Pending Users and User Management
Two admin views:
Pending Users
Table of users waiting for approval
"Approve" opens a drawer: Shows name, title, email, manager / Lets admin assign one or more roles
Primary action: Approve & Assign Roles
User Management
All approved users
Shows: Name, Email, Title, Manager, Roles (chips)
"Edit" opens an EditUserDrawer: Update profile fields / Update roles / Change title/department from dropdowns
9.2. Titles and Permissions as first-class objects
I created simple but powerful admin pages:
Title Management
Table with: Title name, Total users per title
Inline editing
Add Title button
Delete with confirmation
Permissions Overview
Cards for each permission: Icon, Name, Friendly description
This does two things:
Makes the permission model understandable to non-technical leaders
Makes it realistic to evolve the system over time without rewriting code
From "Tool" to "Internal Product"
Feedback Box (Voxa) is not just a coded solution. It's an internal product with:
Clear value
One place for feedback
Transparent lifecycle
Clear roles and ownership
Governance
Role and permission model
Approval and recommendation workflows
Auditable decisions
Technical discipline
Clean architecture
Strong DB design
Secure auth and password flow
Consistent UI patterns
Scaling path
SSO-ready
Possible to extend to multiple business units
Possible to evolve into a broader Ideas / Improvements Platform
Core Insight:
If you strip away the UI, Feedback Box is a governance engine: who can say what, who can decide what, and how those decisions are recorded, communicated, and acted on.
Final Thoughts
On paper, Feedback Box is "just" an internal feedback system.
In reality, it's a case study in:
Turning messy, informal behavior into a structured product
Baking roles, permissions, and workflows into the core model
Applying QA thinking to architecture, not just testing
Designing UI that respects people's time and mental load
This is the kind of work I enjoy most:
Sit in the middle of business, engineering, and operations
See the mess clearly
Design the system around it
Build it end-to-end - from DB schema to microcopy in the drawer header
That's what Feedback Box (Voxa) represents for me: not a side project, but a concrete example of how I think as an engineering leader.