Skip to content

DB Refactoring - concise titles - #116

Merged
brandonpham13 merged 4 commits into
mainfrom
feat/users-table
Aug 9, 2026
Merged

DB Refactoring - concise titles#116
brandonpham13 merged 4 commits into
mainfrom
feat/users-table

Conversation

@brandonpham13

Copy link
Copy Markdown
Owner

This pull request migrates user profile ownership from Clerk metadata to the app's own users table and API, and updates the client to fetch and manage user profile data from /api/user/me instead of Clerk's unsafeMetadata. It also introduces a new React Query hook for accessing and invalidating the current user's profile, updates documentation to reflect these changes, and improves type naming consistency. The most important changes are summarized below.

User profile ownership and fetching:

  • The app now stores and manages user profile data (email, username, Sleeper link, synced leagues) in its own users table, keyed by Clerk user id. Clerk remains responsible for session management only. The client fetches the profile from /api/user/me and mirrors it into Redux, replacing all previous usage of Clerk's unsafeMetadata. [1] [2] [3]

  • Introduced useCurrentUser and useInvalidateCurrentUser hooks for fetching and invalidating the signed-in user's profile via React Query, replacing the old await user.reload() pattern. [1] [2] [3] [4] [5]

Documentation updates:

  • Updated PLAYBOOK.md and CLAUDE.md to document the new user profile flow, the division of responsibility between Clerk and the app, and the rationale and rules for user and auth data ownership. Added instructions for backfilling the users table from Clerk. [1] [2] [3]

Type and API consistency:

  • Renamed types and API payloads for announcements and awards to remove redundant prefixes (e.g., HuddleAnnouncementAnnouncement, HuddleAwardAward) for consistency with new table naming conventions. [1] [2] [3] [4] [5]

Database and migration guidance:

  • Added detailed documentation on table naming conventions, migration pitfalls, and the importance of keeping user ids stable for future auth provider migrations.

These changes centralize user profile data, simplify future auth migrations, and clarify the boundaries between Clerk and application-owned data.

brandonpham13 and others added 4 commits August 9, 2026 17:51
Clerk was acting as three things at once: the session provider, our user
database, and the store for product data (Sleeper link, synced leagues) in
`unsafeMetadata`. The last two are the parts that would be expensive to get
back if we ever move off it, and the metadata had no Postgres copy at all.

Clerk now owns sessions only.

- Add a `users` table keyed by the *Clerk* user id, not a fresh uuid. All 18
  user-referencing columns across 11 tables already store that string, so
  keeping it as the PK means a future auth swap is "issue our own JWTs with the
  same sub" rather than a renumbering of the whole database.
- Move sleeperUsername / sleeperUserId / syncedLeagueIds out of `unsafeMetadata`
  into that table. Nothing writes to Clerk metadata anymore.
- Add FKs on all 18 columns, `ON DELETE RESTRICT` — league history outlives an
  account, and a missed id in a future migration now errors instead of silently
  orphaning a forum post. Migration 0012 seeds placeholder rows from the data
  itself before adding the constraints, so it can't fail on an unseeded DB.
- Replace `clerkClient.users.getUserList()` — an HTTP call to Clerk on the
  critical path of five request handlers — with a single SQL read.
- Add `GET /api/user/me` as the client's source of truth; AuthGuard blocks on it
  (which is also what guarantees the row exists) instead of reading Clerk
  metadata, and `useInvalidateCurrentUser()` replaces `await user.reload()`.
- Add scripts/backfill-users.mjs: idempotent Clerk -> Postgres mirror, usable
  both as the one-time backfill and as a recurring snapshot.

Verified against the dev DB: both migrations apply, the backfill moved all 5
users' Sleeper links across, and the FKs reject both an orphaned insert and a
delete of a referenced user.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e users

The prefix looked like it meant "scoped to a huddle" but was wrong in both
directions: team_claims and side_bets carried huddle_id without it, while the
seven poll/survey child tables carried it without a huddle_id. ~80% consistent
is worse than no convention — it invites an inference that doesn't hold.

Settles on: no prefix = global, huddle_ = lives inside one huddle, directly or
transitively. That makes `users` (and future auth tables) the meaningful
exception rather than an eighteenth arbitrary case.

