Catalog / Appwrite

2.4.0

3 days agosecurityaddedfixedOriginal notes

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

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:

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