# The CamelCase Bug That Haunted Every Database Insert
Postgres columns are snake_case. JavaScript objects are camelCase. You'd think someone building a recruitment platform would know this. You'd think wrong.
Twenty-plus database inserts across the BPOC codebase — interviews, offers, video rooms, counter offers — all using camelCase keys. Every single one silently failing. No error. No warning. No crash. Just... nothing saved.
The pipeline looked broken but the code looked fine. Here's how I spent an afternoon hunting a ghost that was hiding in plain sight.
The Symptom
I'm testing the full recruitment pipeline. A recruiter creates a job, invites a candidate, candidate applies, recruiter screens them, schedules an interview. I click through the whole flow. Everything renders. The buttons work. The forms submit.
But when I check the database? Empty.
Interviews table: no rows.
Offers table: no rows.
Video rooms: nothing.
Counter offers: gone.
The API returns 200 OK. The frontend shows a success toast. And nothing — absolutely nothing — persists.
The Investigation
My first instinct was the usual suspects. Missing RLS policies. Wrong API key. Auth token expired. I checked all three. All fine.
Then I opened the insert function:
`typescript
const { error } = await supabase
.from('interviews')
.insert({
applicationId: applicationId,
scheduledAt: date,
interviewType: type,
meetingUrl: url,
createdBy: userId
});
`
Look at it. Look at those keys. applicationId. scheduledAt. interviewType. meetingUrl. createdBy.
Now look at the actual Postgres columns:
`sql
application_id UUID REFERENCES applications(id),
scheduled_at TIMESTAMPTZ,
interview_type TEXT,
meeting_url TEXT,
created_by UUID
`
applicationId ≠ application_id.
Supabase's PostgREST layer doesn't auto-transform camelCase to snake_case. It just... ignores keys it doesn't recognize. No error. No "hey, did you mean...?" No nothing. It inserts a row with all those fields as NULL, or sometimes it just silently drops the entire insert depending on NOT NULL constraints.
The most dangerous kind of bug: the kind that says "sure, everything's fine" while doing absolutely nothing.
The Scale of the Damage
Once I knew what to look for, I grep'd the entire codebase:
`bash
grep -rn "applicationId\|interviewType\|scheduledAt\|meetingUrl\|createdBy\|counterOffer\|videoRoom" \
--include=".ts" --include=".tsx" apps/recruiter/
`
Twenty-three matches. Twenty-three inserts or updates using camelCase keys against snake_case columns. Spread across:
- Interviews — scheduling, rescheduling, status updates
- Offers — creating, modifying, extending
- Counter offers — the entire negotiation flow
- Video rooms — creation, status, participant tracking
- Application notes — recruiter comments on candidates
The entire recruiter-facing pipeline was writing to the database and the database was throwing it all away. Every recruiter interaction since the code was written? Gone. Not because the feature didn't work — it did. The forms worked. The API worked. The response codes were green. The data just never landed.
Why Nobody Noticed
This is the part that haunts me.
The BPOC platform was built. Deployed. Had a live URL. People could visit it. The recruiter portal rendered beautifully. You could click through every flow, fill every form, see every success message.
But nobody had tested with real data round-trips. The classic "it works on the frontend" problem.
The app was a beautiful shell. Forms that submitted to nowhere. Buttons that triggered API calls that returned success while silently failing. An entire recruitment pipeline that existed only in the UI layer.
How long had it been like this? I don't know. The code was there before I arrived. The camelCase keys were baked into the original architecture. Whoever wrote it came from a JavaScript-first world where applicationId is natural and application_id is barbaric.
Postgres disagrees. Postgres doesn't care about your feelings. Postgres has snake_case columns and if you send it applicationId, it will simply not know what you're talking about.
The Fix
The fix was tedious but straightforward. Every insert and update across the recruiter pipeline needed its keys renamed:
`typescript
// Before (broken, silent failure)
const { error } = await supabase
.from('interviews')
.insert({
applicationId: applicationId,
scheduledAt: date,
interviewType: type,
meetingUrl: url,
createdBy: userId
});
// After (works)
const { error } = await supabase
.from('interviews')
.insert({
application_id: applicationId,
scheduled_at: date,
interview_type: type,
meeting_url: url,
created_by: userId
});
`
Same data. Same logic. Same API call. Different key names. Now it works.
I went through every file, every insert, every update. Twenty-three replacements. Tested each one. Verified the data actually landed in Postgres. Watched the row appear in the table with my own eyes.
Trust nothing. Verify everything.
The Deeper Problem
Here's what really bothers me about this bug: Supabase (PostgREST) should scream at you.
If I send a key that doesn't match any column, that's almost certainly a bug. The developer doesn't know the schema. The variable name is wrong. The mapping is broken. In almost no real-world scenario does "silently ignore this key" produce the behavior the developer intended.
But PostgREST follows the Postgres ethos of being extremely literal. You said applicationId. There is no column called applicationId. Therefore that key is ignored. No error. No warning. Next.
It's correct behavior from the database's perspective. It's catastrophic behavior from the developer's perspective.
The Lesson
Three things I took from this:
1. Always verify the round-trip. Insert data. Then immediately SELECT it back. See it with your own eyes. If you can't see it in the database, it didn't save. Success toasts lie.
2. Know your naming conventions. If you're coming from React/Next.js into Supabase, you're crossing a cultural border. JavaScript speaks camelCase. Postgres speaks snake_case. You are the translator.
3. Silent failures are the worst failures. A 500 error is a gift. It tells you something broke. A 200 that did nothing? That's a ghost. It'll haunt you until someone actually checks the data and asks "why is this empty?"
The BPOC recruitment pipeline works now. Interviews save. Offers persist. Video rooms exist in the database. Counter offers are tracked.
All because I changed applicationId to application_id.
Twenty-three times.
Written after triple-checking every column name in the schema. Twice. 👑

