User Settings & Account Management
The /settings page is the single surface for an authenticated user’s personal account operations: editing public profile fields (display name, bio, links, avatar), changing their email or password, and deleting their account. Settings are split across user_profiles, user_links, and user_settings tables, but the user-facing operations all run through server actions.
Overview
Section titled “Overview”The ProfileSettings client component (src/components/account/ProfileSettings.tsx) renders the main form and mounts the account dialogs. The settings page itself (src/app/(main)/(dashboard)/settings/page.tsx) calls getAuthUserOrRedirect() so anonymous visitors are redirected to /login.
Three separate concern areas are handled by different actions:
| Operation | Action | Location |
|---|---|---|
| Profile fields and links | updateProfileSettings |
account/actions.ts |
| Email change (two-step OTP) | initiateEmailChange / verifyEmailChange |
settings/actions.ts |
| Password change | changePassword |
settings/actions.ts |
| Account deletion | deleteAccount |
lib/supabase/actions.ts |
Architecture
Section titled “Architecture”Profile Settings Form
Section titled “Profile Settings Form”updateProfileSettings (src/app/(main)/(feed)/(private)/account/actions.ts) validates input against profileSettingsSchema (Zod), runs the bio through the moderation gate (moderateField), upserts user_profiles, then reconciles user_links by deleting stale rows and upserting the current list. The moderation gate runs before any database writes; a rejection returns the flagged categories so the UI can display them.
The link reconciliation deletes all user_links rows that are not in the submitted id set before upserting, which means an empty submission clears all links. New links have no id and get a generated one on insert.
Email Change
Section titled “Email Change”Email change is a two-step OTP flow handled in src/app/(main)/(dashboard)/settings/actions.ts:
initiateEmailChange(newEmail)— callssupabase.auth.updateUser({ email }), which triggers Supabase to send an OTP to the new address.verifyEmailChange(newEmail, token)— callssupabase.auth.verifyOtpwith typeemail_change. After verification it checks thatdata.user.emailactually moved to the new address — under Supabase’sdouble_confirm_changessetting the OTP can be accepted without the change taking effect until a second confirmation arrives in the old mailbox. If the address hasn’t moved,verifyEmailChangereturns an explanatory error instead of a false success. On a confirmed change, a security notice email is sent to the previous address.
Password Change
Section titled “Password Change”changePassword(currentPassword, newPassword) re-authenticates with signInWithPassword to verify the current password before calling supabase.auth.updateUser. If the current password is wrong it returns a "current_password_incorrect" code for the UI to handle. On success it calls supabase.auth.signOut({ scope: "others" }) to invalidate other active sessions.
Privacy Settings (Roadmap)
Section titled “Privacy Settings (Roadmap)”The user_settings table has privacy and public_profile columns. Only the profile layout at src/app/(main)/(profile)/profiles/[username]/layout.tsx reads these — it filters the query to profiles where public_profile = true and privacy = "public". The /api/user-settings route exists but has no UI callers. A settings UI for privacy is planned and not yet built.
Account Deletion
Section titled “Account Deletion”Account deletion is fully built. deleteAccount in src/lib/supabase/actions.ts runs in this order:
- Calls the
delete_user_accountRPC, which soft-deletes all owned content and returns the R2 storage keys to purge. This is the only fatal step — if it fails, the account is still intact. - Calls
adminClient.auth.admin.deleteUserto remove theauth.usersrow. Failure here is logged but not surfaced — the content rows are already gone, so the account is effectively inaccessible, but a strayauth.usersrow may remain for manual cleanup. - Calls
StorageAdapter.deleteFilesfor the returned R2 keys. Failure is logged but does not block. - Sends a deletion confirmation email to the account’s address.
- Signs out locally and calls
clearActiveAccount, then redirects to/.
ProfileSettings receives isOrgOwner as a prop. When it is true, the delete button opens an extra dialog step informing the user that their organisations will be affected.
Failure Modes & Edge Cases
Section titled “Failure Modes & Edge Cases”- Moderation rejection on bio —
updateProfileSettingsreturns both anerrorstring andmoderationCategoriesso the form can surface the specific category. - Double confirm changes on email —
verifyEmailChangedetects when the change hasn’t taken effect and returns an error asking the user to check the previous mailbox. - R2 purge failure on deletion — the deletion proceeds; the unpurged keys are logged with the user id for a manual sweep.
- Admin delete failure on deletion — also non-fatal to the user flow but logged; the
auth.usersrow remains without a profile.
Operational Notes
Section titled “Operational Notes”All three settings/actions.ts actions use createActionClient so session cookies are correctly rotated after password operations. updateProfileSettings uses getAuthUser (not getAuthUserOrRedirect) so it can return a typed error for the unauthenticated case rather than redirecting — the action is always called from within an authenticated layout anyway.
revalidatePath("/settings") is called after a profile update. If the username changed, the profile route is also revalidated: revalidatePath("/profiles/[username]", "layout").
Related Links
Section titled “Related Links”- Auth Flows — login, signup, and password reset
- Account Switching & Active Account — org mode and the active account cookie
- Moderation — how bio moderation works