---
layout: post
title: "Support the well-known change password URL with Appwrite Auth"
description: Password managers open /.well-known/change-password when they find a leaked or weak password. Add the redirect and a change password page backed by Appwrite Auth.
date: 2026-09-16
cover: /images/blog/well-known-change-password-url/cover.avif
timeToRead: 9
author: atharva
category: tutorials
faqs:
  - question: "What is the well-known change password URL?"
    answer: "It is a W3C specification that reserves the path /.well-known/change-password on every origin. A site redirects that path to its change password page. Password managers such as Apple Passwords and Google Password Manager open it when they flag a saved password, so the user lands on the right form instead of the homepage."
  - question: "Why do I need the resource-that-should-not-exist path?"
    answer: "Some sites return 200 for every URL, which would make the redirect check meaningless. Clients request /.well-known/resource-that-should-not-exist-whose-status-code-should-not-be-200 first. If that returns 200, they treat the origin as unreliable and ignore the change password URL. Return 404 for it."
  - question: "Should the well-known path serve the form itself?"
    answer: "No. The specification says the change password URL must not host the form. It only redirects, with a 3xx status, to the page where the user changes the password."
  - question: "What happens when the user is not signed in?"
    answer: "Password managers open the URL cold, often in a fresh tab. The change password page checks for an Appwrite session. When there is none it sends the visitor to the login page with a return path, and the login page brings them back to the form after they sign in."
  - question: "How does the app change the password in Appwrite?"
    answer: "The Web SDK calls account.updatePassword with the new password and the current one. Appwrite verifies the current password, applies the project's password policies such as the dictionary and history checks, and stores the new hash."
  - question: "Which password managers use the change password URL?"
    answer: "Apple Passwords on iOS and macOS, Google Password Manager in Chrome, 1Password, and Bitwarden all use it. Any client that follows the W3C specification can discover the page the same way."
---

Every password manager now checks the passwords it stores. It compares them against known breaches, flags the ones that are reused or easy to guess, and shows a button that says "Change password". For most sites that button opens the homepage, so the user has to find the settings page on their own, and many never do.

