Team invites shipped — and the database trigger that almost killed them
The plan for Day 44 was a /faq page and a small toast fix for the GigaChad Invite button. It became 11 commits, 2 database migrations, and one of the heaviest days of the project. Here's what happened.
The toast that became a feature
Day 44 started simple. The Invite button on the Team card showed a window.alert() — "Team invites coming soon." That looked broken to me. A real GigaChad customer paying $99/month clicks Invite and gets a native browser dialog? No.
My first instinct was to install a toast library. Better-looking message, same behavior. But that's the cofounder voice in my head: if we're not building the real thing now, when?
So we built the real thing. Backend, frontend, database, email — full team invite flow on the GigaChad tier. Up to 3 seats per business, invite-by-email, accept via link, member dashboard access, owner-only controls on billing and team management.
What actually shipped
- New
team_invitesPostgres table with RLS (owners-only, plus service role for token-based acceptance) - Four API routes: invite, accept, revoke, remove-member
- Resend-powered invite emails with 1-day token expiry
/accept-invitepage with full unauthenticated and authenticated state handling- SettingsClient Team card rebuilt from a
[1,2].mapstub into a real member list with pending invites, revoke buttons, remove buttons, and an invite form - Member permission lockdown — owner-only API gates on the Stripe billing endpoints, hidden UI controls for members, a gold banner explaining the permission level
The trigger that broke everything
The first end-to-end test failed in the most confusing way. The invitee clicked the email link, signed up, verified their email — and immediately got "You already belong to a business."
They had just signed up. How did they already belong to a business?
I looked at the database. A new business existed, created two milliseconds before the user row. Two milliseconds before.
That's not application code. That's a trigger.
And there it was: an on_auth_user_created trigger from way back in the project, running handle_new_user() automatically on every signup. It inserted a fresh business and made the new user its owner. For regular signups, that's exactly what onboarding needs. For invited signups, it stole the user away before the invite-accept endpoint ever ran.
The fix was small once I knew where to look. Update the trigger function to check team_invites for a pending, unexpired invite matching the new user's email. If one exists, skip business creation entirely and let the invite-accept endpoint handle membership. Normal signups unchanged.
What I learned
Three things, written for the next person who hits this:
Auth triggers are invisible until they break. Nothing in the application code mentions them. Nothing in the migration history hints at how foundational they are. They run before your application logic, and they can quietly invalidate every assumption your code makes about what state a new user is in. If you're debugging "the database state doesn't match the code path," check pg_trigger.
The obvious feature is the right one to build. The toast would have shipped in 30 minutes and felt like progress. The actual invite flow took six hours and felt like a swamp. But one of those is a real product feature. The other is a coat of paint on a missing wall.
Member permissions are not a polish item. Once we had a working invite flow, members could hit the Stripe checkout endpoint and change the owner's billing. Two API routes were silently unlocked. That kind of hole exists in every multi-user app that bolts on team features after the fact — and the only way to find them is to actually look.
What's next
Day 45 starts with the /vs/birdeye and /vs/podium competitor comparison pages — the original Day 45 main task per the August 10 launch roadmap.
The team invite flow is now live for any GigaChad subscriber. If you're on the GigaChad tier, the Team card in Settings is real. Invite a teammate, watch them accept, watch them get scoped to your business with the same review access you have.
Frequently asked questions are now at /faq — 27 of them. We also fixed three broken nav links across the header and footer that used to point at a non-existent /#faq anchor.
11 commits. 2 migrations. One stubborn trigger. Welcome to Day 44.
Tagged
Written by
The founder of Ominvo
Building review management for single-location small businesses. Join the waitlist →