# Avatar System: Flag Builder + Photo Upload ## Context Users and teams currently show initials in a colored box when no avatar is set. OAuth users get their Google/Discord photo in `users.imageUrl`, but there's no custom upload path and no procedurally generated identity. This plan adds: 1. **Flag avatars** — SVG flags (12 patterns, 2–3 colors) as the default identity for every user and team, editable via a builder UI. 2. **Photo upload** — Cloudinary-hosted images with Rekognition NSFW moderation before display. 3. **Hierarchy** — teams inherit their owner's avatar by default; users and teams can independently override. --- ## Architecture Decisions - **Image hosting**: Cloudinary. Store at 256×256, serve at 45×45 or 18×18 via URL-based transformations (e.g. `w_45,h_45,c_fill`). Transformations are generated once per unique image+size then CDN-cached; cost is ~2 credits/1k new uploads. At ~2 KB average delivered size, the free tier (25 GB bandwidth) covers ~80,000 active users. NSFW moderation via the AWS Rekognition add-on. Images are never shown until the webhook approves them. - **Flag library**: None exists. Build a custom `FlagSvg` component (~300 lines). Pure SVG, no dependencies, SSR-safe. - **DB**: Two new JSONB/varchar columns each on `users` and `teams`; keep existing `imageUrl` / `logoUrl` untouched. - **OAuth protection**: BetterAuth's `updateUser` hook will be patched to skip writing `imageUrl` once a user has set a custom avatar. --- ## DB Schema Changes **`database/schema.ts`** — add to `users`: ```ts flagConfig: jsonb("flag_config"), // {pattern: string, colors: string[]} customAvatarUrl: varchar("custom_avatar_url", { length: 512 }), // Cloudinary URL (post-approval) avatarType: varchar("avatar_type", { length: 20 }).notNull().default("flag"), // avatarType: 'flag' | 'uploaded' ``` Add to `teams`: ```ts flagConfig: jsonb("flag_config"), avatarType: varchar("avatar_type", { length: 20 }).notNull().default("owner"), // avatarType: 'owner' | 'flag' | 'uploaded' // existing logoUrl reused for uploaded team images ``` Run `npm run db:generate` then `npm run db:migrate`. --- ## Avatar Display Hierarchy ``` getUserAvatarData(user): if avatarType === 'uploaded' && customAvatarUrl → {type: 'image', url: customAvatarUrl} if flagConfig → {type: 'flag', config: flagConfig} if imageUrl → {type: 'image', url: imageUrl} // OAuth fallback → {type: 'initials'} getTeamAvatarData(team, owner): if avatarType === 'uploaded' && logoUrl → {type: 'image', url: logoUrl} if avatarType === 'flag' && flagConfig → {type: 'flag', config: flagConfig} → getUserAvatarData(owner) // 'owner' default + final fallback ``` --- ## Phases ### Phase 1 — Flag SVG Engine **New files:** - `app/lib/flag-types.ts` — `FlagPattern` union type, `FlagConfig` interface - `app/components/ui/FlagSvg.tsx` — renders `` from `{pattern, colors, size?}` - `app/lib/flag-generator.ts` — `generateFlagConfig(seed: string): FlagConfig` (deterministic from hash) **12 patterns** (pure SVG geometry, all square viewport): | Pattern | SVG approach | |---|---| | `triband-h` | 3 stacked `` | | `triband-v` | 3 side-by-side `` | | `diagonal` | `` bg + `` triangle | | `nordic-cross` | bg + two crossing `` strips, cross offset left | | `cross` | bg + two centered `` strips | | `canton` | bg + `` overlay top-left quarter | | `triangle` | bg + `` left-pointing triangle | | `chevron` | bg + `` V pointing right | | `quartered` | 4 `` in 2×2 | | `bordered` | bg `` + inset `` with gap | | `circle` | bg + `` center | | `star` | bg + `` 5-point star | `generateFlagConfig` picks pattern + 2-3 colors deterministically using `hashString` from `app/lib/avatar-colors.ts` (reuse existing). Colors drawn from `AVATAR_COLORS` palette + white/black. **Tests:** `app/lib/__tests__/flag-generator.test.ts` --- ### Phase 2 — Schema Migration + Backfill 1. Add columns to schema (`database/schema.ts`) 2. `npm run db:generate` → `npm run db:migrate` 3. Update model functions: - `app/models/user.ts` — add `flagConfig`, `customAvatarUrl`, `avatarType` to select/update types - `app/models/team.ts` — add `flagConfig`, `avatarType` 4. Backfill script: `scripts/backfill-flag-configs.ts` - Queries all users/teams where `flagConfig IS NULL` - Calls `generateFlagConfig(id)` for each - Batch-updates. Run once manually: `npx tsx scripts/backfill-flag-configs.ts` --- ### Phase 3 — Avatar Display Components **Modified files:** - `app/components/TeamAvatar.tsx` — accept `flagConfig?` prop; render `` when present instead of initials - `app/components/UserMenu.tsx` — use `customAvatarUrl ?? flagConfig ?? imageUrl` priority - **New:** `app/components/ui/UserAvatar.tsx` — mirrors `TeamAvatar` for user contexts `TeamAvatar` new props: ```ts interface TeamAvatarProps { teamId: string; teamName: string; logoUrl?: string | null; flagConfig?: FlagConfig | null; avatarType?: 'owner' | 'flag' | 'uploaded'; ownerAvatarData?: AvatarData; // resolved by parent from owner user size?: 'sm' | 'md' | 'lg'; } ``` Add `xl` size (`h-16 w-16`) to both components for the editor preview. --- ### Phase 4 — Flag Builder UI **New files:** - `app/components/ui/FlagBuilder.tsx` — interactive editor - Grid of 12 pattern thumbnails (4-col, using `` at ~60×60px) - Selected pattern highlighted with brackt green border - Color slot pickers: each slot shows the 6 brackt palette swatches + a custom hex `