Build a Complete Profile Settings System with Supabase + Next.js (That Doesn’t Suck)

Most profile pages are either too basic or unnecessarily complex. Here’s a clean, scalable approach using Supabase and Next.js that actually feels good to build—and use.
Let’s be honest: most “profile settings” pages are either an afterthought… or a Frankenstein monster of tabs, modals, and half-working features.
So I built one the way it should be done.
Clean. Scalable. Secure. No nonsense.
Here’s how I designed a complete profile system using Supabase + Next.js App Router—and why each decision matters.
The Goal
A single, intuitive page where users can:
Update personal info
Upload a profile photo
Manage social links
Change their password
Enable two-factor authentication (2FA)
All inside:
/dashboard/profile
No tabs. No confusion. Just scroll and update.
Simple wins.
🧱 The Layout: One Page, Five Sections
Instead of splitting everything into tabs (which users hate more than captchas), the page is structured as stacked cards:
General Information
Profile Photo
Social Links
Password
Two-Factor Authentication
Each section has its own Save Changes button—so users don’t lose progress if something fails.
This is small UX detail, big impact.
👤 General Information (Keep It Flexible)
We store:
full_namepronounsbiocompanylocationwebsite
And here’s a key decision:
👉 Email is read-only
Why?
Because email changes are security-sensitive and should live in a dedicated flow—not casually inside a profile form.
🖼️ Avatar Upload (Done Right)
Stored in Supabase Storage:
avatars/{user_id}/avatar.{ext}
Constraints:
JPG, PNG, GIF
Max 2MB
Behavior:
Upload → replaces old file
Remove → deletes file + clears
avatar_url
No versioning headaches. No orphaned files.
Just clean storage hygiene.
🔗 Social Links (No Overengineering)
Seven supported platforms:
Twitter/X
LinkedIn
GitHub
Instagram
Facebook
YouTube
TikTok
Each is just a URL field.
No fancy validation. No API calls.
Because let’s be real—users will paste whatever they want anyway.
🔐 Password Change (Secure but Not Painful)
Flow:
Verify current password
Validate:
New ≠ Current
New === Confirm
Update via Supabase
supabase.auth.updateUser({ password })
Re-authentication step prevents session hijacking abuse.
🔒 Two-Factor Authentication (TOTP)
This is where things get serious.
We use Supabase MFA (TOTP only)—no SMS, no hardware keys.
Enable Flow
User clicks "Enable 2FA"
Supabase returns:
QR code
Manual secret
User scans with apps like:
Google Authenticator
Authy
1Password
User enters 6-digit code
Verify → done
Once enabled, login now requires a second step.
Disable Flow
Simple:
Confirm action
Call:
supabase.auth.mfa.unenroll()
Done.
🚧 The Important Part: Login Enforcement
This is where most people mess up.
After login:
Check MFA assurance level
If user has 2FA but hasn’t completed it → redirect
if (nextLevel === 'aal2' && currentLevel === 'aal1') {
redirect('/auth/mfa')
}
This ensures:
👉 Password alone is never enough once 2FA is enabled
That’s real security—not checkbox security.
🧠 Database Design (Minimal but Scalable)
We extend the profiles table with:
bio, pronouns, company, location, website
all social links
And here’s the smart part:
👉 No 2FA columns in the database
Why?
Because Supabase Auth handles MFA state.
Don’t duplicate what your auth provider already manages.
🧩 Feature Architecture
Everything lives in a clean module:
features/profile/
With:
Server actions (
updateProfile,updateAvatar, etc.)Typed forms
Isolated components per section
And yes:
👉 All mutations use server actions
Cleaner than API routes. Less boilerplate. Better DX.
⚡ Why This Approach Works
Most implementations fail because they:
Mix auth logic with UI
Overcomplicate validation
Scatter features across multiple pages
Forget real-world edge cases
This one avoids all that by:
✔ Keeping everything modular
✔ Letting Supabase handle auth complexity
✔ Using simple, predictable UX patterns
✔ Designing for failure (per-section saves)
🚀 Final Thoughts
A profile system sounds boring… until you realize:
👉 It touches auth, storage, database, UX, and security all at once.
Do it wrong, and users feel it immediately.
Do it right, and nobody notices—which is exactly the point.
If you’re building a SaaS, dashboard, or any user-based app, this is one of those features worth doing properly.
Because nothing says “unfinished product” faster than a broken profile page.