Catalog / Appwrite

2.3.0

yesterdaysecurityaddedfixedOriginal notes

Appwrite 2.3 removes dev keys, builds every console link from one _APP_CONSOLE_URL, reports execution duration the way the caller experienced it, adds Jaspr to Sites and a nightly channel for self-hosting, and folds in two weeks of fixes across auth, databases, functions, messaging, storage and the installer. It ships with the 1.1.159 self-hosted Console (was 1.1.96). Upgrading from 2.2.0 is a single command and a migration that touches every project's users and identities collections.

Highlights

  • Dev keys are gone. Creating dev keys was already switched off, but existing ones were still accepted and still bypassed abuse limits, CSRF protection and origin and redirect checks, including the OAuth2 success and failure URLs. The dev key endpoints, the X-Appwrite-Dev-Key header and devKey query parameter, the devKeys.read / devKeys.write scopes and the devKeys collection are removed; migrate drops the collection and strips the scopes from API keys, functions and sites. Until you run it, the API keeps serving. (#13696, #13800)
  • One setting for console links. _APP_CONSOLE_URL is the origin every console link is built from: OAuth2 success and failure defaults, magic URL, build and webhook-failure emails, VCS comments and commit statuses, and VCS installation callbacks, which now land on /projects/:projectId/settings. Leave it empty to use _APP_CONSOLE_DOMAIN (or _APP_DOMAIN) as before; set it when the console runs on a different host from the API. _APP_CONSOLE_URL_SCHEME and the legacy /console/... link shapes are gone, and, when the console is served from a different host than the API, /invite, /login, /recover and /register redirect to the matching console pages again. (#13689)
  • Execution duration includes cold start. duration on an execution is now the time the caller waited, however it was triggered. Executions created through the API or run asynchronously used to report only the time after the runtime was warm, so a cold start that took 6 seconds could show as 311ms in the Console. Expect higher numbers for cold-started executions and in compute usage. (#13754)
  • Native ID token sign-in keeps the token. POST /v1/account/sessions/id-token stores the verified ID token on the identity as providerIdToken (encrypted at rest, refreshed on every sign-in), so clients can read claims Appwrite does not model, such as Google's locale. The Console shows it on the user's identities. The provider parameter now only accepts providers that support ID tokens (Apple and Google) and gets its own IdTokenProvider enum in the SDKs. (#13698, #13759)
  • Jaspr on Sites. Jaspr is available as an SSR framework, built with jaspr build on the Flutter 3.44 runtime. Add flutter-3.44 to _APP_SITES_RUNTIMES to use it (the default list has flutter-3.29). Pick it by hand for now; repository detection does not suggest it yet. (#13710, #13773, appwrite/vibes#407)
  • A nightly channel. install and upgrade take --channel nightly, or set _APP_VERSION=X.Y-nightly, to follow the newest CI-green commit on the active release branch, rebuilt daily. Nightly is unsupported and stays within one patch line; stable remains the default. (#13659)
  • Console 1.1.159, self-hosted build. Site preview authorization works, self-hosted member invites offer Owner and Developer roles, impersonation loads files and avatars as the impersonated user, the datetime picker shows and switches time zones, and Jaspr is selectable.

Fixes

Console

  • Site preview domains send visitors to a console /auth/preview page that exists, instead of the console's 404. (appwrite/vibes#366, #13714, fixes #13713)
  • Member invitations and edits on self-hosted offer the organization-wide Owner and Developer roles and save the one selected. (appwrite/vibes#387, #13734)
  • While impersonating a user, file previews and downloads, avatars and deployment downloads load as that user instead of returning 401. (appwrite/vibes#393, appwrite/vibes#394)
  • The datetime picker accepts hours of 10 or more and shows and switches the time zone. (appwrite/vibes#361, appwrite/vibes#356)
  • Enum values containing commas are kept intact, and creating a row with a custom ID creates it instead of trying to update a missing row. (appwrite/vibes#363, appwrite/vibes#391)
  • The users list pages past 5,000 users, and deleted users and teams show a proper not-found page. (appwrite/vibes#425, appwrite/vibes#441)
  • Function and site pages render when a referenced deployment is missing; the deployment details panel shows on small screens. (appwrite/vibes#386, appwrite/vibes#442)
  • The function create wizard takes its domains from console variables and no longer pins the domain to a branch. (appwrite/vibes#379, appwrite/vibes#416)
  • Deleting function executions and site logs works again, and the logs Method filter matches the stored request method. (appwrite/vibes#423, appwrite/vibes#422)
  • Usage and activity pages keep working when ClickHouse is unavailable. (Console 1.1.139)
  • Self-hosted shows the S3 settings, and hides the database Specification tab, account Applications and the old console link. (appwrite/vibes#362, appwrite/vibes#367, appwrite/vibes#371)

Auth and users

  • POST /v1/account/tokens/email, /tokens/magic-url and /tokens/phone return 409 user_already_exists when the given userId belongs to another user, instead of a 500. (#13790)
  • Refreshing an OAuth2 session with PATCH /v1/account/sessions/:sessionId also refreshes the identity's photo, so providers with expiring avatar URLs stay current. (#13671)
  • Authentik sign-in opens the consent screen instead of an Authentik 404. (#13819, fixes #9567)
  • Flutter iOS apps are recognized as mobile clients, so sessions and OTP and magic URL emails name the app. (#13739, fixes #9372)
  • A team member always sees their own membership, whatever the membership privacy policy, so members can find their membershipId and leave a team. (#13786, fixes #13767)

Databases and TablesDB

  • A transaction whose commit fails a permission check is marked failed and returns 401, instead of staying in committing. (#13703)
  • Cleanup of expired transactions removes their logs in batches, so more than 5,000 expired transactions no longer leave logs behind. (#13563)
  • The relationship column update lives at PATCH /v1/tablesdb/:databaseId/tables/:tableId/columns/relationship/:key, like every other column type; the old columns/:key/relationship path keeps working. (#13822, fixes #9022)
  • Sending an object on the child side of a one-way one-to-one relationship explains that the value can't be set from that side, instead of "array given". (#13738, fixes #8345)

Functions and Sites

  • A deployment reports ready only after the function points at it, so executing right after seeing ready no longer returns 404. (#13741)
  • Domains pinned to a git branch bind to that branch's newest build: activating a deployment repoints them, function builds on non-production branches bind them, and a new rule no longer starts on another branch's build. Rules already stuck bind on their branch's next build. (#13795)
  • Redeploying a function or site whose code lives in a subdirectory builds from the configured root directory, and APPWRITE_VCS_ROOT_DIRECTORY is set for push and redeploy builds. (#13574, fixes #13587)
  • When several functions subscribe to one event, a failing function no longer stops the others from running. (#13661, fixes #7205)
  • Build logs containing binary output or a split multibyte character keep updating instead of failing to save. (#13706)
  • A GitHub push for a repository whose function or site was deleted is skipped, instead of failing the whole webhook. (#13793)
  • Pull request comments show the building image for a site deployment in processing, not a broken image. (#13771)
  • GitHub App installations on private profiles list their repositories; _APP_VCS_GITHUB_PRIVATE_KEY accepts a PEM, a base64-encoded PEM or a PEM with escaped newlines; a top-level root directory named 0 is cloned correctly. (#13695, #13738)

Messaging

  • An email to a single recipient keeps them in the To header; messages with several recipients still send everyone as BCC. (#13534, fixes #12365, #10474)
  • Concurrent sends in the mails and notifications workers each use their own SMTP session, instead of sharing one socket. (#13722)
  • Addresses no provider can deliver to (an invalid domain, a malformed sender or reply-to) are skipped instead of retried, and display names such as Doe, John are quoted so the mail is delivered. (#13831)
  • Registering a push token for a session that already has a target updates that target instead of adding a second one, so a rotated token no longer delivers messages twice. Re-registering the same token returns the existing target, and targets marked expired are no longer sent to. (#13836)
  • PUT /v1/account/targets/:targetId/push returns 409 user_target_already_exists when the identifier is taken, instead of a 500. (#13789)

Storage

  • File responses always include encryption and compression, fixing SDKs that crashed on a missing field. (#12405, fixes #11647)
  • Uploads that sniff as their container keep their real type: .jar, .aar, .kmz, .3mf, .usdz, .xpi, .msix, .ttf and .otf. Files uploaded before keep their stored type. (#13825, fixes #9418)
  • HeadObject through the S3-compatible API works for clients that sign it with x-id=HeadObject (such as boto3) when a proxy rewrites HEAD to GET. (#13744)

Domains and certificates

  • Deleting a rule no longer removes the certificate of a new rule for the same domain when the delete job runs late. (#13570)
  • A domain verified by the API is not checked again by the certificates worker, so a DNS hiccup can't flip a just-verified rule to unverified. (#13765)
  • Rules saved without a project no longer fail in the certificates worker after verification. (#13709)

API and GraphQL

  • Sending null for name on account create, schedule on function create or path on execution create applies the default instead of returning a 500. (#13707)
  • GraphQL accepts JSON array literals, keeps nested variables and strings, and keeps integer literals exact. (#13667)
  • PATCH /v1/presences/:presenceId returns 404 presence_not_found when the presence is deleted or expires mid-update, instead of a 500. (#13792)

Installer and workers

  • MongoDB starts reliably on a fresh volume; it initialises the data directory on a separate port instead of racing the real server for 27017. (#13593)
  • migrate restores the five email metadata attributes on users when an earlier V23 step failed, which made every user write fail with Unknown attribute: "emailCanonical". (#13820, fixes #11906)
  • install and upgrade take --domain to set _APP_DOMAIN and _APP_DOMAIN_TARGET in headless installs. The CLI installer now sends the same anonymous install report as the web installer, including --no-start and loopback installs; set DO_NOT_TRACK=1 to opt out. (#13844)

Under the hood

  • utopia-php/queue 2.2 → 5.0. Each queue consumer now holds two dedicated Redis connections, stranded jobs are recovered while a worker is idle, and worker concurrency is read as coroutines; _APP_WORKER_MAX_COROUTINES still works. (#13762, #13809, #13814)
  • Redis cache entries are stored with igbinary instead of JSON, about a quarter of the size for lists. Old entries read as misses and refill on first use, so expect a short burst of misses after the upgrade. (#13732)
  • stats-resources spreads project counts across the interval instead of queuing every project at once. (#13669)
  • New projects no longer get a project-database executions collection; executions have been ClickHouse only since 2.2. (#13699)
  • The Utopia libraries agents, bus, lock, psr7, telemetry, compression, system, smtp and pools now live in packages/ and load directly; no version changes. The agents client no longer leaks a connection per call. (#13700, #13715, #13718, #13723, #13731, #13798, #13804, #13829, #13845, #13794)
  • OpenAPI specs: x-appwrite.idGenerator removed, alias desc renamed to summary, the ProjectPath security scheme replaced by x-appwrite.config. (#13742, #13745, #13757)
  • Compose orchestrator 2.2.0 → 2.2.2, Autogravity 0.0.10 → 0.0.11 with MODEL_BACKEND=focalnet, base image appwrite/base 2.0.5 → 2.1.0 (adds igbinary), utopia-php/database 7.3.11, vcs 5.5, messaging 2.5, user-agent 0.1.2, detector 0.2.8, cdn 0.0.12, schedule 0.3. (#13720, #13753, #13732, #13738, #13851, #13739, #13773, #13760, #13746)

Removed

  • Dev keys: the endpoints, the X-Appwrite-Dev-Key header and devKey parameter, the devKeys.read / devKeys.write scopes, the DevKey model and the devKeys collection. (#13696)
  • _APP_CONSOLE_URL_SCHEME and the legacy /console/project-{region}-{id}/... link shapes. Use _APP_CONSOLE_URL. (#13689)
  • The /card web redirect; the new console has no matching page. (#13689)

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.3.0

Upgrade

Upgrading from 2.2.0 uses the same command with --entrypoint="upgrade", then run the migration:

cd appwrite
docker compose exec appwrite migrate

The migration drops the devKeys collection and project attribute from the console database, removes the dev key scopes from API keys, functions and sites, and in every project adds providerIdToken to identities and a passwordPwned attribute and index to users, restoring any missing email metadata attributes on the way. Its run time grows with the number of projects. The upgraded compose file adds an appwrite-mqtt container and has Traefik publish ports 8883 and 8084; set _APP_MQTT_PORT and _APP_MQTT_WSS_PORT in .env if those are taken. Remove _APP_CONSOLE_URL_SCHEME from your .env, and set _APP_CONSOLE_URL if your console is served from a different host than the API. To roll back to 2.2.0, stop the workers and let queued jobs drain first, since the older queue client can't recover jobs the new one has reserved. Back up your data first.

Contributors

Thank you to everyone who contributed to 2.3.0:

@HarshMN2345 @ChiragAgg5k @Meldiron @levivannoort @loks0n @TorstenDittmann @ArnabChatterjee20k @imtia33 @jaysomani @deepshekhardas @abnegate @lohanidamodar