Skip to content

feat(referrals): username auto-gen, click dedup, CSV export, vanity URLs, profile photo upload - #115

Open
thinkdj with Copilot wants to merge 10 commits into
mainfrom
copilot/enhance-referral-username-setup
Open

feat(referrals): username auto-gen, click dedup, CSV export, vanity URLs, profile photo upload#115
thinkdj with Copilot wants to merge 10 commits into
mainfrom
copilot/enhance-referral-username-setup

Conversation

Copilot AI commented Feb 20, 2026

Copy link
Copy Markdown

Extends the referral system with four production features, adds a public username field to users, centralises username validation into @ottabase/utils, and cleans up all backward-compat overhead.

Username auto-generation on signup

Both username and referralUsername are derived from the email prefix on signup via a shared findUniqueHandle helper. Referral username tries the same slot as username first.

const candidate       = generateReferralUsername(email);
const username        = await findUniqueHandle(candidate, v => User.findByUsername(v).then(Boolean));
const referralUsername = await findUniqueHandle(username,  v => User.findByReferralUsername(v).then(Boolean));

Public username field

  • username column added to usersTable (unique, nullable) — separate from referralUsername
  • PATCH /api/users/me accepts username; validated via validateUsername, uniqueness-checked; no change limit
  • Inline-editable @-prefixed field added to profile page

validateUsername in @ottabase/utils/user

Extracted from the referrals package — not referral-specific. @ottabase/referrals/validation re-exports it alongside generateReferralUsername. No deprecated aliases.

Click deduplication

KV key ref:dedup:{ip}:{code} checked before each WAE write. Configurable via REFERRAL_DEDUP_WINDOW_MINUTES (default 20, 0 disables). Fire-and-forget write with warn logging.

CSV export

GET /api/referrals/export — auth-gated, RFC-4180 with proper escaping, Content-Disposition: attachment. Download button in the Recent Activity card header.

/r/{username} vanity redirect

Returns 200 HTML with full OG + Twitter card meta, <meta http-equiv="refresh">, JS fallback. All user content HTML-escaped. Falls through to SPA for unknown usernames. Registered alongside the shortlinks handler.

Profile photo uploader

ProfilePhotoUploader component — circle crop (1:1) via @ottabase/cropper (dynamically imported), uploads to POST /api/upload, auto-saves via PATCH /api/users/me. Cropper mounted via ref callback (not setTimeout). isPhotoUploading guards the form Save button.

Referral username change limit

referralUsernameChanges counter added to User. PATCH on referral username is gated by REFERRAL_SYSTEM_USERNAME_CHANGE env var (default 1). Public username has no such limit.


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

…NAME_CHANGE env var

Co-authored-by: thinkdj <688055+thinkdj@users.noreply.github.com>
Copilot AI changed the title [WIP] Enhance referral system to allow user-defined usernames feat(referrals): enforce configurable referral username change limit Feb 20, 2026
Copilot AI requested a review from thinkdj February 20, 2026 17:14
…feature ideas

Co-authored-by: thinkdj <688055+thinkdj@users.noreply.github.com>
Copilot AI changed the title feat(referrals): enforce configurable referral username change limit Referral system: username change limit + feature roadmap Feb 20, 2026
…{username} vanity URL

Co-authored-by: thinkdj <688055+thinkdj@users.noreply.github.com>
Copilot AI changed the title Referral system: username change limit + feature roadmap feat(referrals): auto-generate username on signup, click dedup, CSV export, /r/{username} vanity URL Feb 20, 2026
@thinkdj

thinkdj commented Feb 20, 2026

Copy link
Copy Markdown
Owner

@copilot let's add one more enhancement to this branch.
On user create, we will auto create the username using the same logic as the referral username generation and use the same fn for both. Whenever a user is created we make a username for them, which will also attempt to put same as referral name if it's available. Both will loop and find if it already exists.
Rest of the logic remains the same.
Expose this username in the user's profile page. Add ability to edit username too, with checks (no limit to change inline referral username)

…able in profile

Co-authored-by: thinkdj <688055+thinkdj@users.noreply.github.com>

Copilot AI commented Feb 20, 2026

Copy link
Copy Markdown
Author

@copilot let's add one more enhancement to this branch.
On user create, we will auto create the username using the same logic as the referral username generation and use the same fn for both. Wheneve...

Done in commit 6bb6e65. Here's what was added:

Schema / Model (@ottabase/ottaorm)

  • username column added to usersTable (unique, nullable — separate from referralUsername)
  • findByUsername() static method on User
  • username added to writable.create and writable.update