The [well-known change password URL](https://www.w3.org/TR/change-password-url/) fixes this with one redirect. This tutorial adds it to a TanStack Start app that uses Appwrite Auth. It covers the redirect, the change password page that handles signed-out visitors, and the deployment to Appwrite Sites. At the end it opens the URL the way a password manager would and changes the password of a test account.

**Companion repository**

The complete project lives at [appwrite-community/smart-passwords](https://github.com/appwrite-community/smart-passwords). It contains the routes, the Appwrite client, and the environment file template.

# Why password managers need a change password URL

Password managers know when a password is weak, reused, or part of a public breach. What they did not know was where on a given site the user could change it. Every site puts that form somewhere different, behind different navigation, so the manager could only open the homepage.

Apple proposed a fix in 2019, and it became a W3C Working Draft from the Web Application Security Working Group. Every origin reserves the path `/.well-known/change-password`. A site that supports it redirects that path to its change password page. A client that finds a bad password opens the well-known path, follows the redirect, and the user is on the form.

Apple Passwords, Google Password Manager, 1Password, and Bitwarden all use the URL.

The next step is already shipping. In iOS 27, iPadOS 27, and macOS 27, the Passwords app can fix a weak or compromised password on its own. Apple describes it as Apple Intelligence and Safari signing in to the site, changing the password, and saving the new one, with a single tap from the user to start. Google is testing the same idea in Chrome Canary, where the Change password button becomes a Gemini-driven "Change it for me". An agent that has to find your change password form without a person guiding it needs a reliable way to locate it, and the well-known URL is that way. Sites that serve it are the ones these features can work on.

# How the URL works

Supporting the specification takes two pieces on your site.

- **The redirect.** `GET /.well-known/change-password` returns a 3xx status with a `Location` header that points at your change password page. The well-known path must not host the form itself.
- **The autocomplete hints.** The form marks its fields with `autocomplete="current-password"` and `autocomplete="new-password"`, so the manager can fill the old value, generate a new one, and update the saved entry.

The rest of this tutorial builds both with a TanStack Start app and Appwrite Auth.

# Register the app in your Appwrite project

Create a project in the [Appwrite Console](https://appwrite.io/sign-in) or open an existing one. Email and password sign-in is enabled by default under **Auth** > **Settings**, so there is nothing to switch on.

Go to **Apps**, click **Add app**, and select **Web** as the platform with **TanStack Start** as the framework.

![The Connect your app wizard with Web selected as the platform and TanStack Start as the framework](/images/blog/well-known-change-password-url/choose-platform.avif)

Give the app a name and set the hostname to `localhost` for development. Later you will add a second web app for the deployed domain.

![The app details step with the name Smart Passwords web and the hostname localhost](/images/blog/well-known-change-password-url/add-web-app.avif)

Next, create a user to test with. Go to **Auth** and click **Create User**. Give the user a name, an email, and a password. The example uses `password123` on purpose, because it is weak and on every breach list, so a password manager will flag it.

![The Create User form in the Appwrite Console with the name Maya Chen, the email maya@example.com, and a password](/images/blog/well-known-change-password-url/create-user.avif)

Copy the project ID and API endpoint from **Settings**. The app reads both from environment variables.

# Set up the TanStack Start app

Create a new TanStack Start project and add the Appwrite Web SDK.

```bash
npm create @tanstack/start@latest smart-passwords
cd smart-passwords
npm install appwrite
```

Add the Appwrite values to a `.env` file. Vite exposes variables that start with `VITE_` to the browser bundle.

```env
VITE_APPWRITE_ENDPOINT=https://<REGION>.cloud.appwrite.io/v1
VITE_APPWRITE_PROJECT_ID=<PROJECT_ID>
```

Create one shared client in `src/lib/appwrite.ts`.

```ts
import { Account, Client } from 'appwrite'

export const client = new Client()
  .setEndpoint(import.meta.env.VITE_APPWRITE_ENDPOINT)
  .setProject(import.meta.env.VITE_APPWRITE_PROJECT_ID)

export const account = new Account(client)
```

# Build the sign-in page

Password managers open the change password URL cold, often in a new tab where the user has no session. The login page therefore accepts a `redirect` search parameter and sends the user back to it after they sign in.

Create `src/routes/login.tsx`.

```tsx
import { useState } from 'react'
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { account } from '@/lib/appwrite'

// Only allow same-origin paths, so a crafted link cannot send a signed-in
// user to another site.
function safeRedirect(value: unknown): string {
  if (typeof value !== 'string') return '/'
  if (!value.startsWith('/') || value.startsWith('//') || value.startsWith('/\\')) return '/'
  for (const char of value) if (char.charCodeAt(0) < 32) return '/'
  return value
}

export const Route = createFileRoute('/login')({
  validateSearch: (search: Record<string, unknown>) => ({
    redirect: safeRedirect(search.redirect),
  }),
  component: Login,
})

function Login() {
  const { redirect } = Route.useSearch()
  const navigate = useNavigate()
  const [error, setError] = useState<string | null>(null)

  async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault()
    const form = new FormData(event.currentTarget)
    try {
      await account.createEmailPasswordSession({
        email: String(form.get('email')),
        password: String(form.get('password')),
      })
      navigate({ href: redirect })
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Sign in failed')
    }
  }

  return (
    <form onSubmit={onSubmit}>
      <label>
        Email
        <input name="email" type="email" autoComplete="username" required />
      </label>
      <label>
        Password
        <input name="password" type="password" autoComplete="current-password" required />
      </label>
      {error && <p>{error}</p>}
      <button type="submit">Sign in</button>
    </form>
  )
}
```

The `redirect` value comes from the URL, so `safeRedirect` only accepts a same-origin path. Anything that starts with a scheme, a double slash, or a backslash falls back to the home page, so a crafted link cannot send a signed-in user to another site.

The `autoComplete="username"` and `autoComplete="current-password"` attributes let the password manager fill the form and recognize which saved entry it belongs to.

# Build the change password page

Create `src/routes/settings.password.tsx`. On load it asks Appwrite for the current account. When there is no session, it redirects to the login page with the return path set to itself. When there is one, it renders the form.

```tsx
import { useEffect, useState } from 'react'
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { account } from '@/lib/appwrite'

export const Route = createFileRoute('/settings/password')({ component: ChangePassword })

function ChangePassword() {
  const navigate = useNavigate()
  const [ready, setReady] = useState(false)
  const [status, setStatus] = useState<{ ok?: string; error?: string }>({})

  useEffect(() => {
    account.get().then(
      () => setReady(true),
      () => navigate({ to: '/login', search: { redirect: '/settings/password' } }),
    )
  }, [navigate])

  async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault()
    const form = event.currentTarget
    const data = new FormData(form)
    try {
      await account.updatePassword({
        password: String(data.get('new-password')),
        oldPassword: String(data.get('current-password')),
      })
      form.reset()
      setStatus({ ok: 'Your password has been updated.' })
    } catch (err) {
      setStatus({ error: err instanceof Error ? err.message : 'Update failed' })
    }
  }

  if (!ready) return null

  return (
    <form onSubmit={onSubmit}>
      <label>
        Current password
        <input name="current-password" type="password" autoComplete="current-password" required />
      </label>
      <label>
        New password
        <input name="new-password" type="password" autoComplete="new-password" minLength={8} required />
      </label>
      {status.error && <p>{status.error}</p>}
      {status.ok && <p>{status.ok}</p>}
      <button type="submit">Update password</button>
    </form>
  )
}
```

The `updatePassword` call sends both values. Appwrite checks the current password before it accepts the new one. Appwrite also ships [security features](/docs/products/auth/security) for the new password, which you can turn on per project in the Console:

- **Password dictionary.** Appwrite rejects a new password that appears in the list of the 10,000 most common passwords.
- **Password history.** Appwrite remembers up to 20 previous passwords and stops the user from going back to one of them.

The `autoComplete="new-password"` attribute lets the password manager generate a strong password, fill it, and update its saved entry once the form submits.

# Serve the well-known redirect

TanStack Start file routes can declare server handlers that run before any rendering, which is all the redirect needs.

TanStack Router treats a dot in a file name as a path separator, so the leading dot in `.well-known` is escaped with square brackets. Create `src/routes/[.]well-known.change-password.ts`.

```ts
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/.well-known/change-password')({
  server: {
    handlers: {
      GET: () =>
        new Response(null, {
          status: 302,
          headers: { Location: '/settings/password' },
        }),
    },
  },
})
```

## Return 404 for the not-found path

Some sites answer every unknown URL with 200, which would make a redirect at the well-known path meaningless. Before trusting it, clients request `/.well-known/resource-that-should-not-exist-whose-status-code-should-not-be-200` and expect anything other than 200. A site that returns 200 there is treated as unreliable and its change password URL is ignored.

TanStack Start already returns 404 for unknown routes, so this app passes the check as it is. An explicit route still pins that behavior down, so a later catch-all route or a custom not-found page cannot turn it into a 200. Create `src/routes/[.]well-known.resource-that-should-not-exist-whose-status-code-should-not-be-200.ts`.

```ts
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute(
  '/.well-known/resource-that-should-not-exist-whose-status-code-should-not-be-200',
)({
  server: {
    handlers: {
      GET: () => new Response('Not Found', { status: 404 }),
    },
  },
})
```

Start the dev server and open `http://localhost:3000/.well-known/change-password` in a browser. The redirect lands on the change password page, which finds no session and forwards to the login page with the return path in the query string. Sign in with the test user, and the login page sends you straight back to the change password form. Submit it and Appwrite stores the new password.

![The change password form showing the message Your password has been updated](/images/blog/well-known-change-password-url/password-updated.avif)

# Deploy to Appwrite Sites

Password managers only trust the well-known URL on an HTTPS origin, so deploy the app before the last step. Go to **Sites**, click **Create site**, and choose to upload the site manually, or connect the Git repository if you pushed the project to one.

Select **TanStack Start** as the framework, import the two `VITE_` variables from your `.env` file, and pick a domain.

![The Create site form with TanStack Start selected, the two VITE environment variables imported, and the domain smart-passwords](/images/blog/well-known-change-password-url/create-site.avif)

The wizard creates TanStack Start sites with the static adapter, which serves files from `./dist/client` and drops server handlers. The well-known routes need the server, so open the site's **Settings** > **Build** page, switch the adapter to **SSR**, and confirm that the output directory is `./.output`. Redeploy after the change.

Once the deployment is ready, go back to **Apps** and add a second Web app with the site's domain as the hostname, so the Web SDK can create sessions from the deployed origin.

# Test it with a password manager

Open the deployed site, sign in as the test user, and let your password manager save the credential. The next steps depend on the manager. This walkthrough uses Google Password Manager in Chrome, and Apple Passwords works the same way from its **Security** section.

Open `chrome://password-manager` and go to **Checkup**. Chrome checks every saved password locally for strength and reuse, and against known breaches when you are signed in to Chrome. The saved password for the site is flagged.

![Google Password Manager Checkup showing one password checked and one weak password](/images/blog/well-known-change-password-url/chrome-checkup.avif)

Open the flagged group. Each entry has a **Change password** button, and because the site serves the well-known URL, Chrome points that button at `/.well-known/change-password` instead of the homepage.

![The weak password entry for smart-passwords.appwrite.network with a Change password button](/images/blog/well-known-change-password-url/chrome-weak-password.avif)

Click it. Chrome opens the well-known URL in a new tab, the redirect sends it to the change password page, and the page finds no session and forwards to the login page with the return path. Chrome autofills the saved credential.

![The demo app sign-in page opened by Chrome's Change password button, with the saved email and password autofilled](/images/blog/well-known-change-password-url/chrome-landing.avif)

After sign-in the user is on the form. The manager fills the current password, generates a new one into the `new-password` field, and updates its saved entry when the form submits.

# How the Appwrite Console does it

The Appwrite Console supports the same URL for your own account. Opening [appwrite.io/.well-known/change-password](https://appwrite.io/.well-known/change-password) redirects to the password card on the account security page, and the form carries the same autocomplete attributes. If a password manager ever flags your Console password, the button takes you to the right place.

# Resources

- [Appwrite Auth documentation](/docs/products/auth)
- [Email and password sign-in](/docs/products/auth/email-password)
- [Auth security settings](/docs/products/auth/security)
- [Deploy a TanStack Start site](/docs/products/sites/quick-start/tanstack-start)
- [W3C: A Well-Known URL for Changing Passwords](https://www.w3.org/TR/change-password-url/)
- [Join the Appwrite Discord](https://appwrite.io/discord)
