Support the well-known change password URL with Appwrite Auth_
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.

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 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.
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-passwordreturns a 3xx status with aLocationheader 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"andautocomplete="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 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.
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.
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.
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.
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.
VITE_APPWRITE_ENDPOINT=https://<REGION>.cloud.appwrite.io/v1
VITE_APPWRITE_PROJECT_ID=<PROJECT_ID>
Create one shared client in src/lib/appwrite.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.
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.
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 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.
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.
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.
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 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.
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.
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.
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 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.