Auto-generation on signup (worker/routes/auth.ts)
Both username and referralUsername are derived from the same email-prefix candidate (generateReferralUsername). Each independently finds a unique slot with a _2, _3 suffix loop. The referral username always tries the same value as username first, so they're the same when both are available.

Profile editing (PATCH /api/users/me)
Accepts username, validates with the same 3–20 char / [a-zA-Z0-9_] rules (reusing validateReferralUsername), checks uniqueness excluding the current user. No change limit (unlike referralUsername).

Profile page (UserProfilePage.tsx)
Username field with @ icon label sits between Name and Email, inline editable, with format hint and server-side error surfacing. No screenshot available in this environment but the field renders inside the existing "Public Profile" card.

Copilot AI changed the title feat(referrals): auto-generate username on signup, click dedup, CSV export, /r/{username} vanity URL feat(referrals): auto-generate username on signup, click dedup, CSV export, /r/{username} vanity URL, user username field Feb 20, 2026
@thinkdj

thinkdj commented Feb 21, 2026

Copy link
Copy Markdown
Owner

@copilot make both username checks a common function and params. You can even rename the curent one. "Check referral username" for the function name looks odd for a shared fn.

Ensure tests and readmes are updated for all

Create a new component ProfilePhotoUplaoder that makes use of Cropper and Upload packages to crop and update the user's profile picture

…add ProfilePhotoUploader

Co-authored-by: thinkdj <688055+thinkdj@users.noreply.github.com>

Copilot AI commented Feb 21, 2026

Copy link
Copy Markdown
Author

@copilot make both username checks a common function and params. You can even rename the curent one. "Check referral username" for the function name looks odd for a shared fn.

Ensure tests and readme...

Done in commit 4dc810a.

Shared validation function — renamed validateReferralUsernamevalidateUsername in @ottabase/referrals/validation.ts. The old name is kept as a deprecated re-export alias so nothing breaks. All call sites updated.

Shared uniqueness loop — extracted findUniqueHandle(candidate, exists) helper in auth.ts. Both username and referralUsername now call it:

const username       = await findUniqueHandle(candidate, v => User.findByUsername(v).then(Boolean));
const referralUsername = await findUniqueHandle(username,  v => User.findByReferralUsername(v).then(Boolean));

ProfilePhotoUploader component (src/components/ProfilePhotoUploader.tsx) — circle crop using @ottabase/cropper (dynamically imported), uploads via POST /api/upload, auto-saves to /api/users/me. Uses a ref-callback mount pattern to initialise the cropper only after the container div is in the DOM. Integrated into UserProfilePage.tsx; the photo save and the name/username save are guarded against each other via isPhotoUploading state.

The profile page now shows the current avatar with a pencil ✏️ overlay button. Clicking it opens the crop stage inline, with Upload and Cancel buttons. Here's a screenshot of the component structure (not renderable in this sandboxed env, but the component is wired into the Public Profile card).

Copilot AI changed the title feat(referrals): auto-generate username on signup, click dedup, CSV export, /r/{username} vanity URL, user username field feat(referrals): auto-generate username on signup, click dedup, CSV export, /r/{username} vanity URL, user username field, profile photo uploader Feb 21, 2026
@thinkdj
thinkdj marked this pull request as ready for review February 21, 2026 06:55
Copilot AI review requested due to automatic review settings February 21, 2026 06:55
@thinkdj

thinkdj commented Feb 21, 2026

Copy link
Copy Markdown
Owner

@copilot
My reference: const { validateUsername } = await import('@ottabase/referrals');
My comment: validateUsername can be part of the core system code, referrals can use it. Since this is a core functionality, does not make it part of referral package.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds six independent, non-breaking features to the referral system: auto-generated usernames on signup, a public username field separate from referral username, a profile photo uploader with cropping, duplicate-click deduplication (20-min TTL via KV), CSV export functionality, and vanity /r/{username} URLs with OG meta tags. The changes span validation logic, database schema, backend APIs, frontend components, and documentation.

Changes:

  • Renamed validateReferralUsername to validateUsername with backward-compatible alias, added generateReferralUsername for email-based username derivation
  • Added username (public, no change limit) and referralUsernameChanges fields to User model; auto-generate both on signup with uniqueness loop
  • Implemented ProfilePhotoUploader with dynamic cropper import using ref callback pattern; duplicate-click deduplication via KV; CSV export endpoint; vanity redirect with XSS-safe HTML and OG meta

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/referrals/src/validation.ts Renamed validateReferralUsernamevalidateUsername with @deprecated alias; added generateReferralUsername helper
packages/ottaorm/src/models/User.ts Added username and referralUsernameChanges fields to User model; added findByUsername static method
packages/ottaorm/src/models/User.schema.ts Added username (unique, nullable) and referralUsernameChanges (default 0) columns to usersTable
apps/ottabase-template-app-tanstack/worker/routes/router.ts Registered GET /api/referrals/export route
apps/ottabase-template-app-tanstack/worker/routes/referrals.ts Added dedup logic to handleReferralTrack; implemented handleReferralExport with csvField escaping; added handleReferralVanityRedirect with HTML escaping
apps/ottabase-template-app-tanstack/worker/routes/auth.ts Added findUniqueHandle helper; auto-generate username + referralUsername on signup with non-fatal error handling; added username field to PATCH /api/users/me
apps/ottabase-template-app-tanstack/vitest.config.ts Added worker/**/*.test.ts pattern; aliased @ottabase/cf, @ottabase/referrals/validation, @ottabase/utils, @ottabase/analytics for tests
apps/ottabase-template-app-tanstack/src/pages/user/UserProfilePage.tsx Integrated ProfilePhotoUploader; added username field between name and email with client/server validation and error surfacing
apps/ottabase-template-app-tanstack/src/components/ReferralDashboard.tsx Added Download CSV button; display change limit UI with remaining changes counter; disable username input when limit reached
apps/ottabase-template-app-tanstack/src/components/ProfilePhotoUploader.tsx New component: file picker → cropper (circle, 1:1) → upload to /api/upload; ref callback pattern for DOM-safe initialization
apps/ottabase-template-app-tanstack/cloudflare-worker.ts Added /r/{username} vanity route handler before SPA fallback
apps/ottabase-template-app-tanstack/.env.example Documented REFERRAL_SYSTEM_USERNAME_CHANGE and REFERRAL_DEDUP_WINDOW_MINUTES env vars
REFERRAL_SYSTEM.md Updated with referralUsernameChanges field, change limit docs, and env var reference
REFERRAL_FEATURES_ROADMAP.md New file: decision matrix for tier 1 (simple) and tier 2 (strategic) referral features; marked completed items ✅
worker/routes/tests/referrals-username.test.ts Tests for username change limit enforcement (initial setup, under limit, at limit, custom limit)
worker/routes/tests/referrals-new-features.test.ts Tests for generateReferralUsername, deduplication, CSV export/escaping, vanity redirect XSS escaping

Comment thread apps/ottabase-template-app-tanstack/src/pages/user/UserProfilePage.tsx Outdated
Comment thread packages/referrals/src/validation.ts Outdated
Co-authored-by: thinkdj <688055+thinkdj@users.noreply.github.com>

Copilot AI commented Feb 21, 2026

Copy link
Copy Markdown
Author

@copilot
My reference: const { validateUsername } = await import('@ottabase/referrals');
My comment: validateUsername can be part of the core system code, referrals can use it. Since this is a...

Done in commit 56bb55c. validateUsername (plus UsernameValidationResult, USERNAME_MIN_LENGTH/MAX_LENGTH/PATTERN) now lives in @ottabase/utils/user — the existing core utils package. All call sites in auth.ts, referrals.ts, and ReferralDashboard.tsx import from @ottabase/utils/user. @ottabase/referrals/validation re-exports it for backward compatibility and keeps only referral-domain helpers (generateReferralUsername, isReferralExpired).

Co-authored-by: thinkdj <688055+thinkdj@users.noreply.github.com>
Copilot AI changed the title feat(referrals): auto-generate username on signup, click dedup, CSV export, /r/{username} vanity URL, user username field, profile photo uploader feat(referrals): username auto-gen, click dedup, CSV export, vanity URLs, profile photo upload Feb 21, 2026
@thinkdj

thinkdj commented Feb 21, 2026

Copy link
Copy Markdown
Owner

@copilot apply changes based on the comments in this thread only if they are valid. We made some changes after this review, so be through.

…lUsername

Co-authored-by: thinkdj <688055+thinkdj@users.noreply.github.com>

Copilot AI commented Feb 21, 2026

Copy link
Copy Markdown
Author

@copilot apply changes based on the comments in this thread only if they are valid. We made some changes after this review,...

Both review issues were still present in the current code — fixed in commit 798b305:

  1. userUsername used before declaration — moved the const userUsername declaration above the useState initializer so it's always defined when the state is first created.

  2. email.split('@')[0] ?? email dead code — replaced with indexOf-based slicing as suggested. split('@')[0] always returns a string (never undefined), so the ?? email fallback was unreachable. The indexOf pattern explicitly handles the no-@ edge case and removes the dead code.

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.

3 participants