- 0013: rename team_claims -> huddle_team_claims, side_bets -> huddle_side_bets
  (real ALTER TABLE ... RENAME, resolved through drizzle-kit's TTY prompt, so
  rows are preserved rather than dropped and recreated).
- 0013: huddle_active_trophies.enabled from integer 1/0 to boolean — the schema
  uses boolean() in 8 other places. Drizzle's bare SET DATA TYPE would have
  failed; hand-edited to DROP DEFAULT / SET DATA TYPE ... USING / SET DEFAULT.
- 0014: rename the leftover primary keys. Postgres keeps constraint names across
  a table rename and Drizzle doesn't track them, so groups_pkey had survived
  since 0001 and 0013 had just created two more of the same. Every PK now
  matches its table.
- Drop the 1/0 translation in trophyControlService; the API already spoke
  boolean, only storage didn't.
- Point backfill-users.mjs at the renamed tables.

Verified on the dev DB: 3 team-claim rows survived the rename, enabled is
boolean defaulting true, all 23 PKs match their table, 18 FKs intact, and the
services reading these tables plus a live trophy toggle all still work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reverses the direction of 692a3f0, which made the prefix consistent when the
right call was to remove it. Every table in this database belongs to Huddle, so
the prefix carried no information — it just made `\dt` and autocomplete on
`hud` return almost the entire schema.

21 tables renamed (huddle_commissioners -> commissioners, huddle_polls ->
polls, ...). `huddles` and `users` are unchanged. team_claims and side_bets end
up back at their original names, so 0013's renames were round-tripped; the rest
of 0013 (the boolean conversion, the groups_pkey fix) still stands.

The structural distinction the prefix pretended to encode isn't in the names and
doesn't need to be: `users` is global, everything else is reachable from a
huddle, directly or through a parent.

Mechanics worth knowing (now in PLAYBOOK):
- Postgres renames the table and nothing else, and Drizzle doesn't track
  constraint or index names, so they'd rot invisibly. 0015 renames 115 objects,
  generated from pg_constraint/pg_index against the live schema rather than by
  hand.
- drizzle-kit can't be driven through 21 "created or renamed?" prompts
  reliably, so it was answered "create" — the snapshot records end state and is
  correct either way — and its destructive DROP/CREATE output replaced.
- 0016 fixes a constraint name that had been silently truncated at Postgres's
  63-byte limit when first created, losing its `_fk` suffix; 0015 carried the
  truncation forward. Caught by diffing the snapshot against pg_constraint,
  which is a stronger check than "no schema changes" from db:generate.

Verified on the dev DB: row counts unchanged across the rename (3 claims,
3 huddles, 3 commissioners, 5 users), no table/index/constraint still carries
the prefix, all 18 FKs to users intact, zero drift across all 77 FK and index
names, and every service that reads these tables plus a live trophy toggle
still works.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follows 6120cd0 through to the TypeScript layer. `HuddleAward` -> `Award`,
`HuddlePoll` -> `Poll`, `HuddleSurveyResponse` -> `SurveyResponse`, and so on
for 21 types across schema.ts, the services and the client.

This mostly brings the server in line with the client rather than the reverse:
client/src/types/huddle.ts had already settled on DuesConfig, ForumTopic, Poll,
PollOption, SideBet and PayoutEntry, with only HuddleAward and HuddleAnnouncement
still carrying the prefix. Both are now unprefixed too.

Kept where `Huddle` is load-bearing rather than decorative: Huddle, HuddleClaim,
HuddleClaimSummary, HuddleDetail, HuddleDetailResponse, HuddleMemberStatus.

One collision: lucide-react exports an `Award` icon that CommissionerPage passes
to StubSection, and `AwardIcon` was already taken by the award-glyph asset type
from useHuddles — so the icon is imported as `LucideAward`.

Server and client both typecheck; services reading every renamed table still
return the same rows (1 claim, 1 commissioner, 2 announcements, 1 award,
5 payouts, 5 trophies) and the API responds as expected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
huddle Ready Ready Preview Aug 9, 2026 11:36pm

@brandonpham13
brandonpham13 merged commit 92153cd into main Aug 9, 2026
3 checks passed
@brandonpham13
brandonpham13 deleted the feat/users-table branch August 9, 2026 23:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant