๐ View Raw Release Notes (Original Developer Log)
Appwrite 2.4 adds custom profile photos, Webflow sign-in, push messages straight to a user without a device target, and webhook delivery IDs with retries that only resend what failed. The Console gets CLI and MCP onboarding and a breached password policy. Outbound requests and sign-in tokens are locked down further, ClickHouse no longer fills the disk with its own logs, and the Appwrite Assistant is retired. It ships with the 1.2.55 self-hosted Console (was 1.1.159).
Upgrading from 2.3.0 is one command plus `migrate`. Read the Upgrade section first if you run behind a public load balancer, use the GitHub integration without a webhook secret, or reach private hosts for OAuth2, webhooks or messaging.
### Highlights
- **Custom profile photos.** Users can upload a photo with `PUT /v1/avatars/photo` and remove it with `DELETE /v1/avatars/photo`. `GET /v1/avatars/photo` serves it ahead of the OAuth2, Gravatar and initials fallbacks. The new `avatars.write` scope covers both writes. In the Console, the Profile photo card is on Account โบ General, and user photos show across Auth and teams. (#13949, #14055, #14065, #14129, appwrite/vibes#516, appwrite/vibes#511, appwrite/vibes#512)
- **Webflow sign-in.** Webflow is a new OAuth2 provider, with a card in the Console. Webflow does not issue refresh tokens. (#13946, #13978, appwrite/vibes#487)
- **Push to users directly.** With the Appwrite push provider, a message to `users` reaches the user on a reserved `users/<userId>` topic without registering a push target, and the payload includes `messageId`. (#13860, #13876, #13981)
- **Retries repeat only what failed.** Webhook requests carry a stable `X-Appwrite-Webhook-Delivery-Id`, and a retry goes only to the endpoints that failed. A function that already ran is never run again by a retry. (#14025, #14023)
- **Console 1.2.55.** Adds profile photos, the Webflow card, CLI and MCP onboarding, and the breached password policy. Databases with more than 100 tables now work, and manual uploads follow the server's size limit. The Assistant is gone.
- **Outbound requests are checked where they land.** OAuth2 and OIDC providers, webhooks, messaging webhooks, migration sources, avatar images and identity photos now refuse private and reserved addresses, checked on the address each connection and redirect actually reaches. To allow specific private hosts, list them in `_APP_ALLOWED_INTERNAL_ADDRESSES`. `_APP_DNS_EXTERNAL` sets the DNS servers used to check migration and avatar hosts (defaults to `_APP_DNS`). Avatar image, favicon and screenshot URLs with a non-public host now return 400. `appwrite-browser` moves to its own network and only reaches public hosts and `appwrite`. (#13998, #13995, #14061, #14066, #14127)
- **Tokens are bound to where they came from.** A user JWT only works in the project that issued it, over HTTP, Realtime and MQTT. OAuth2 callbacks are checked against a per-flow cookie and fail with `user_oauth2_state_invalid` on a mismatch. Server-started OAuth2 token sign-in still works, and `createOAuth2Token` / `createOAuth2Session` take an optional `state` that is returned on the success and failure URLs. (#13992, #13997, #14002, #14012, #14157)
- **ClickHouse logs no longer fill the disk.** ClickHouse's `system.*` logs grew by hundreds of MB a day on idle installs. The unused logs are now off, the rest are kept 3 to 14 days, and existing log tables get a retention on start, so the space comes back on its own. External ClickHouse servers are untouched. (#13735, #13872)
### Fixes
**Console**
- Databases with more than 100 tables open the right one, and pickers, Export / Import and the schema view list every table. (appwrite/vibes#461, appwrite/vibes#462, appwrite/vibes#464, #13908)
- Manual deployment uploads use `_APP_COMPUTE_SIZE_LIMIT` instead of fixed 10 MB and 100 MB caps. (appwrite/vibes#483, #13967, fixes #13916)
- Self-hosted GitLab and Gitea owners link to your own instance instead of `gitlab.com`. (appwrite/vibes#448, #13875, #13879)
- The breached password policy can be set under Auth, and each user's result shows in the Console. (appwrite/vibes#428)
- CLI and MCP installs count as onboarding stages, and skipped stages complete once done. (#13717, #13951, appwrite/vibes#373, appwrite/vibes#405, appwrite/vibes#506)
- The create bucket dialog, email template preview, project settings and Init page work again. (appwrite/vibes#460, appwrite/vibes#467, appwrite/vibes#473, appwrite/vibes#535)
- Function scopes moved to Settings โบ Executions, and role changes apply without a reload. (appwrite/vibes#447, appwrite/vibes#561)
- Updated to the TanStack Start release that fixes CVE-2026-102989, and concurrent page loads no longer share router state. (appwrite/vibes#493, appwrite/vibes#509, appwrite/vibes#570)
**Auth and users**
- Impersonation is read-only: account reads return the target user and writes fail with `403 user_impersonation_read_only`. Before, `DELETE /v1/account` deleted the target. MFA checks use the impersonator's own factors. (#13653, #14017)
- JWT requests resolve their session, so MFA checks, the `current` keyword and session invalidation work with JWTs. (#13900)
- Exchanging the same token secret twice at once creates one session. (#13983)
- API keys without `users.write` get an empty token `secret`. Recovery tokens are single-use, and password changes end other sessions and pending MFA challenges. (#14045, #14048)
- `/v1/users/:userId/sessions` routes need `users.read` / `users.write`; `sessions.*` alone is no longer enough. (#13994)
- Console organization developers can no longer change membership roles or invite owners. (#14097, #13993)
- Native OAuth2 logins redirect straight to `appwrite-callback-<projectId>://`. (#13970, #13976, fixes #3142)
- GitLab sign-in trusts only the verified primary email. Bad Google and GitHub responses go to the failure URL instead of a 500. (#14010, #13968, #14093)
- Passwords hash with the configured Argon2 parameters and are rehashed on login when they differ. (#14101, fixes #14099)
- Deleting your own account also removes its identities and targets, so you can sign up again right away. (#14208, fixes #8135)
- `POST /v1/teams` accepts 81-character roles, and the anti-phishing phrase uses a CSPRNG. (#13944, fixes #8051; #13943, fixes #12783; #13940)
- Typos fixed in the Arabic recovery email, and German emails no longer fall back to English. (#13975, fixes #11425; #14189, fixes #14134)
**Databases and TablesDB**
- Transaction reads use the caller's permissions, and nested related documents can only carry roles the caller holds. (#14045)
- Committing one transaction concurrently applies it once; the others get `400 transaction_not_ready`. (#14163, fixes #14142)
- Rows created before a column became required can be updated without it. (#14202, fixes #14175)
- Deleting a document in a two-way relationship sends realtime updates for the related documents. (#13690, fixes #6265)
- `getColumn` and `getAttribute` declare every column type, so typed SDKs decode the newer ones. (#13877)
- A column keyed `relationship` updates through its typed route. Relationships named after a type must use `relationship/:key`. (#14165, fixes #8760)
- Creating an index right after its column is `available` no longer fails. (#14104)
**Functions and Sites**
- Template and VCS deployments keep their source again, so Download source works. Older deployments need a redeploy. (#13934)
- Function templates move to 1.2.0 and work on `node-26`. Templates are no longer offered on `node-14.5`, `node-16.0` or `python-3.8`, and fresh installs enable `node-22`. The MCP server template supports OAuth. (#13881, #14131)
- Build logs no longer leak internal storage URLs, and download failures say why. (#13856, #13882)
- Chunked deployment uploads validate `x-appwrite-id`, and local build workers only see their own project. (#13996, #14098)
- Malformed execution headers return 400 instead of 500. (#13963, fixes #13962)
- Listing executions by `$createdAt` no longer scans the whole history in ClickHouse. (#13982)
- Builds with no measured duration are billed at most their timeout plus 300 seconds. (#14029)
- Invalid branch preview hostnames are skipped, and the preview authorize route only accepts relative paths. (#13957, #14013)
- TanStack Start with Nitro is detected as SSR, and a bare `package.json` is no longer detected as Angular. (#13960, fixes #13912; #13965, fixes #13911)
**Messaging**
- Users can only subscribe their own targets to a topic; API keys and the Console still can subscribe any. (#14019)
- Push messages to more than 25 recipients reach all of them. (#14238)
- The Appwrite push provider keeps user topics private, refuses first-level wildcard subscriptions, and works with strict MQTT 5 clients. (#14030, #13907)
- When a project's own SMTP server fails, the job ends with the error instead of filling the failed queue. (#14139, #14027)
**Webhooks**
- `authPassword` is write-only and is dropped when the URL changes or TLS verification is turned off. (#14016)
- A paused webhook alerts its owners once. (#13823, fixes #12944)
- Fixed a memory leak of about 140 KiB per hostname lookup. (#13952)
**Storage**
- Single-byte ranges and ranges past the end of a file are served, so media players can seek. (#13824, fixes #8600)
- Only the uploader, or someone who can update the file, can resume a chunked upload. (#14014)
- If ClamAV is unreachable, the upload is removed instead of left pending. (#13977, fixes #8603)
- S3 retries `InternalError` responses and safe-to-replay connection failures. (#13979)
**Domains and certificates**
- Certificate retries reset the attempt count, and certificates still being issued are requeued instead of stuck on "Generating certificate". (#14009, #14117, #14125)
**VCS**
- GitHub webhooks now require `_APP_VCS_GITHUB_WEBHOOK_SECRET`. See Upgrade. (#13999)
- Group- and organization-owned GitLab and Gitea repositories resolve to the right owner. (#13854)
- Pushes no longer build functions or sites that disconnected the repository. (#13974, fixes #9318)
- Updating a GitHub installation repository checks that the installation belongs to the project. (#14044)
- GitHub webhooks no longer return 500 for removed installations or non-JSON file listings. (#14119, #14168)
- `.env` detection keeps `#` in unquoted values and reads `export` lines. (#14235)
**Realtime**
- The `Origin` check applies to every project and accepts the `_APP_CONSOLE_URL` host. (#14020)
- Open connections follow user changes and close with a 401 when the session ends or the user is blocked or deleted. (#14021)
**Migrations**
- Migrating from another project on the same instance checks the source API key first. Webhook passwords are no longer copied. (#14003, #14016)
**API**
- An explicit `null` for an optional parameter counts as omitted instead of returning 500. (#13852)
- Execution header values are always strings, and empty objects serialize as `{}`, matching the spec. (#14158)
- `updateEmail`, `updateSms` and `updatePush` take parameters in the same order as their create methods. This changes the generated SDK signatures. (#13959, fixes #7919)
**Installer and workers**
- Fresh installs generate `_APP_OPENSSL_KEY_V1` instead of using `your-secret-key`. Upgrades keep the existing key. (#14048)
- The web installer needs a one-time secret, which `install` and `upgrade` print. (#14048)
- `doctor` checks for updates against the latest GitHub release. (#14162, fixes #7717)
- The deletes worker no longer loops on large sets of retained documents. (#14159)
### Under the hood
- 38 more Utopia libraries now live in `packages/`, and `utopia-php/client` replaces `utopia-php/fetch`. (#13858, #13862, #13863, #13865, #13867, #13869, #13870, #13885, #13886, #13887, #13888, #13889, #13890, #13891, #13892, #13894, #13896, #13897, #13899, #13921, #13922, #13923, #13925, #13926, #13927, #13928, #13929, #13930, #13931, #13932, #13953, #13954, #13955, #13956, #13969, #14054, #14227, #13893, #13910, #14072)
- utopia-php/queue 6.0: the failed-queue health check now also counts dead-lettered messages. (#13841)
- utopia-php/database 7.4.1, migration 2.0.8, abuse 3.0, query 0.7, storage 4.3 and `appwrite/geo` 0.3.2. (#14202, #13978, #14056, #14180, #13883, #14100)
- The anonymous install report now goes to `cloud.appwrite.io`; `DO_NOT_TRACK=1` still opts out. (#13917)
- MQTT broker telemetry and per-project usage. (#14043, #14115, #14188, #14106)
- Shipped scopes are locked and can only be deprecated, and a new E2E suite checks every route for access-control and SSRF issues. (#14206, #14229, #14062, #14064, #14245)
### Removed
- The Appwrite Assistant: its endpoint, the `appwrite-assistant` container and `_APP_ASSISTANT_OPENAI_API_KEY`. The `assistant.read` scope still validates for existing OAuth2 clients but grants nothing, and `migrate` removes it from API keys, functions and sites. (#14167, #14200, appwrite/vibes#547)
- `VITE_GROWTH_ENDPOINT` from the console service. (#13917)
### Install
```bash
docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:2.4.0
```
### Upgrade
Run the same command with `--entrypoint="upgrade"`, then migrate:
```bash
cd appwrite
docker compose exec appwrite migrate
```
The migration stays on V25. It adds columns for passkeys, authenticator names and profile photos to every project, and removes `assistant.read` from keys, functions and sites. It takes longer the more projects you have. The new compose file drops `appwrite-assistant`, moves `appwrite-browser` to its own network and adds two ClickHouse config mounts. Remove `_APP_ASSISTANT_OPENAI_API_KEY` from your `.env`.
Before upgrading, check these:
- **Public load balancer:** forwarded client IPs are only trusted from `_APP_TRUSTED_PROXIES` (private ranges by default). If a public hop like Cloudflare sits in front of Appwrite, add its ranges to `_APP_TRUSTED_PROXIES`. If it sends the client IP in its own header, also set `_APP_TRUSTED_HEADERS` to that header (for example `cf-connecting-ip`); the header is only read from trusted proxies. Otherwise all clients look like that hop and share its rate limits. (#14048)
- **GitHub integration:** set a webhook secret in your GitHub App and the same value in `_APP_VCS_GITHUB_WEBHOOK_SECRET`, or GitHub stops working. (#13999)
- **Private hosts:** OAuth2 and OIDC providers, webhooks, messaging webhooks and migration sources on private addresses need to be listed in `_APP_ALLOWED_INTERNAL_ADDRESSES`. Messaging providers no longer use `HTTP_PROXY` / `HTTPS_PROXY`. If your screenshots router is not `appwrite`, add it to `ALLOWED_HOSTS` on `appwrite-browser`. (#13998, #14127)
- **Function templates:** Node templates need `node-18.0` or later in `_APP_FUNCTIONS_RUNTIMES`. The old default only has `node-16.0`, and upgrades keep it. (#13881)
- **Monitoring:** the failed-queue health check now includes dead-lettered messages, so alerts may fire without new failures. (#13841)
- **Sign-ins in flight:** OAuth2 flows started before the upgrade fail once, and session-less JWTs from before it stop working (they expire within an hour). (#14012, #13992)
If you upgrade with `--use-existing-config`, copy the two ClickHouse mounts from the new `docker-compose.yml` yourself to get log retention. **Back up your data first.**
### Contributors
Thank you to everyone who contributed to 2.4.0:
@ChiragAgg5k @HarshMN2345 @abnegate @loks0n @levivannoort @Meldiron @ArnabChatterjee20k @eldadfux @jaysomani @TorstenDittmann @lohanidamodar @imtia33 @fogelito @Indra55 @AayushKrGupta