Claude Code Boilerplate
FeaturesPricingBlogDocs
Get started →

Product

  • Features
  • Pricing
  • Skills

Compare

  • vs ShipFast
  • vs MakerKit
  • vs supastarter

Resources

  • Docs
  • Blog
  • Discord

Legal

  • License
  • Privacy Policy
  • Terms of Service
Claude Code Boilerplate

© 2026 Claude Code Boilerplate. All rights reserved.

← All posts

How to Build a User Settings Page in Your Next.js SaaS -- Profile, Password, and Notifications

August 3, 2026
nextjssaascloudinarydrizzle-ormauth

The Settings Page Nobody Plans For

You wire up auth on day one. Then someone asks: can I change my profile photo? Then: how do I update my password? Then: can you email me less?

Suddenly you have three features, one route, and code that is harder to maintain than the signup flow that took three times as long to build.

A settings page looks simple. The mistake is treating it as one form. It is actually three separate concerns that happen to live on the same page -- and bundling them into a single component is the first thing founders regret when they need to change one of them.

Three Tabs, Not One Form

The cleanest pattern is a tabbed layout: Profile, Security, and Notifications. Each tab owns its own form, its own API endpoint, and its own Zod validation schema. This keeps each component under 100 lines, avoids full-page reloads when one section saves, and lets you expand any tab later without touching the others.

Here is the routing and API shape:

  • /settings -- renders the Profile tab by default
  • PATCH /api/users/me/profile -- display name, bio, avatar URL
  • PATCH /api/users/me/password -- current password + new password
  • PATCH /api/users/me/notifications -- boolean flags per notification type

Each endpoint reads the user from the JWT via getUserFromRequest, validates the request body with Zod using safeParse, and delegates to the user service. No business logic lives in the route.

Profile: Display Name and Avatar

The profile tab is the one users visit most. Name and bio are a standard React Hook Form + Zod form that patches /api/users/me/profile.

Profile photos are the part that trips people up. You need to:

  1. Upload the file to Cloudinary via /api/upload (already wired in the boilerplate)
  2. Store the returned public_id in the database -- not the full URL
  3. Construct the display URL at render time using Cloudinary transformation parameters

Storing only the public_id matters because it lets you change image dimensions, format, or quality later without a database migration. The upload flow from the client is two separate calls:

// upload to Cloudinary, get back a URL
const { url } = await uploadImage(file)
 
// save the URL to the user profile
await putter('/api/users/me/profile', { avatarUrl: url })

Keep the upload and the profile patch as two separate API calls. If the upload fails, the profile does not change. If the profile patch fails, the file is already in Cloudinary and the user can retry without re-uploading.

The image upload docs cover the full Cloudinary setup if you need it.

Security: Changing a Password

Password changes need three fields: current password, new password, and confirm new password. The confirm field is frontend validation only -- use Zod .refine() to check that both fields match, and never send the confirm value to the server.

On the server, your service layer must:

  1. Fetch the user record including passwordHash
  2. Verify currentPassword against the stored hash using bcryptjs.compare
  3. Hash the new password and write it back

If verification fails, throw HttpError(400, 'Current password is incorrect'). Do not reveal which specific check failed -- a single generic message is correct here.

There is one edge case to plan for before you have users: accounts created via OAuth will have a null passwordHash. If a user signs in through Google and then tries to open the Security tab, the password change form will fail in a confusing way. Detect the null hash early and show a different message: your account uses social login -- set a password first if you want to log in with email. You can wire a set-password flow that skips the current-password check.

This edge case is easy to handle while you have zero users. It is painful to retrofit after the fact.

Notifications: Email Preference Flags

Notification preferences are the easiest tab to build and the easiest to skip -- which is a mistake. Giving users control over email frequency is one of the cheapest retention levers you have. Users who can tune their emails churn less than users who unsubscribe from everything.

Add notification preference columns to your userTable in Drizzle ORM. A sensible default set:

emailMarketing       -- product updates, tips, announcements
emailDigest          -- weekly activity summary
emailTransactional   -- receipts, password resets (always on)

The Notifications tab renders a list of toggles using shadcn Switch components. One PATCH /api/users/me/notifications call saves all flags at once when the user clicks Save.

Lock emailTransactional to always-on in both the UI and the service layer. Show the toggle as checked and disabled. Security emails and receipts must reach users regardless of their marketing preferences -- this is not optional.

Putting It Together in the Boilerplate

If you are starting from the Next.js SaaS boilerplate, the infrastructure is already in place:

  • JWT auth -- getUserFromRequest is already wired; call it at the top of each PATCH handler
  • Drizzle ORM -- add the preference columns to userTable, run npm run db:generate && npm run db:migrate
  • Cloudinary -- the /api/upload route is already protected and returns { url }
  • React Hook Form + Zod -- define one schema per tab, infer the TypeScript type, pass to useForm
  • shadcn UI -- Tabs, Form, Input, Switch, and Button are all available without installing anything new

The settings page does not need a new module. Add three service methods to the existing user module: updateProfile, changePassword, and updateNotifications. Three endpoints. Three forms. One tab component per concern.

What to Ship and When

Not all three tabs have the same urgency:

  • Password change -- ship this the same day as auth, or at minimum before you send any user an invitation to your product. Changing auth code after you have real users is stressful.
  • Profile -- ships with or shortly after the MVP. Users expect to be able to update their name.
  • Notifications -- can wait a sprint. Implement it before you send your first marketing email.

The settings page is not glamorous. It does not appear in demo videos or on your landing page. But it is one of the first things users look for after they sign up -- and finding nothing is the kind of small friction that stacks up into churn.

If you want the foundation without building it yourself, the boilerplate has auth, Cloudinary uploads, Drizzle ORM, and all the UI components ready on day one. Get started here and spend your time on the features that make your product unique.