# Account management (/docs/account-management) Open it with `AuthUI.open("account")`, from the `` menu, or embed it directly with ``. ## Profile [#profile] * **Name**: update the display name. * **Account ID**: muted mono badge under the profile fields. Click to copy the user `$id` to the clipboard (screen readers hear "Copied"). * **Email**: change the address (requires the current password). When the address is unverified, Auth UI starts an in-panel email OTP (with an optional security phrase) and falls back to a verification link if the project does not return OTP mode. A badge shows whether the address is verified. * **Phone**: change the number (requires the current password), send an SMS code and verify it. * **Delete account**: calls `account.updateStatus()`, which blocks the account and ends the session. Appwrite keeps the record so an administrator can restore it. Client-side deletion is not offered by Appwrite; use a server function if you need it. Guest accounts see a **Create your account** form instead, which attaches an email and password to the existing user. ## Security [#security] * **Change password**, with the current password when the account has one. OAuth-only accounts can set a first password. * **Two-factor authentication** switch, authenticator enrollment and removal. * **Recovery codes** generation, display and regeneration. Changing the password ends other sessions when the project's session-invalidation policy is on. Auth UI refreshes its state afterwards. ## Sessions [#sessions] Lists every active session with client, operating system, country and IP, and marks the current device. Each row has a sign-out action. **Sign out everywhere** ends all sessions, including the current one. ## Connections [#connections] Lists connected OAuth identities with the provider's email or user ID, offers to disconnect each, and shows buttons for any configured provider that is not yet connected. Connecting goes through the same OAuth redirect as sign in. ## Authorized apps [#authorized-apps] When Appwrite exposes OAuth2 consents (`listConsents`), the account screen shows an **Authorized apps** tab. Users can review client apps, the scopes they granted, and revoke access. When the server also exposes consent token families (`listConsentTokens`), each app row can list and revoke individual devices without revoking the whole consent. The tab hides itself on servers without the route. ## Teams [#teams] Create teams, leave teams, and (as an owner) invite members by email. Invite links use Auth UI's redirect handler so recipients land back on your page and join automatically. The same surface is linked from **Manage teams** on ``. ## Activity [#activity] The account security log from `account.listLogs()`, when the server exposes it. Appwrite Cloud 2.2.0 does not (the route returns 404), and some self-hosted builds omit it too. Auth UI probes once on load and only shows the Activity tab when the endpoint responds successfully. # Common mistakes (/docs/common-mistakes) A short cookbook of the failures newcomers hit most often. Pair it with [Configuration](/docs/configuration) and the [Misconfiguration](/docs/components/config#misconfiguration) notes on ``. ## Endpoint without `/v1` [#endpoint-without-v1] Appwrite's HTTP API lives under `/v1`. If you pass `https://cloud.appwrite.io` (or your self-hosted host) without that suffix, Auth UI shows a single sticky config error banner asking you to end the endpoint with `/v1`. ```ts // Wrong AuthUI.init({ endpoint: "https://cloud.appwrite.io", project: "…" }); // Right AuthUI.init({ endpoint: "https://cloud.appwrite.io/v1", project: "…" }); ``` Regional Cloud endpoints look like `https://fra.cloud.appwrite.io/v1`. Auth UI also warns in the console when the endpoint does not end with `/v1`. ## Hostname not registered as a Web platform [#hostname-not-registered-as-a-web-platform] Auth UI runs on your origin. Appwrite rejects requests from hosts that are not listed under **Overview → Integrations → Platforms**. Add a Web platform for `localhost` (and your production hostname) before testing, or the first `account.get()` fails with `general_unknown_origin`. ## Methods enabled in the UI but not in the Console [#methods-enabled-in-the-ui-but-not-in-the-console] `methods` on `` / `AuthUI.init()` only controls which buttons Auth UI shows. Each method must also be enabled under **Auth → Settings** in the Appwrite Console (and each OAuth provider must be configured with client IDs). Otherwise the UI looks ready and the request fails at runtime. ## CDN URL: use the CDN builds, not `dist/authui.js` [#cdn-url-use-the-cdn-builds-not-distauthuijs] Bare unpkg / jsDelivr package URLs and the dedicated CDN files are safe in a ` ``` With a bundler, prefer `import { AuthUI } from "@getauthui/core"`. Pin a version on the CDN in production. ## Protected content flashes (FOUC) [#protected-content-flashes-fouc] The in-module FOUC guard cannot run until the script loads. Load the shipped FOUC stylesheet in `` before the Auth UI script so `` children stay hidden (same rules as `CRITICAL_FOUC_CSS`): ```html ``` Bundlers: `import "@getauthui/core/fouc.css"`. See [``](/docs/components/show). In React, use the `Show` wrapper from `@getauthui/core/react` so children are not mounted while hidden. ## Building custom login forms [#building-custom-login-forms] Auth UI already covers email/password, magic URL, email OTP, phone, anonymous, OAuth and MFA. Do not rebuild those forms yourself. Use ``, `` or `AuthUI.open()`, and let the Appwrite SDK own the session. Never pass session secrets or JWTs through URLs. For apps that only need the session (no Auth UI chrome), call `AuthUI.init()` / `AuthUI.on("signed-in", …)` and still skip hand-rolled credential forms. ## Related [#related] * [Getting started](/docs/getting-started) * [AI agents](/docs/guides/ai-agents) * [Security notes](/docs/guides/security) # Comparison (/docs/comparison) ## Feature comparison [#feature-comparison] | Feature | Auth UI | Clerk Components | Auth0 Universal Login | Supabase Auth UI | Hand-rolled Appwrite forms | | ------------------------------------- | --------------------------- | ---------------------- | --------------------------- | --------------------------- | -------------------------- | | Works with Appwrite | Yes | No | No | No | Yes | | Hosted IdP / auth backend | No (uses your Appwrite) | Yes | Yes | Yes (Supabase Auth) | No | | Session on your origin | Yes | Depends on setup | Hosted login domain | Yes (your Supabase project) | Yes | | Drop-in Lit web components | Yes | React-first components | Hosted pages | React / framework packages | You build them | | One script tag / CDN | Yes | No | Embed / redirect | Limited | N/A | | Email / password, OAuth, passwordless | Via Appwrite | Yes | Yes | Yes | You wire Appwrite SDK | | MFA UI | Yes | Yes | Yes | Partial | You build it | | Account / sessions UI | Yes | Yes | Dashboard / APIs | Limited | You build it | | Framework agnostic | Yes | React-centric | Hosted | Framework packages | Whatever you choose | | Bundle (CDN, gzipped) | \~106 KB ESM / \~92 KB IIFE | Larger app SDK | Hosted (no local UI bundle) | Smaller UI kit | Your code | Sizes for Auth UI are the published `@getauthui/core@0.1.45` CDN builds (Lit and the Appwrite Web SDK included). ## Honest framing [#honest-framing] Auth UI is **Appwrite-only**. It is not a hosted identity provider. There is no Auth UI backend, no custom login domain, and no session hand-off through a third-party origin. You bring an Appwrite project; Auth UI is the UI layer on top of the official Web SDK, running as Lit custom elements inside your page. If you need a full IdP with social connections, enterprise SAML, or a hosted login box that is not Appwrite, look at Clerk, Auth0, or similar. If your stack is Supabase, use Supabase Auth UI. If you already call Appwrite `Account` yourself and only want a few fields, hand-rolled forms can be enough. ## Where Auth UI wins [#where-auth-ui-wins] **Session stays on your origin.** Requests go from your page to your Appwrite endpoint. Third-party cookie blocking does not break the session the way a separate hosted login domain can. **Drop-in web components.** One pinned script (or an npm import) registers ``, ``, ``, ``, and the rest. Works with plain HTML and any framework that tolerates custom elements. **Appwrite surface area without reinventing forms.** Sign-in methods, MFA challenges, recovery, verification, sessions, and account settings are covered in one themeable kit that follows the Appwrite Console design system. **No Auth UI server to operate.** Configuration is attributes or `AuthUI.init`. Persistence is the Appwrite SDK's job. ## Where the others win [#where-the-others-win] **Clerk Components** shine when you want a complete user management product (organizations, billing hooks, React-first DX) and are fine depending on Clerk as the IdP. **Auth0 Universal Login** wins for enterprise federation, extensive IdP connections, and a hosted, centrally branded login experience across many apps. **Supabase Auth UI** is the natural fit when your backend is already Supabase. It is not a substitute for Appwrite. **Hand-rolled Appwrite forms** win when you need a bespoke flow, minimal bytes, or only one method (for example email OTP alone) and you are happy to maintain the UI and error handling yourself. ## When to use Auth UI [#when-to-use-auth-ui] * Your auth backend is Appwrite (Cloud or self-hosted) * You want sign-in, MFA, and account UI without building forms from scratch * You need the session on your origin (SPA, static site, or same-site app) * You prefer web components or a single CDN script over a React-only kit * You do not want a second auth vendor beside Appwrite ## When not to use Auth UI [#when-not-to-use-auth-ui] * You need a hosted IdP that is not Appwrite (Clerk, Auth0, Cognito, and so on) * Your project is on Supabase, Firebase Auth, or similar (use that ecosystem's UI) * You must have server-rendered auth pages with no client JavaScript * You only need a single custom field and already own the Appwrite SDK calls * You need Auth UI to store or broker sessions on its own domain (it deliberately does not) See also [How it works](/docs/how-it-works) for the origin and redirect model, and [Getting started](/docs/getting-started) to try it on a page. # Configuration (/docs/configuration) Call `AuthUI.init(config)` or place an `` element. Both write to the same global store, and calling either again replaces the configuration. ## Options [#options] | Option | Attribute | Type | Default | Description | | ---------------------------- | -------------------- | --------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------ | | `endpoint` | `endpoint` | `string` | required | Appwrite API endpoint, e.g. `https://cloud.appwrite.io/v1`. | | `project` | `project` | `string` | required | Appwrite project ID. | | `redirectUrl` | `redirect-url` | `string` | current page | Absolute URL that email links and OAuth return to. Must be on a registered platform. | | `successUrl` | `success-url` | `string` | none | Navigate here after a successful sign in. Without it the page stays put and the modal closes. | | `methods` | `methods` | object / list | email-password | Which sign-in methods to show. See below. | | `signUp` | `sign-up` | `boolean` | `true` | Show the sign-up link and screen. | | `signUpUrl` | `sign-up-url` | `string` | none | When `signUp` is false, keep "Don't have an account?" linking to this external URL. | | `forgotPassword` | `forgot-password` | `boolean` | `true` | Show the Forgot password link on sign-in. | | `oauthPosition` | `oauth-position` | `top` / `bottom` | `top` | Place OAuth buttons above or below the credential form. | | `requireName` | `require-name` | `boolean` | `true` | Ask for a display name during sign up. | | `mfa` | `mfa` | `boolean` | `true` | Let users enroll and manage MFA from the account screen. | | `securityPhrase` | `security-phrase` | `boolean` | `true` | Request a security phrase for magic URL and email OTP and show it in the UI. | | `oauthScopes` | none | object | none | Extra scopes per provider, e.g. `{ google: ["profile"] }`. | | `branding.name` | `name` | `string` | none | Product name used in headings. | | `branding.logo` | `logo` | `string` | none | Logo URL rendered above the form. | | `branding.theme` | `theme` | `light` / `dark` / `auto` | `auto` | Colour scheme. `auto` follows an `html.dark` class, then `prefers-color-scheme`. | | `branding.radius` | `radius` | `none` / `sm` / `md` / `lg` / `xl` / `full` | `md` | Corner radius scale. | | `branding.primary` | `primary` | CSS colour | zinc | Primary button colour. | | `branding.primaryForeground` | `primary-foreground` | CSS colour | white | Text colour on primary buttons. | | `legal.termsUrl` | `terms-url` | `string` | none | Adds a Terms of Service link under the form. | | `legal.privacyUrl` | `privacy-url` | `string` | none | Adds a Privacy Policy link under the form. | | `legal.requireAcceptance` | `require-acceptance` | `boolean` | `false` | On sign-up, require a checkbox accepting terms/privacy before submit or OAuth. | | `legal.helpUrl` | `help-url` | `string` | none | Help / support URL in the legal footer and blocked-user "Contact support" link. | | `methods.oauthLayout` | `oauth-layout` | `stack` / `accordion` / `icon` / `horizontal` | auto | OAuth button layout. Default: stack for 1–2 providers, accordion for 3+. `icon` / `horizontal` = icon row. | | `strings` | none | object | English | Override any UI string. Applied after the locale pack. See [Theming](/docs/theming#strings). | | `locale` | `locale` | `en` / `cs` / `de` / `fr` | `en` | Built-in locale pack. Unknown tags fall back to English. See [Theming](/docs/theming#locale). | | `oneTap` | `one-tap` | `boolean` | `false` | Auto-prompt Google One Tap on signed-out sign-in/sign-up. Requires `googleClientId`. Soft-fails if GIS is blocked. | | `googleClientId` | `google-client-id` | `string` | none | Google OAuth Web client ID for One Tap (from Google Cloud). Appwrite does not expose provider client IDs. | | `identifierFirst` | `identifier-first` | `boolean` | `false` | Sign-in shows email then Continue, then password. OAuth stays on step 1. Sign-up keeps email+password together. | | `preview` | `preview` | `boolean` | `false` | Preview mode: no requests, any credentials accepted, sample data. See [Preview mode](/docs/guides/preview-mode). | ## Methods [#methods] In JavaScript, `methods` is an object: ```ts methods: { emailPassword: true, magicUrl: true, emailOtp: false, phone: false, anonymous: true, oauth: ["google", "github", "apple"], oauthLayout: "icon", // or "stack" | "accordion" | "horizontal" } ``` As an attribute it is a space separated list. OAuth providers are prefixed with `oauth:`: ```html ``` Valid tokens: `email-password`, `magic-url`, `email-otp`, `phone`, `anonymous` (alias `guest`), and `oauth:` for any provider slug Appwrite accepts, for example `google`, `github`, `apple`, `microsoft`, `discord`, `facebook`, `x`, `linkedin`, `slack`, `gitlab`, `bitbucket`, `twitch`, `spotify`, `notion`, `dropbox`, `figma`, `oidc`, `okta`, `auth0`. Every method you enable here must also be enabled in the Appwrite Console. If it is not, Appwrite responds with `user_auth_method_unsupported` and Auth UI shows "This sign-in method is disabled for this project." ## Full example [#full-example] ```ts AuthUI.init({ endpoint: "https://cloud.appwrite.io/v1", project: "acme", redirectUrl: "https://acme.com/auth", successUrl: "/dashboard", methods: { emailPassword: true, emailOtp: true, anonymous: true, oauth: ["google", "github"] }, signUp: true, branding: { name: "Acme", logo: "/logo.svg", theme: "auto", radius: "lg", primary: "#fd366e" }, legal: { termsUrl: "/terms", privacyUrl: "/privacy", requireAcceptance: true }, locale: "de", oneTap: true, googleClientId: "YOUR_GOOGLE_WEB_CLIENT_ID.apps.googleusercontent.com", identifierFirst: true, strings: { signIn: "Log in", continueAsGuest: "Try without an account" }, }); ``` # Vue, Svelte, Angular and plain HTML (/docs/frameworks) Auth UI ships standard custom elements, so every framework can render them. Import the package once at startup and use the tags in templates. Listen to `authui-success`, `authui-view` and the `authui:*` window events like any DOM event. ## Plain HTML [#plain-html] ```html Sign in ``` The default unpkg / jsDelivr ESM entry (`type="module"` on the bare package URL, or `…/dist/authui.cdn.mjs`) registers the custom elements only. It does not set `window.AuthUI`. For classic scripts that call `AuthUI.init` / `AuthUI.on` / `AuthUI.open`, load the IIFE build instead: ```html ``` Do not load `…/dist/authui.js` from a CDN (that path is the bundler entry). See [Common mistakes](/docs/common-mistakes). In Vue, Svelte, Angular and other bundlers, use `import { AuthUI } from "@getauthui/core"` as shown below. ## Vue [#vue] ```ts // main.ts import "@getauthui/core"; ``` ```vue ``` Tell Vue the tags are custom elements in `vite.config.ts`: ```ts vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith("authui-") } } }); ``` ## Svelte [#svelte] ```svelte goto("/dashboard")}> ``` Svelte renders unknown tags as custom elements out of the box. Use `onMount` in SvelteKit so the import only runs in the browser. ## Angular [#angular] Add `CUSTOM_ELEMENTS_SCHEMA` to your module or standalone component and import the package in `main.ts`. ```ts import "@getauthui/core"; ``` ```html Sign in ``` ## Reading state outside components [#reading-state-outside-components] ```ts import { AuthUI } from "@getauthui/core"; const stop = AuthUI.on("change", (state) => console.log(state.status, state.user)); stop(); // unsubscribe ``` # Getting started (/docs/getting-started) ## 1. Register your domain in Appwrite [#1-register-your-domain-in-appwrite] Auth UI talks to Appwrite from your own page, so your app's hostname must be a registered **Web platform** in the Appwrite Console. Open your project, go to **Overview** → **Integrations** → **Platforms**, and add a Web platform with your hostname, for example `myapp.com` or `localhost`. Then enable the sign-in methods you plan to use under **Auth** → **Settings**. Email/password, magic URL, email OTP, phone, anonymous and each OAuth provider are individual toggles. ## 2. Install [#2-install] Add one script tag anywhere in your HTML. The bundle includes Lit and the Appwrite SDK. ```html ``` The `fouc.css` link hides `` and undefined buttons until Auth UI is ready. The IIFE build registers the custom elements and sets `window.AuthUI`, so classic scripts can call `AuthUI.init` / `AuthUI.on`. The default unpkg / jsDelivr ESM entry (`type="module"` on the bare package URL, or `…/dist/authui.cdn.mjs`) only defines the elements; it does not set a global. Pin a version in production. Do **not** load `…/dist/authui.js` from a CDN. That file is the bundler entry (bare `import "appwrite"`) and fails in the browser with `Failed to resolve module specifier "appwrite"`. See [Common mistakes](/docs/common-mistakes). With a bundler, prefer `import { AuthUI } from "@getauthui/core"` instead of the CDN script. ```bash npm install @getauthui/core appwrite lit ``` ```ts import "@getauthui/core/fouc.css"; import "@getauthui/core"; ``` Import `fouc.css` in your entry (or put the CDN `` in ``) so `` stays hidden before the module runs. `appwrite` and `lit` are peer dependencies so they are shared with the rest of your app. ## 3. Configure [#3-configure] Either declaratively with an element: ```html ``` Or imperatively from JavaScript: ```ts import { AuthUI } from "@getauthui/core"; AuthUI.init({ endpoint: "https://cloud.appwrite.io/v1", project: "YOUR_PROJECT_ID", methods: { emailPassword: true, magicUrl: true, oauth: ["google", "github"] }, branding: { name: "Acme" }, }); ``` Configuration is global. Every Auth UI element on the page reads from the same store. ## 4. Add a trigger and the modal [#4-add-a-trigger-and-the-modal] ```html Sign in ``` Wrap each control in [``](/docs/components/show) so only one appears at a time. Without it, `` and `` both render "Sign in" while signed out. `` opens the modal. `` shows the avatar and account menu once signed in. The modal element is created automatically the first time it is needed, or you can place `` yourself. ## Prefill email [#prefill-email] Pass `email` or `login-hint` on ``, or open the page with `?login_hint=user@example.com`. The field is filled once and stays editable. ## 5. React to sign in [#5-react-to-sign-in] ```ts import { AuthUI } from "@getauthui/core"; AuthUI.on("signed-in", (user) => { console.log("Hello", user.name); }); ``` Pick the matching path: * **IIFE CDN** (the ` Sign in ``` ## Why it runs on your domain [#why-it-runs-on-your-domain] Earlier versions of Auth UI were a hosted login page on a separate domain. Browsers now block or partition third-party cookies, so a session created on one site cannot be read from another. Auth UI v2 runs inside your page instead. Its requests to Appwrite are indistinguishable from your own app's requests, so the session lands exactly where your app needs it. Read more in [How it works](/docs/how-it-works). ## What it's not [#what-its-not] Auth UI is not a hosted identity provider and not a multi-backend auth kit. It does not: * Replace Appwrite (or work with Clerk, Auth0, Supabase Auth, and so on) * Run an Auth UI backend or a custom login domain * Store or broker sessions off your origin * Replace a fully custom form when you only need one Appwrite call If you need those things, see the [Comparison](/docs/comparison) page. For Appwrite apps that want drop-in Lit components on your own domain, start with [Getting started](/docs/getting-started). ## Where to go next [#where-to-go-next] # Multi-factor authentication (/docs/mfa) ## During sign in [#during-sign-in] After any primary sign in, Auth UI calls `account.get()`. If Appwrite answers `user_more_factors_required`, Auth UI lists the factors the account can use and shows a chooser: * **Authenticator app** (TOTP) * **Email code**, available when the account's email is verified * **SMS code**, available when the account's phone is verified * **Recovery code** Choosing a factor creates a challenge with `createMFAChallenge()`. For email and SMS this sends the code. The user enters the code and Auth UI completes the challenge with `updateMFAChallenge()`. Cancelling ends the partial session so the user can start over. The same detection runs on every page load, so a user who refreshes in the middle of a challenge lands back on the chooser. ## Enrolling [#enrolling] The **Security** tab of the account screen has a **Two-factor authentication** card: 1. **Add authenticator** calls `createMFAAuthenticator("totp")`. Auth UI renders the returned `otpauth://` URI as a QR code using your own Appwrite endpoint's Avatars service, and prints the secret for manual entry. 2. The user scans it and types the six-digit code. `updateMFAAuthenticator()` verifies it. 3. Flip the **Two-factor authentication** switch to enforce MFA on the account with `updateMFA(true)`. Appwrite only requires a second factor when the account both has MFA enabled and has at least one usable factor. Auth UI still warns if you enable the switch with no authenticator and no verified email or phone, because the setting alone does not protect the account until a factor exists. ## Recovery codes [#recovery-codes] **Generate recovery codes** creates a set of one-time codes and shows them with a copy button. Each code signs in once. On Appwrite 2.x the full list can be read only once after generation; later attempts need a fresh second factor (or regenerate). **Regenerate** replaces the whole set. ## Step-up verification [#step-up-verification] Reading or regenerating recovery codes and removing an authenticator are protected: Appwrite requires a factor verified within the last 30 minutes and otherwise answers `user_challenge_required`. Verifying a newly enrolled authenticator does **not** count as that recent challenge on the server (unlike preview mode), so opening recovery codes right after enrolment often triggers step-up. Auth UI catches `user_challenge_required`, shows an inline challenge card, and retries the original action once the challenge succeeds. ## Disabling MFA in the UI [#disabling-mfa-in-the-ui] Set `mfa: false` in the configuration to hide the MFA and recovery code cards. Challenges during sign in still appear, because they are enforced by the server for accounts that already have MFA on. # React (/docs/react) ```bash npm install @getauthui/core appwrite lit ``` ```tsx import { AuthUIProvider, AuthUIButton, AuthUIUserButton, Show, useAuthUI, } from "@getauthui/core/react"; const config = { endpoint: "https://cloud.appwrite.io/v1", project: "YOUR_PROJECT_ID", methods: { emailPassword: true, oauth: ["google"] }, }; export function App() { return (
); } function Header() { const { status, user, open, signOut } = useAuthUI(); return (
Sign in {user?.name} {status === "signed-in" && }
); } ``` ## `useAuthUI()` [#useauthui] Returns the current state plus helpers, and re-renders on every change. | Field | Type | Description | | ------------- | --------------------------------------------------------------- | ------------------------------------------------------------- | | `status` | `"loading"` / `"signed-out"` / `"signed-in"` / `"mfa-required"` | Current state. | | `user` | `Models.User \| null` | The signed-in user. | | `mfaFactors` | `Models.MfaFactors \| null` | Available factors while `status` is `mfa-required`. | | `pending` | pending action or `null` | A redirect flow waiting to be finished. | | `configured` | `boolean` | Whether a complete config (endpoint and project) was applied. | | `configError` | `string \| null` | Developer-facing config problem, or `null`. | | `open(view?)` | function | Open the modal. | | `close()` | function | Close the modal. | | `signOut()` | function | End the current session. | | `store` | `AuthStore` | Full imperative API. | ## Components [#components] | Component | Wraps | Props | | ------------------ | ---------------------------------- | ------------------------------------------------------------------------- | | `AuthUIProvider` | configures the store synchronously | `config` | | `AuthUIModal` | `` | `view`, `open`, `closeOnSuccess`, `onOpenChange`, `onSignedIn`, `onClose` | | `AuthUIButton` | `` | `view`, `variant`, `size`, `children` | | `AuthUIUserButton` | `` | `src`, `showTeams`, `menuItems`, `onMenuAction`, `children` | | `AuthUISignIn` | `` | `view`, `email`, `loginHint`, `onSuccess` | | `AuthUIAccount` | `` | `tab` | | `Show` | conditional render (not Lit slot) | `when`, `unless`, `children` | ## `Show` vs `` [#show-vs-authui-show] The React `Show` wrapper returns `null` when the status does not match, so children are not mounted and their effects do not run. That is what you want when a child calls `store.getClient()` on mount. The Lit `` element only skips its slot. Children stay in the DOM (hidden). Prefer the React `Show` in React apps; keep `` for plain HTML. ## `AuthUIProvider` configuration timing [#authuiprovider-configuration-timing] `AuthUIProvider` calls `configure()` during render, so the first paint already sees `configured: true` and a non-null `getClient()` when `endpoint` and `project` are set. If either is empty, it mirrors ``: `configured` stays false and `configError` explains what is missing. Changing any field on `config` (methods, branding, preview, strings, and so on) reconfigures the store. Passing a new object every render with the same values is fine; the provider fingerprints a canonical (sorted-key) JSON of the config, so key insertion order does not matter. ## `closeOnSuccess={false}` [#closeonsuccessfalse] Pass `closeOnSuccess={false}` to `AuthUIModal` to keep the modal open after sign in. In HTML use `close-on-success="false"` on `` (the string `"false"` is required; a bare boolean attribute would stay on). ## Controlled `open` [#controlled-open] Pass `open` as a boolean to drive the dialog from React state. `open={true}` opens it; `open={false}` force-closes it (including after the user opened it another way). Omit `open` to leave the modal uncontrolled. When the user closes via Escape, backdrop click, or the X button, Lit hides the dialog. Wire `onOpenChange` so parent state stays in sync; otherwise the next `setOpen(true)` is a no-op because React still thinks the dialog is open. ```tsx const [modalOpen, setModalOpen] = useState(false); <> ; ``` `onOpenChange(false)` runs when the dialog closes. `onClose` is a convenience alias for that case. `onSignedIn` bridges the Lit `authui-signed-in` event so Modal + Button apps do not need to subscribe to the store by hand. ```tsx console.log(user.$id)} onClose={() => console.log("closed")} /> ``` Setting `open` to `false` writes through to the element so the dialog closes even when it was opened via `AuthUI.open()` or a button. ## Typing custom elements in JSX [#typing-custom-elements-in-jsx] If you prefer to use the elements directly in TSX, add a declaration: ```ts import type { AuthUIView } from "@getauthui/core"; declare module "react" { namespace JSX { interface IntrinsicElements { "authui-button": React.DetailedHTMLProps< React.HTMLAttributes & { view?: AuthUIView; variant?: string; size?: "sm" | "md" | "lg"; }, HTMLElement >; "authui-modal": React.DetailedHTMLProps< React.HTMLAttributes & { view?: AuthUIView; open?: boolean; "close-on-success"?: boolean | string; }, HTMLElement >; "authui-sign-in": React.DetailedHTMLProps< React.HTMLAttributes & { view?: AuthUIView; email?: string; "login-hint"?: string; }, HTMLElement >; "authui-account": React.DetailedHTMLProps< React.HTMLAttributes & { tab?: string }, HTMLElement >; "authui-user-button": React.DetailedHTMLProps< React.HTMLAttributes & { src?: string; "show-teams"?: boolean | string; }, HTMLElement >; "authui-show": React.DetailedHTMLProps< React.HTMLAttributes & { when?: string; unless?: string }, HTMLElement >; "authui-config": React.DetailedHTMLProps< React.HTMLAttributes & { endpoint: string; project: string }, HTMLElement >; } } } ``` ## FOUC CSS in React [#fouc-css-in-react] Import `@getauthui/core/fouc.css` (or the CDN `dist/fouc.css` link). Prefer `authui-show:not([ready])` over `:defined` for FOUC CSS. React can define custom elements before Auth UI sets `ready`, so `[ready]` is the reliable gate. See [``](/docs/components/show). ## Next.js [#nextjs] The components touch `window`, so import the package from a client component or inside `useEffect`. The wrappers in `@getauthui/core/react` are safe to import in client components. # Sign-in methods (/docs/sign-in-methods) ## Email and password [#email-and-password] The default. The sign-in screen shows email and password fields with a "Forgot password?" link and, when `signUp` is enabled, a link to the sign-up screen. Sign up creates the account with `account.create()` and immediately signs in. Set `identifierFirst` to split sign-in into email → Continue → password (see below); sign-up stays one step. Password rules come from your project: minimum length, dictionary check, personal data check, password history and the breached-password check are all enforced by Appwrite. Auth UI maps each rejection to a specific message. **Password recovery** prefers an in-panel OTP (`createRecoveryOTP` / `updateRecoveryOTP`) when the Appwrite server supports it: the user enters a code, then a new password. On older servers the UI falls back to `createRecovery()` and emails a link that opens the **Reset password** screen. Deep links with `userId` and `secret` still complete via `updateRecovery`. ## Magic URL [#magic-url] The user enters an email and receives a link. Opening the link on the same device signs them in. Auth UI requests a **security phrase** by default and shows it under the confirmation, so the user can check that the email they received is the one they asked for. Disable it with `securityPhrase: false`. ## Email OTP [#email-otp] The user enters an email and receives a six-digit code, then types it into Auth UI. No page navigation involved, which makes it a good fit for mobile web. The security phrase is shown here too. ## Phone [#phone] The phone step includes a country dial-code picker and a national number field. Auth UI composes an E.164 value for Appwrite, then the user receives an SMS code and types it in. The default country follows `navigator.language` when possible, otherwise `+1`. Appwrite needs an SMS provider configured under **Messaging**. On Appwrite Cloud, phone authentication is metered. ## Anonymous (guest) [#anonymous-guest] Creates a session without any identifier. The account screen detects guest accounts and offers a **Create your account** form that attaches an email and password to the same user with `account.updateEmail()`, so any data they created stays with them. ## OAuth2 [#oauth2] Each provider you list renders a "Continue with ..." button with a brand icon for the common providers and a generic one for the rest. Clicking it redirects to the provider using Appwrite's token endpoint and returns to your `redirectUrl`, where Auth UI exchanges the token for a session. Configure the provider's client ID and secret in the Appwrite Console under **Auth** → **Settings**, and add the callback URL Appwrite shows there to the provider's own configuration. Signed-in users can connect additional providers from the **Connections** tab of the account screen and disconnect them again. Third-party apps the user has authorized appear under **Authorized apps** (OAuth2 consents) when the server exposes `/account/consents`. ## Google One Tap [#google-one-tap] Optional auto-prompt on signed-out **sign-in** and **sign-up** mounts. Enable with `oneTap: true` and pass your Google OAuth **Web** client ID as `googleClientId` (from Google Cloud Console → Credentials). Appwrite stores Google provider secrets server-side and does not expose them to the browser, so the client ID must be set explicitly. ```html ``` Auth UI loads `https://accounts.google.com/gsi/client` once, generates a cryptographically random nonce per prompt, passes it to `google.accounts.id.initialize`, and on credential forwards the ID token plus the same raw nonce into `createIdTokenSession({ provider: "google", idToken, nonce })` (required by Appwrite when the JWT carries a nonce claim). The token and nonce are discarded after the session call. Keep `oauth:google` as a visible fallback: One Tap is passive and browsers may hide it (FedCM cool-down, dismissed, third-party cookies). Soft-fails never break email/password or OAuth buttons. Requires project via Client (`appwrite@28` ships native `createIdTokenSession` with `X-Appwrite-Project`; REST fallback still sends it for older SDKs). **CSP:** allow `https://accounts.google.com` in `script-src` and `frame-src` (and often `connect-src`). See [Security notes](/docs/guides/security). **Manual QA:** signed-out mount with Google account in the browser → One Tap chrome appears → credential creates an Appwrite session. Dismiss or block GIS → form still works. Missing `googleClientId` → console warning, no prompt. ## Native ID token (bridges) [#native-id-token-bridges] For Capacitor, WebView or custom One Tap bridges, call `authStore.createIdTokenSession({ provider, idToken, nonce?, accessToken?, accessTokenExpiry?, name? })` after the native SDK returns an OIDC JWT. The built-in `oneTap` attribute covers the browser GIS prompt; use this API for native shells. Soft-detects missing routes on older Appwrite. ## Identifier-first sign-in [#identifier-first-sign-in] When `identifierFirst` is true (attribute `identifier-first`), sign-in shows **email → Continue → password** instead of both fields at once. OAuth, magic URL, email OTP, phone and guest stay on step 1. The password step shows the email (read-only), password, Sign in, and a "Use a different email" control. Sign-up keeps email and password together so account creation stays a single form. Pure client progressive disclosure; no Appwrite user-lookup API. ## MFA [#mfa] Not a method on its own. When an account has MFA enabled, every method above is followed by a second-factor challenge. See [Multi-factor authentication](/docs/mfa). # Theming (/docs/theming) Auth UI follows the Appwrite Console design system: the shadcn "new-york" zinc palette, a 10px radius scale, system UI font and the console's focus ring. Every colour is a CSS custom property on the component host, so you can restyle it from your own stylesheet without touching the shadow DOM. ## Dark mode [#dark-mode] `branding.theme` defaults to `auto`, which resolves in this order: 1. `html.dark` or `html.light` class (what next-themes, shadcn and the Appwrite Console use) 2. `data-theme="dark"` or `"light"` on `` 3. `prefers-color-scheme` Set `theme: "light"` or `"dark"` to force a scheme. Components watch for class changes, so a theme toggle in your app switches Auth UI live. ## Tokens [#tokens] Override any token on the elements you want to change, or on `:root` to affect all of them: ```css authui-modal, authui-sign-in, authui-account, authui-button, authui-user-button { --authui-primary: #fd366e; --authui-primary-foreground: #ffffff; --authui-radius: 0.875rem; --authui-font: "Inter", system-ui, sans-serif; } ``` | Token | Purpose | | --------------------------------------------------------------------------------- | ----------------------------------------------------- | | `--authui-background`, `--authui-foreground` | Dialog surface and default text | | `--authui-card`, `--authui-card-foreground` | Panel surface | | `--authui-popover`, `--authui-popover-foreground` | User menu | | `--authui-primary`, `--authui-primary-foreground` | Primary buttons and switches | | `--authui-secondary`, `--authui-secondary-foreground` | Passwordless buttons | | `--authui-muted`, `--authui-muted-foreground` | Avatars, hints, descriptions | | `--authui-accent`, `--authui-accent-foreground` | Hover states | | `--authui-destructive`, `--authui-destructive-foreground` | Danger actions | | `--authui-border`, `--authui-input`, `--authui-ring` | Borders, inputs, focus ring | | `--authui-brand`, `--authui-brand-foreground` | The `brand` button variant (Appwrite pink by default) | | `--authui-success-*`, `--authui-warning-*`, `--authui-error-*`, `--authui-info-*` | Alert and badge tints | | `--authui-radius` | Base radius; `sm`, `md`, `lg`, `xl` derive from it | | `--authui-font`, `--authui-font-mono` | Typography | | `--authui-shadow-lg` | Panel and dialog shadow | | `--authui-overlay` | Modal backdrop colour | | `--authui-z` | Stacking order of the dialog and menus | The `primary` and `radius` branding options are shortcuts that set `--authui-primary` and `--authui-radius` for you. ## Parts [#parts] The main panels expose `part="panel"` and the trigger button exposes `part="button"` for `::part()` styling: ```css authui-sign-in::part(panel) { box-shadow: none; } ``` ## Locale [#locale] Set `locale` (attribute `locale`) to a BCP-47 short tag. Auth UI ships English defaults plus packs for **cs**, **de** and **fr**. Unknown tags fall back to English. Missing keys inside a pack also fall back to English. Merge order: **English defaults ← locale pack ← `strings` override**. ```html ``` ```ts AuthUI.init({ // ... locale: "de", strings: { signIn: "Einloggen" }, // wins over the German pack for this key }); ``` ### Adding a pack [#adding-a-pack] 1. Add `packages/core/src/locales/.ts` exporting a full or partial `AuthUIStrings` object (reuse placeholders like `{provider}`). 2. Register it in `packages/core/src/locales/index.ts` (`localePacks`). 3. Keep tone Concise and avoid em dashes. `supportedLocales`, `localePacks`, `resolveLocale`, `getLocalePack` and `mergeStrings` are exported from `@getauthui/core` for tooling and tests. ## Strings [#strings] Every visible string can be replaced through `strings`. Placeholders in braces are substituted at render time. Overrides apply after the locale pack. ```ts AuthUI.init({ // ... strings: { signIn: "Log in", signInTitle: "Log in to {name}", continueAsGuest: "Browse without an account", errorInvalidCredentials: "That email and password do not match.", }, }); ``` The full list of keys with their defaults is exported as `defaultStrings` from `@getauthui/core`. # AuthUI (/docs/api/authui) ```ts import { AuthUI } from "@getauthui/core"; ``` Every function is also exported by name: `import { init, open, on } from "@getauthui/core"`. | Member | Signature | Description | | --------------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `init` | `(config: AuthUIConfig) => typeof AuthUI` | Configure the Appwrite client, handle any pending redirect in the URL and load the current user. | | `open` | `(view?: AuthUIView) => void` | Open the modal. Creates `` on `` if none exists. | | `close` | `() => void` | Close the modal. | | `signOut` | `() => Promise` | End the current session. | | `getUser` | `() => Models.User \| null` | The signed-in user. | | `getState` | `() => AuthUIState` | A snapshot of the store. | | `getClient` | `() => Client \| null` | The configured Appwrite `Client`, for reuse with other services. | | `getAccount` | `() => Account \| null` | The Appwrite `Account` service Auth UI signs in with. Reuse it for `getPrefs()`, `updatePrefs()`, JWTs and so on. In preview mode it is the in-memory stand-in. | | `previewSignIn` | `() => Promise` | Preview mode only: sign the sample user in without a form. | | `on` | `(event, listener) => () => void` | Subscribe to an [event](/docs/api/events). Returns an unsubscribe function. | | `store` | `AuthStore` | The full [store](/docs/api/store). | ## The Appwrite SDK [#the-appwrite-sdk] The package re-exports the Appwrite Web SDK it is built on as the `appwrite` namespace, so script-tag users can reach every service without loading the SDK a second time: ```ts import { AuthUI, appwrite } from "@getauthui/core"; const { Storage, Databases, Query, ID, Permission, Role } = appwrite; const storage = new Storage(AuthUI.getClient()); // already signed in ``` With npm the SDK is a peer dependency, so `import { Storage } from "appwrite"` works just as well. ```ts AuthUI.init({ endpoint: "https://cloud.appwrite.io/v1", project: "YOUR_PROJECT_ID" }); AuthUI.on("signed-in", (user) => console.log(user.email)); document.querySelector("#login")!.addEventListener("click", () => AuthUI.open()); ``` # Events (/docs/api/events) ## Store events [#store-events] Subscribe with `AuthUI.on(name, listener)`. Each is also dispatched on `window` as a `CustomEvent` named `authui:` with the same `detail`. | Name | Detail | When | | ------------- | ------------------------- | --------------------------------------------------------------------------------- | | `change` | `AuthUIState` | Any state change: status, user, factors, pending action. | | `signed-in` | `Models.User` | Status became `signed-in`. | | `signed-out` | `undefined` | The user signed out or the session ended. | | `error` | `{ message, type, code }` | Any store operation failed. Emitted before the promise rejects. | | `active-team` | `{ teamId, team }` | Active team changed via `setActiveTeam` or the teams switcher on the user button. | ```ts window.addEventListener("authui:change", (e) => console.log((e as CustomEvent).detail.status)); ``` ## Window commands [#window-commands] Dispatch these to control the modal without a reference to it. `AuthUI.open()` and `AuthUI.close()` do exactly this. | Name | Detail | | -------------- | ----------------------- | | `authui:open` | `{ view?: AuthUIView }` | | `authui:close` | none | ## Component events [#component-events] All bubble and are composed. | Element | Event | Detail | | ---------------------- | -------------------- | --------------------- | | `` | `authui-success` | `{ method }` | | `` | `authui-view` | `{ view }` | | `` | `authui-open` | `{ view: "account" }` | | `` | `authui-signed-in` | `Models.User` | | `` | `authui-close` | none | | `` | `authui-menu-action` | `{ actionId }` | | `` | `authui-active-team` | `{ teamId, team }` | `authui-menu-action` fires when a custom `menuItems` row with `actionId` is clicked. `authui-active-team` fires on the element when the teams switcher picks a team (with `show-teams`). The store also emits `active-team` (and `authui:active-team` on `window`) for the same change; subscribe with `AuthUI.on("active-team", ...)` for the store-level event, or listen on the element for the component event. # AuthStore (/docs/api/store) ```ts import { authStore } from "@getauthui/core"; ``` All methods reject with the original `AppwriteException` and emit an `error` event before rethrowing. Use `describeError(err, authStore.getStrings())` for a user-facing message. ## State [#state] | Method | Returns | | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `configure(config)` | `void`. Also runs `handleRedirect()` then `refresh()`. | | `getState()` | `AuthUIState` | | `getConfig()` | `AuthUIConfig \| null` | | `getClient()` | `Client \| null` | | `getAccount()` | `Account \| null`, the service every method above calls | | `getStrings()` | merged `AuthUIStrings` | | `refresh()` | `Promise`. Re-fetches the user and derives `status`. | | `on(event, listener)` | unsubscribe function | | `getActiveTeamId()` | `string \| null`. Local active team id for the project. | | `setActiveTeam(team)` | `void`. Remember `{ $id, name }` or `null`; emits `active-team`. Does not call Appwrite. | | `listTeams()` | `Promise`. Teams the signed-in user belongs to; empty when signed out or in preview. What `show-teams` on the user button uses. | | `createTeam(name)` | `Promise`. Creates a team; the signed-in user becomes owner. | | `listTeamMemberships(teamId)` | `Promise`. Members of a team. | | `createTeamMembership(teamId, email, roles?)` | Invite by email. Redirect URL uses `authui=team-invite`. | | `deleteTeamMembership(teamId, membershipId)` | Leave or remove a member. | | `leaveTeam(teamId)` | Leave as the signed-in user. | | `acceptTeamInvite(teamId, membershipId, userId, secret)` | Accept invite via `teams.updateMembershipStatus` (also opens a session). | | `listConsentTokens(consentId)` / `deleteConsentToken(consentId, tokenId)` | Per-device token families under an OAuth2 consent. | | `previewSignIn()` | `Promise`. Preview mode only: sign the sample user in without a form. Also on `AuthUI`. | | `notifyConfigIncomplete()` | `void`. Called by `` when endpoint or project is missing; clears loading and sets `configError`. | | `reset()` | `void`. Test helper: clear client, config, state and listeners. | ## Sign in [#sign-in] | Method | Description | | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | `signInWithEmailPassword(email, password)` | `createEmailPasswordSession` | | `signUp(email, password, name?)` | `create` then `createEmailPasswordSession` | | `signInAnonymously()` | `createAnonymousSession` | | `signInWithOAuth(provider)` | `createOAuth2Token`; navigates away | | `sendMagicUrl(email)` | `createMagicURLToken`; returns the token with its `phrase` | | `sendEmailOtp(email)` | `createEmailToken` | | `sendPhoneOtp(phone)` | `createPhoneToken` | | `signInWithToken(userId, secret)` | `createSession`; redeems any token | | `createIdTokenSession({ provider, idToken, nonce?, accessToken?, accessTokenExpiry?, name? })` | Native OIDC ID token session (Capacitor / WebView / One Tap bridges). Soft-detects missing routes. Not a sign-in button. | | `signOut(sessionId = "current")` | `deleteSession` | | `signOutEverywhere()` | `deleteSessions` | ## MFA [#mfa] | Method | Description | | ---------------------------------------------------------------------------- | ------------------------------------------------------ | | `listMfaFactors()` | factors available to the account | | `createMfaChallenge(factor)` | `factor` is `totp`, `email`, `phone` or `recoverycode` | | `completeMfaChallenge(challengeId, otp, { signIn })` | verify; with `signIn: false` it acts as a step-up only | | `setMfaEnabled(enabled)` | `updateMFA` | | `addAuthenticator()` | returns `{ secret, uri }` | | `verifyAuthenticator(otp)` | activates the authenticator | | `removeAuthenticator()` | needs a recent challenge | | `createRecoveryCodes()` / `getRecoveryCodes()` / `regenerateRecoveryCodes()` | the last two need a recent challenge | ## Recovery and verification [#recovery-and-verification] | Method | Description | | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `sendPasswordRecovery(email)` | prefers recovery OTP (`createRecoveryOTP`); falls back to a reset link when the route is missing. Returns `{ mode: "otp", token }` or `{ mode: "link" }` | | `completePasswordRecovery(userId, secret, password, { otp? })` | sets the new password via `updateRecoveryOTP` when `otp: true`, otherwise `updateRecovery` | | `sendEmailVerification()` | prefers email verification OTP; falls back to a verification link. Returns `{ mode: "otp", token }` or `{ mode: "link" }` | | `confirmEmailVerification(otp)` | `updateEmailVerificationOTP` | | `sendPhoneVerification()` / `confirmPhoneVerification(otp)` | SMS verification | ## Account [#account] | Method | Description | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | | `updateName(name)`, `updateEmail(email, password)`, `updatePhone(phone, password)`, `updatePassword(password, oldPassword?)` | profile updates | | `convertGuest(email, password, name?)` | upgrade an anonymous account | | `listSessions()`, `listIdentities()`, `deleteIdentity(id)`, `listLogs()`, `listConsents()`, `deleteConsent(id)`, `listConsentTokens(consentId)`, `deleteConsentToken(consentId, tokenId)` | lists | | `deleteAccount()` | `updateStatus`; blocks the account and signs out | ## Redirects [#redirects] | Method | Description | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `redirectUrl(action)` | the URL registered with Appwrite for a flow, with the `authui` marker | | `handleRedirect()` | finish a flow present in `location.search` (including team invites with or without `authui=team-invite`); returns `true` if one was handled | | `setPending(action)` | set or clear the pending action shown by the UI | # Types (/docs/api/types) ```ts import type { AuthUIConfig, AuthUIState, AuthUIView, AuthUIStatus, AuthUIStrings, OAuthProviderName, } from "@getauthui/core"; ``` ## `AuthUIConfig` [#authuiconfig] See [Configuration](/docs/configuration). ## `AuthUIState` [#authuistate] ```ts interface AuthUIState { status: "loading" | "signed-out" | "signed-in" | "mfa-required"; user: Models.User | null; mfaFactors: Models.MfaFactors | null; pending: AuthUIPendingAction | null; configured: boolean; configError: string | null; } ``` ## `AuthUIPendingAction` [#authuipendingaction] ```ts type AuthUIPendingAction = | { type: "reset-password"; userId: string; secret: string } | { type: "verify-email"; userId: string; secret: string } | { type: "oauth-failed" } | { type: "notice"; message: string; tone: "success" | "error" | "info" }; ``` ## `AuthUIView` [#authuiview] `"sign-in" | "sign-up" | "forgot-password" | "reset-password" | "magic-url" | "email-otp" | "phone" | "mfa" | "account"` ## `OAuthProviderName` [#oauthprovidername] Every provider slug Appwrite accepts, including `google`, `github`, `apple`, `microsoft`, `discord`, `facebook`, `x`, `linkedin`, `slack`, `gitlab`, `bitbucket`, `twitch`, `spotify`, `notion`, `dropbox`, `figma`, `oidc`, `okta`, `auth0`, `amazon`, `paypal`, `zoom`, and the rest of the list in Appwrite's `OAuthProvider` enum. ## `AuthUIStrings` [#authuistrings] All UI strings. The defaults are exported as `defaultStrings`. Values may contain `{placeholders}`: | Key | Placeholders | | -------------------------------- | ------------ | | `signInTitle`, `signUpTitle` | `{name}` | | `continueWith` | `{provider}` | | `magicLinkSent`, `resetLinkSent` | `{email}` | | `codeSent` | `{target}` | ## Helpers [#helpers] ```ts import { describeError, toAuthUIError, ErrorTypes, providerLabel, defaultStrings, FOUC_CSS, CRITICAL_FOUC_CSS, scorePassword, isConfigError, PHONE_COUNTRIES, defaultPhoneCountryIso, digitsOnly, flagEmoji, getPhoneCountry, parsePhone, toE164, type ParsedPhone, type PhoneCountry, getLastMethod, rememberLastMethod, clearLastMethod, getStoredActiveTeamId, setStoredActiveTeamId, getLocalePack, localePacks, resolveLocale, supportedLocales, mergeStrings, loadGoogleIdentityServices, promptGoogleOneTap, resetOneTapPromptState, } from "@getauthui/core"; ``` * `describeError(err, strings, context?)` maps an Appwrite error to a friendly string. Pass `"code"` as `context` when the user typed a one-time code. * `toAuthUIError(err)` normalises anything thrown into `{ message, type, code }`. * `ErrorTypes` lists the Appwrite error `type` strings the UI reacts to. * `isConfigError(err)` returns whether an error points to a wrong project, origin or endpoint, or a dead network on first contact. * `providerLabel(slug)` returns the display name of an OAuth provider. * `FOUC_CSS` / `CRITICAL_FOUC_CSS` are the critical CSS strings the in-module FOUC guard uses. Prefer the shipped stylesheet `@getauthui/core/fouc.css` (CDN: `dist/fouc.css`) in `` before the Auth UI script (see [``](/docs/components/show)). * `scorePassword(password)` returns `{ level, percent, labelKey, checks }` for the live strength meter (client-only). * `PHONE_COUNTRIES` is the country list used by the phone picker. `defaultPhoneCountryIso(lang?)` selects a country from a BCP 47 language tag, `getPhoneCountry(iso)` looks one up, and `flagEmoji(iso)` returns its flag emoji. * `digitsOnly(value)` strips non-digits. `toE164(iso, national)` builds an E.164 number, and `parsePhone(input, fallbackIso?)` splits one into `{ iso, national, e164 }` (`ParsedPhone`), using `PhoneCountry` entries for country data. * `getLastMethod()` / `rememberLastMethod(method)` / `clearLastMethod()` read and write `localStorage` key `authui:last-method` (what the sign-in form highlights as last used). * `getStoredActiveTeamId(project)` / `setStoredActiveTeamId(project, teamId)` read and write `localStorage` key `authui:active-team:`. Prefer `AuthUI.store.getActiveTeamId()` / `setActiveTeam()` in app code; these are the underlying helpers. * `supportedLocales` lists built-in packs (`cs`, `de`, `fr`). `localePacks` holds the partial string maps. `resolveLocale(tag)` normalises a BCP-47 tag to a pack key (or `null`). `getLocalePack(tag)` returns the pack or `null`. `mergeStrings(defaults, locale, overrides)` applies English defaults ← locale pack ← user `strings`. * `loadGoogleIdentityServices()` loads the GIS script once. `promptGoogleOneTap(options)` shows Google One Tap when configured. `resetOneTapPromptState()` clears the in-session prompt guard (useful in tests). # (/docs/components/account) ```html ``` Renders the tabs described in [Account management](/docs/account-management). If nobody is signed in it renders `` instead, so the same element can sit on an account page. ## Attributes and properties [#attributes-and-properties] | Name | Type | Default | Description | | ---------- | ----------------------------------------------------------------------------------------------------- | ----------- | --------------------------------------------------------------------------------------- | | `tab` | `"profile"` / `"security"` / `"sessions"` / `"connections"` / `"consents"` / `"teams"` / `"activity"` | `"profile"` | Initial tab. `consents` and `activity` hide themselves when the server lacks the route. | | `embedded` | `boolean` | `false` | Removes the card chrome. The modal sets this. | ## Parts [#parts] `::part(panel)` targets the card. ## Sizing [#sizing] The host is `display: block` with `max-width: 560px`. # (/docs/components/button) ```html Sign in Create account Account ``` ## Attributes [#attributes] | Name | Type | Default | Description | | --------- | ---------------------------------------------------------------------------- | ----------- | ----------------------------------------------------------------------------- | | `view` | screen name | `"sign-in"` | Which screen the modal opens on. | | `variant` | `"primary"` / `"brand"` / `"outline"` / `"secondary"` / `"ghost"` / `"link"` | `"primary"` | Visual style, matching the console button variants. `brand` is Appwrite pink. | | `size` | `"sm"` / `"md"` / `"lg"` | `"md"` | Height 32, 36 or 40 px. | ## Slots [#slots] The default slot is the label. Without content it falls back to "Sign in", "Sign up" or "Manage account" depending on `view`. ## Parts [#parts] `::part(button)` targets the inner ` ``` ## Teams switcher [#teams-switcher] Set `show-teams` to list the signed-in user's Appwrite teams in the menu and let them pick an active team. The choice is stored in `localStorage` (`authui:active-team:`) and Auth UI emits `authui:active-team` on `window` (and `authui-active-team` on the element) with `{ teamId, team }`. Use **Manage teams** in the menu (or the Teams tab on the account screen) to create teams, invite members, and leave teams. Invite emails return through Auth UI's redirect handler (`authui=team-invite`). ```html ``` # Keep your 2048 best score (/docs/guides/2048) [2048](https://github.com/gabrielecirulli/2048) keeps its best score in `localStorage`, which lives and dies with one browser. In this guide you add Auth UI to it and mirror the best score into the signed-in user's Appwrite preferences. About thirty lines of code, no build step, no server of your own. What you end up with: a "Sign in" button under the board, a best score that uploads whenever it improves, and the higher of the two scores winning when you sign in on another device. Auth UI Sign in to 2048 modal over the classic game board 2048 after sign in with the Account button under the board ## Clone 2048 [#clone-2048] ```bash git clone https://github.com/gabrielecirulli/2048.git cd 2048 ``` Open `index.html` in a browser. The game is plain HTML and JavaScript, so there is nothing to install. ## Create an Appwrite project [#create-an-appwrite-project] Follow the Appwrite quick start up to the point where you have a project and a **Web platform**: 1. [Create a project](https://appwrite.io/docs/quick-starts/web#step-1) on Appwrite Cloud or your own instance. 2. Add a Web platform with hostname `localhost` for development. 3. **Email/Password** under **Auth → Settings** is enabled by default. Leave it on. Note the project ID and the API endpoint from the Console. ## Add the sign-in button [#add-the-sign-in-button] Below the "New Game" row in `index.html` there is room for one more row. Reuse the game's own `restart-button` style so it fits in. Put this in `` so the buttons stay hidden until Auth UI loads (see [``](/docs/components/show)): ```html title="index.html" ``` Below the "New Game" row: ```html title="index.html"

Sign in to keep your best score on every device.

``` [``](/docs/components/show) renders its children only while the auth state matches, so the row shows "Sign in" or "Account", never both.
## Configure Auth UI [#configure-auth-ui] Add a module script after the game's own scripts at the bottom of `index.html`: ```html title="index.html" ``` Reload, click "Sign in", and create an account with any email and password. The button turns into "Account". ## Upload the best score when it improves [#upload-the-best-score-when-it-improves] The game writes the best score through `LocalStorageManager.prototype.setBestScore`, which `GameManager` calls after every move that beats the record. Wrap it so it also calls [`account.updatePrefs()`](https://appwrite.io/docs/references/cloud/client-web/account#updatePrefs). A one second delay avoids one request per move during a good run. Add this inside the same module script: ```js title="index.html" var account = AuthUI.getAccount(); var timer; var setBestScore = LocalStorageManager.prototype.setBestScore; LocalStorageManager.prototype.setBestScore = function (score) { setBestScore.call(this, score); if (!AuthUI.getUser()) return; clearTimeout(timer); timer = setTimeout(function () { account.updatePrefs({ bestScore: Number(score) }); }, 1000); }; ``` `AuthUI.getAccount()` returns the Appwrite `Account` service Auth UI signs in with, so the request is already authenticated. Patching the prototype works even though the game creates its `LocalStorageManager` before this script runs, because methods are looked up at call time. ## Restore it on sign in [#restore-it-on-sign-in] When a user signs in, compare the score on the account with the one in this browser and keep the higher. `signed-in` fires on every page load that finds a session, not only after the form, so a returning player always sees their real best. ```js title="index.html" AuthUI.on("signed-in", async function () { var prefs = await account.getPrefs(); var remote = Number(prefs.bestScore || 0); var local = Number(localStorage.getItem("bestScore") || 0); if (remote > local) { localStorage.setItem("bestScore", remote); document.querySelector(".best-container").textContent = remote; } else if (local > remote) { await account.updatePrefs({ bestScore: local }); } }); ``` The game reads `bestScore` from `localStorage` on the next move, so writing it there is enough. Updating `.best-container` shows the number right away instead of after the first move. ## Try it [#try-it] Auth UI account screen opened from 2048 1. Play a few moves, then sign up. Your best score is uploaded. In the Console it is visible under **Auth → Users → your user → Prefs**. 2. Open `index.html` in a private window and sign in. The best score appears before you make a move. 3. Beat it there, come back, reload. The new record is waiting.
## Where to go from here [#where-to-go-from-here] * **Save the board too.** The game also stores `gameState` through `setGameState()` and `clearGameState()`. Wrap them the same way to resume a game on another device. It is a few hundred bytes of JSON, far under the 64 KB preferences limit. * **Add a leaderboard.** Preferences are private to each user. For a public leaderboard, write scores to an Appwrite database table with read access for everyone and create access for users. * **Sign in with one tap.** `methods: { emailPassword: true, oauth: ["google"] }` adds a Google button with no other changes. This guide follows the happy path. `updatePrefs()` replaces all preferences on the user, so if you store more than the score, send the whole object each time. # AI agents (/docs/guides/ai-agents) Auth UI has an HTML-first API that agents write correctly on the first try. Point your agent at the machine-readable docs: Working Auth UI chrome with Sign in and Sign up buttons from the recommended snippet Auth UI sign-in modal produced by the agent-friendly HTML snippet * [`/llms.txt`](/llms.txt) is a short index. * [`/llms-full.txt`](/llms-full.txt) is the complete documentation in one file. * Every docs page has a Markdown version at `/llms.mdx/docs//content.md`. ## Prompt for humans [#prompt-for-humans] Copy this into your AI assistant along with your endpoint and project ID: ```text Add authentication to this web app with Auth UI (@getauthui/core), a web component library for Appwrite. Docs: https://getauthui.appwrite.network/llms-full.txt 1. Put critical CSS in : then load it: (or `npm i @getauthui/core appwrite lit` and `import "@getauthui/core"`). 2. Configure it once with . 3. Put Sign in inside and inside . 4. Do not build custom login forms. Do not store sessions yourself; Appwrite's SDK handles the session and any `new Client()` in the app sees it. 5. Register the app hostname as a Web platform in the Appwrite Console. ``` ## Instructions for AI agents [#instructions-for-ai-agents] Add this to your system prompt, `CLAUDE.md`, or agent instructions: ```text When adding authentication to a web app, use Auth UI (@getauthui/core) for Appwrite. Docs: https://getauthui.appwrite.network/llms-full.txt. Prefer , , , and . Do not build custom login forms or store sessions yourself. ``` ## Rules of thumb for agents [#rules-of-thumb-for-agents] * Use the declarative `` element unless the config has to be computed. * Everything reads global state. One configuration, any number of elements. * Use `` and `` for conditional UI instead of reading state in JavaScript. `unless="signed-in"` covers everyone without a complete session. * For CDN installs, include the critical CSS in `` so protected content does not paint before the module runs. See [``](/docs/components/show). * Never pass the Appwrite session or a JWT through URLs. Auth UI handles all redirects. * To react to sign in, call `AuthUI.on("signed-in", cb)` or set `successUrl`. * Watch for the traps in [Common mistakes](/docs/common-mistakes) (missing `/v1`, unregistered platform, ESM vs IIFE). ## Agent skill [#agent-skill] This repo ships an Auth UI creator skill at [`skills/authui-creator/SKILL.md`](https://github.com/Meldiron/getauthui/blob/main/skills/authui-creator/SKILL.md). Point coding agents that support Agent Skills at that file (or paste its contents) so they load copy-paste patterns and the "Tips for AI-Generated Code" checklist. Also skim [Common mistakes](/docs/common-mistakes) before inventing setup code. # Guides (/docs/guides) Each guide starts from an existing project, adds Auth UI, and ends with something you can deploy. They stick to the happy path on purpose. The [docs](/docs) cover every option and edge case. Collage of Auth UI sign-in modals from WeekToDo, 2048, Nullboard and OpenHabitTracker # Migrating from Auth UI v1 (/docs/guides/migrating-from-v1) Auth UI v1 was a hosted page at `.authui.site` that you linked to. Version 2 runs inside your own page. The move takes a few minutes. Embedded Auth UI Sign in button on your own origin Auth UI sign-in modal running inside the app instead of a hosted authui.site page ## Why the change [#why-the-change] Browsers block or partition third-party cookies. A session created on `authui.site` could not be read from your app on another domain, so sign in silently failed in Safari, Firefox, Brave and Chrome with tracking protection on. Running on your origin removes the cross-site hop entirely. Read [How it works](/docs/how-it-works) for the details. ## Mapping v1 settings [#mapping-v1-settings] | v1 page setting | v2 equivalent | | --------------------------------------------------- | ----------------------------------------------------------------------------------------- | | Provider endpoint and project | `endpoint`, `project` | | Success URL | `successUrl` | | Failure URL | not needed; errors render inline | | Domain (`name.authui.site`) | not needed; your app's own domain is the platform | | Allow guest, magic URL, email OTP, phone | `methods.anonymous`, `methods.magicUrl`, `methods.emailOtp`, `methods.phone` | | Allow Google, GitHub, Twitter, Facebook | `methods.oauth: ["google", "github", "x", "facebook"]` | | Allow sign up | `signUp` | | Name, logo, brand colour, border radius, dark theme | `branding.name`, `branding.logo`, `branding.primary`, `branding.radius`, `branding.theme` | | Privacy policy, terms of service | `legal.privacyUrl`, `legal.termsUrl` | ## Steps [#steps] 1. In the Appwrite Console, add your app's hostname as a Web platform. You can remove the `*.authui.site` platform afterwards. 2. Add the script tag or install the package. 3. Replace the link to `https://name.authui.site/` with `Sign in`. 4. If you used the v1 URL as a sign-out link, use `` or `AuthUI.signOut()` instead. 5. Delete the v1 page from your Auth UI dashboard. ## What you gain [#what-you-gain] MFA, account management, sessions, connected accounts, every OAuth provider, theming tokens, React wrappers, and a login flow that works in every browser. # Sync Nullboard across devices (/docs/guides/nullboard) [Nullboard](https://github.com/apankrat/nullboard) is a single-file kanban board that stores everything in the browser's `localStorage`. That makes it fast and private, and it also means a cleared browser or a second laptop starts from nothing. In this guide you add Auth UI to it and mirror the boards into the signed-in user's Appwrite preferences. About forty lines of code, no build step, no backend of your own. What you end up with: a "Sign in to sync..." entry in Nullboard's menu, boards that upload after every change, and boards that appear on any device where you sign in. Auth UI Sign in to Nullboard modal over the kanban board Nullboard with an active board after signing in through Auth UI ## Clone Nullboard [#clone-nullboard] ```bash git clone https://github.com/apankrat/nullboard.git cd nullboard ``` Everything lives in `nullboard.html`. Open it in a browser and you have a working board. Keep that tab around to compare later. ## Create an Appwrite project [#create-an-appwrite-project] Follow the Appwrite quick start up to the point where you have a project and a **Web platform**: 1. [Create a project](https://appwrite.io/docs/quick-starts/web#step-1) on Appwrite Cloud or your own instance. 2. Add a Web platform with hostname `localhost` for development. You will add your real hostname the same way when you deploy. 3. Make sure **Email/Password** is enabled under **Auth → Settings**. It is on by default. Note the project ID and the API endpoint shown in the Console, for example `https://fra.cloud.appwrite.io/v1`. You need both in the next step. ## Add Auth UI to the menu [#add-auth-ui-to-the-menu] Nullboard's hamburger menu at the top right is a list of links inside `
`. Add two entries right after "Add new board...": Put critical CSS in `` so the menu entries stay hidden until Auth UI loads: ```html title="nullboard.html" ``` Then add the menu entries: ```html title="nullboard.html" Add new board... ``` [``](/docs/components/show) renders its children only for the matching state, so the menu shows one entry or the other. Plain links keep Nullboard's own menu styling. ## Configure and wire up sign in [#configure-and-wire-up-sign-in] Nullboard's own code is a classic ` ``` Module scripts run after the page and Nullboard's script have loaded, so `$` and `NB` are available. `AuthUI.open()` creates the modal on first use. Reload the page, open the menu, and sign up with any email and password. The menu entry turns into "Account...". ## Upload boards after every change [#upload-boards-after-every-change] Nullboard keeps boards in `NB.storage`. Two methods matter: `saveBoard()` runs on every edit and `nukeBoard()` on delete. Wrap both so they also upload, and use [`account.updatePrefs()`](https://appwrite.io/docs/references/cloud/client-web/account#updatePrefs) to store the boards on the user. Add this inside the same module script: ```js title="nullboard.html" const account = AuthUI.getAccount(); const SYNCED = "nullboard.synced"; let restoring = false; let timer; // All boards in Nullboard's own export format, plus a timestamp. function snapshot() { const boards = []; NB.storage.getBoardIndex().forEach((meta, id) => boards.push(NB.storage.loadBoard(id, null))); return { savedAt: Date.now(), boards }; } // Upload at most once per second, only while signed in. function scheduleUpload() { if (restoring || !AuthUI.getUser()) return; clearTimeout(timer); timer = setTimeout(async () => { const data = snapshot(); await account.updatePrefs({ nullboard: data }); localStorage.setItem(SYNCED, data.savedAt); }, 1000); } // Every save or delete inside Nullboard now also uploads. for (const method of ["saveBoard", "nukeBoard"]) { const original = NB.storage[method]; NB.storage[method] = function (...args) { const result = original.apply(this, args); scheduleUpload(); return result; }; } ``` `AuthUI.getAccount()` returns the same Appwrite `Account` service Auth UI signs in with, so the call is already authenticated. `snapshot()` uses `loadBoard()`, which returns the current revision of each board without the undo history, keeping the payload small. Preferences hold up to 64 KB, which is plenty for boards. ## Restore boards on sign in [#restore-boards-on-sign-in] The last piece pulls boards down. When a user signs in, compare the timestamp stored in preferences with the one this device last synced. If another device saved more recently, import those boards and reload. If there is nothing in preferences yet, upload what is here. ```js title="nullboard.html" // Replace local boards with the ones from Appwrite, then reload to show them. function restore(data) { restoring = true; for (const board of data.boards) { board.revision--; // saveBoard() adds one back NB.storage.saveBoard(board); // updates the board index too } if (data.boards.length) NB.storage.setActiveBoard(data.boards[0].id); localStorage.setItem(SYNCED, data.savedAt); location.reload(); } AuthUI.on('signed-in', async () => { const prefs = await account.getPrefs(); const remote = prefs.nullboard; const local = Number(localStorage.getItem(SYNCED) || 0); if (remote && remote.savedAt > local) restore(remote); else if (!remote) scheduleUpload(); }); ``` The `revision--` trick is the same one Nullboard's own import uses: `saveBoard()` increments the revision, so this keeps the numbers matching. `signed-in` fires on every page load where a session exists, not only after the form, so a returning user gets the latest boards automatically. ## Try it [#try-it] Auth UI account screen opened from Nullboard 1. Open `nullboard.html`, sign up, and add a note to the demo board. A second later the boards are in your preferences. You can see them in the Console under **Auth → Users → your user → Prefs**. 2. Open the same file in a private window, or on another machine. Sign in. The page reloads with your boards. 3. Change something there, go back to the first window and reload. The change is waiting for you. ## Where to go from here [#where-to-go-from-here] * **Deploy it.** Nullboard is static, so any host works. Add the deployed hostname as a Web platform in Appwrite, then [Appwrite Sites](https://appwrite.io/docs/products/sites) is a one-command deploy. * **Add sign-in methods.** `methods: { emailPassword: true, magicUrl: true, oauth: ["github"] }` gives you passwordless and GitHub sign in with no other changes. See [Sign-in methods](/docs/sign-in-methods). * **Match the theme.** Nullboard toggles a `theme-dark` class on ``. Pass `branding: { theme: "dark" }` when it is set, or leave `auto` to follow the system. This guide follows the happy path. Two devices editing the same board at the same moment means the later upload wins, and deleting a board on one device leaves it on devices that already have it. Both are fine for a personal board and easy to refine once you need to. # Back up OpenHabitTracker to Appwrite (/docs/guides/openhabittracker) [OpenHabitTracker](https://github.com/Jinjinov/OpenHabitTracker) is a habit, task and note tracker written in C# with Blazor. Its [PWA](https://pwa.openhabittracker.net) keeps everything in the browser's IndexedDB, with a manual JSON export as the only way out. In this guide you add Auth UI to the PWA, put "Back up" and "Restore" buttons next to it, and store the backup as a private file in Appwrite Storage. One JavaScript file, one Razor component, no server code. Backups grow past the 64 KB that user preferences allow as soon as a habit has a year of history, which is why this guide uses Storage. Preferences remain the right place for small values, as in the [2048 guide](/docs/guides/2048). Auth UI Sign in to OpenHabitTracker modal over the habit tracker nav OpenHabitTracker nav with Auth UI after a successful Appwrite backup ## Clone and run the PWA [#clone-and-run-the-pwa] You need the [.NET 10 SDK](https://dotnet.microsoft.com/download). ```bash git clone https://github.com/Jinjinov/OpenHabitTracker.git cd OpenHabitTracker dotnet run --project OpenHabitTracker.Blazor.Wasm --launch-profile http ``` The first build takes a few minutes. Open [http://localhost:5173](http://localhost:5173) and you have the app with a welcome note. ## Create an Appwrite project and a bucket [#create-an-appwrite-project-and-a-bucket] 1. [Create a project](https://appwrite.io/docs/quick-starts/web#step-1) on Appwrite Cloud or your own instance and add a Web platform with hostname `localhost`. 2. Under **Storage**, [create a bucket](https://appwrite.io/docs/products/storage/buckets) with the ID `backups`. 3. In the bucket's **Settings**, enable **File security** and add a permission that lets the **Users** role **create** files. Each backup will carry its own read, update and delete permissions for the user who owns it, so nobody else can see it. Note the project ID and API endpoint from the Console. ## Load Auth UI [#load-auth-ui] Add one script tag at the end of `OpenHabitTracker.Blazor.Wasm/wwwroot/index.html`, after the service worker registration: ```html title="wwwroot/index.html" ``` ## Write the JavaScript side [#write-the-javascript-side] Blazor talks to JavaScript through modules. Create `OpenHabitTracker.Blazor.Wasm/wwwroot/appwrite.js` with five functions. `init` configures Auth UI and reports sign-in changes back to C#. The upload keeps one file per user, named after the user ID, with permissions for that user only. The download goes through Auth UI's client, so the request is authenticated the same way as everything else, and because the file is JSON the client hands back the parsed object. ```js title="wwwroot/appwrite.js" import { AuthUI, appwrite } from "https://unpkg.com/@getauthui/core@0.1.45"; const { Storage, Permission, Role } = appwrite; const BUCKET = "backups"; export function init(endpoint, project, dotnetRef) { AuthUI.init({ endpoint, project, methods: { emailPassword: true }, branding: { name: "OpenHabitTracker" }, }); AuthUI.on("change", (state) => dotnetRef.invokeMethodAsync( "OnAuthChanged", state.status === "signed-in", state.user?.email ?? "" ) ); } export function open(view) { AuthUI.open(view); } // One file per user, named after the user ID, only that user can read or replace it. export async function uploadBackup(json) { const storage = new Storage(AuthUI.getClient()); const userId = AuthUI.getUser().$id; const file = new File([json], "OpenHabitTracker.json", { type: "application/json" }); const permissions = [ Permission.read(Role.user(userId)), Permission.update(Role.user(userId)), Permission.delete(Role.user(userId)), ]; try { await storage.deleteFile(BUCKET, userId); } catch { /* first backup */ } await storage.createFile(BUCKET, userId, file, permissions); } export async function downloadBackup() { const storage = new Storage(AuthUI.getClient()); const userId = AuthUI.getUser().$id; const url = new URL(storage.getFileDownload(BUCKET, userId)); const data = await AuthUI.getClient().call("get", url); // the file is JSON, so the client parses it return JSON.stringify(data); } ``` `appwrite` is the Appwrite Web SDK, re-exported by Auth UI so the page loads it once. `AuthUI.getClient()` is the signed-in client, and `AuthUI.getUser()` the current user. See the Appwrite docs for [`createFile`](https://appwrite.io/docs/references/cloud/client-web/storage#createFile) and [permissions](https://appwrite.io/docs/advanced/platform/permissions). ## Enable the nav bar slot [#enable-the-nav-bar-slot] The shared layout has a slot for a login display in the navigation bar, but it is commented out. Open `OpenHabitTracker.Blazor/Layout/Main.razor`, find the block near the end of the `