Social account business placement
The bug
When a user connects a provider (X/Twitter, LinkedIn, Threads, Instagram…), the provider row is created by Better Auth and a matching social_media_accounts row is created automatically from the databaseHooks.account.create.after hook in packages/auth/lib/auth.ts. That hook resolved the owning business with:
const businessFromUser = await this.db.query.businessProfiles.findFirst({
where: eq(businessProfiles.userId, user.id),
});findFirst with no isActive filter always returns the user's first business by insertion order. The UI passes the active business in the OAuth callbackURL (?businessId=...) but nothing consumed it, and the hook has no access to that query. Result: accounts connected while a non-first business was active silently landed on the wrong business and did not show up in the app.
The fix
packages/db/server/services/social-media-account.service.ts:
- New private
resolveBusinessForUser(userId)— the user's active business (isActive = true, newestupdatedAt), else their first business (asc createdAt), else''. createOrUpdateAccountFromAuthnow calls it. Every provider in the auth hook funnels through this one method, so the fix covers all nine (threads, instagram, twitter, linkedin, youtube, discord, reddit, tiktok, dribbble).
This mirrors the UI's own active-business resolution (/api/v1/business/active → businessProfileService.getActive), so a connected account appears where the user is looking.
Related hardening (same change)
createOrUpdateAccount used to look up the existing row by accountId alone:
const account = await this.getAccountByAccountId(id); // no user/platform scopingIf the same platform account id already existed for another user, the code found that row and overwrote its tokens/name — a cross-tenant write. It now scopes the lookup by (userId, platform, accountId).
Gotchas
is_activeis reliable for owners (setActiveflips the target totrueand all others tofalse); members never get theirs flipped, which is why the active pick orders byupdatedAtfor determinism.- Reconnect does not re-home an existing row:
updateAccountnever writesbusinessId. Moving an already-mis-filed account requires disconnect → reconnect. - The
account.create.afterhook only fires on insert. Disconnecting only the social account (not the Better Authaccountrow) means a later reconnect never recreates it. Tracked as a follow-up.
Verify
- With business B active, connect a provider → row's
business_id = B. - With no active business, connect → first business (by
createdAt). - Test one native (reddit) and one generic-OAuth (threads) provider — same path.