๐ View Raw Release Notes (Original Developer Log)
Appwrite 2.7.0 adds server key rotation, a way to sign out every user of a project, a realtime policy that keeps guests from subscribing, origin-bound `OTP-Token` headers on code emails, Gitea in the Console's Git connect flow, and per-connection limits for realtime and MQTT. It also carries the security and reliability fixes merged since 2.6.2 across auth, databases, realtime, storage, functions, sites, VCS and migrations. It ships with the 1.2.138 self-hosted Console (was 1.2.108). Upgrading from 2.6.2 needs `migrate`: V25 now adds new attributes, indexes and collections. No configuration change is required, but several defaults tighten, so read **Upgrade** before you upgrade.
### Highlights
- **Server key rotation.** The server key is now versioned. `_APP_OPENSSL_KEY_CURRENT` picks the `_APP_OPENSSL_KEY_V{n}` that signs new tokens and encrypts new data. `_APP_OPENSSL_KEY_OLDEST` is the oldest version still accepted when verifying JWTs, ephemeral keys, resource tokens and OAuth/VCS state. Stored values keep decrypting with the version they record. The new `encryption` task re-encrypts stored data under the current version. With the defaults (`CURRENT=1`, `OLDEST=1`) nothing changes (#15198, #15199).
- **Sign out every user in a project.** `DELETE /v1/project/sessions` and a new card on **Auth > Settings** invalidate every session in the project at once. Old session documents are removed in the background, and realtime connections on those sessions are closed, impersonated ones included (#14647, #15089).
- **Realtime guests policy.** **Auth > Policies > Realtime** can stop guests from opening realtime subscriptions (#14945, #14996).
- **Realtime and MQTT limits.** A realtime connection can hold at most 100 subscriptions, 100 channels and 100 queries in total (nested `and`/`or` queries count), and one project user at most 32 connections per realtime worker. Set the first three with `_APP_LIMIT_REALTIME_SUBSCRIPTIONS`, `_APP_LIMIT_REALTIME_CHANNELS` and `_APP_LIMIT_REALTIME_QUERIES`. The MQTT broker caps topic filters per SUBSCRIBE at 100, connections at 32 per user per worker, and SUBSCRIBE packets at 32 a minute per user when `_APP_OPTIONS_ABUSE` is on. Realtime also closes connections that have been silent for two minutes, so presence no longer sticks for clients that dropped off (#14945, #14948, #15056, #15059).
- **`OTP-Token` email header.** Email OTP sign-in, OTP password recovery, OTP email verification and email MFA challenges carry an `OTP-Token` header bound to the request's origin, so mail clients and password managers can autofill the code only on that origin. It is sent only for HTTPS origins (or `localhost`), so server SDK calls and native apps get the same email as before (#15096).
- **Gitea in Git connect.** Create function, create site, the repository picker and project Git settings offer **Connect Gitea** when `_APP_VCS_GITEA_*` is configured. The connect buttons now show only the providers the server has configured, instead of offering ones whose authorization fails (#15004).
- **Playground.** API Explorer and Realtime now share one **Playground** screen with a REST / Realtime switch. Old Explorer and Realtime links redirect to it.
- **Console 1.2.138.** Besides the screens above: **Logs** on verified domains, so certificate renewal warnings can be read (#15015); a warning and Disconnect action when a site's or function's repository is no longer reachable through its installation (#15043); column editor fixes (#15081, #15117, #15072); API explorer keys kept in memory instead of `localStorage` and cleared on sign-out (#14994); and patched Zod copies (#14908).
### Fixes
**Auth and accounts**
- Require an MFA challenge from the last few minutes to turn MFA off, as deleting an authenticator already does (#15057).
- Make TOTP codes single-use, so one code can't complete a challenge on a second session; the enrolment code can't be reused either (#15154).
- Proving an unverified phone now claims the account the same way an unverified email does. Every claim path (magic URL, email OTP, password recovery, signed-out team invite) revokes what the registrant left behind, keeps MFA in force when the account needs two factors, and closes the registrant's live realtime and MQTT connections (#15058, #14976, #15144, #15062).
- Bind the OAuth2 state to the user who started the flow (#15029).
- Scope the email sign-in rate limit to the project and address (#15023).
- Keep created token and session secrets out of realtime events (#14520). Fixes #14415.
- Apply a project's custom session alert template only when its custom SMTP is enabled, like every other email (#15055).
- Stop granting `rules.read` to project end users, who could list the project's proxy rules (#15013).
- Let API keys call `public` scope endpoints such as the runtimes and frameworks lists, which `appwrite init function` needs in CI (#15156).
- Fall back to the default locale when a translation is an empty string, which queued recovery emails without a subject (#15069).
**Users and teams**
- Stop a blocked user from impersonating (#15021).
- Reject imported Argon2 hashes that can never verify (#14924).
- Delete a user's presence when the user is blocked or deleted (#14397). Fixes #14344.
- Revoke MQTT topic subscriptions when a user's push targets are deleted with their credentials (#14952).
- Check whether the caller may invite to a team before the invitee is looked up or created (#14947).
- Apply membership privacy to the create and update membership responses (#15027).
- Enforce the last-owner rule on console teams for every caller, API keys included, and count only confirmed owners (#15153).
**Databases**
- Scope transactions to the API they were created from: a transaction created through VectorsDB can't be read or committed through TablesDB, and the other way around. Existing transactions are treated as legacy and stay usable through Databases and TablesDB (#14350).
- Require an API key or a privileged user for bulk update, upsert and delete, as bulk create already did (#15235).
- Emit row events for bulk operations in a transaction (#14383). Fixes #14219.
- Lock the transaction while staging operations (#14384). Fixes #14332.
- Apply transaction bulk updates once, to matching rows only, and handle missing query operators in transaction state filters (#14378, #14324). Fixes #14217, #14222.
- Load transaction metadata once per database and table (#14190). Fixes #14176.
- Bound offset and limit for transaction-aware listing (#14964).
- Refresh cached lists when document permissions change (#14869).
- Chunk the batch duplicate lookup by the maximum number of query values (#15017).
- Accept `upsert` in row and document event subscriptions for functions and webhooks (#14742). Fixes #14653.
- Console: restrict column number inputs and stop prefilling unbounded min/max (#15081, Fixes #7706, #9217); drop renamed or deleted columns from the saved rows selection, which made every row list fail (#15117); don't crash the columns and indexes tabs while no table is selected (#15072).
- The project onboarding checklist counts VectorsDB databases, collections and documents (#15186).
**Realtime and MQTT**
- Reset a connection's authorization when it re-authenticates, so a connection opened in admin mode doesn't stay privileged after switching to a project user (#15064).
- End admin-mode connections when the console user loses access to the project (#15060).
- Re-authorize console tails on project transfer and re-authentication (#14989).
- Send console copies of project events to the project's readers, and address cross-tree event copies to the right audience (#14988, #15002).
- Keep execution logs and errors out of realtime payloads (#15024).
- Fail closed when a permission check is stuck, instead of stalling delivery for every project on the worker, and keep exceptions out of the pub/sub subscribe loop (#15086, #15166, #15169, #15168).
- Bound the MQTT revoke wait and fail closed on a verification timeout (#15085, #15170).
- Scope stored push messages to the topic instance (#14867).
**Storage**
- Harden chunked uploads, file rename and preview (#14935).
- Decode only supported raster formats in image transformations, and return 404 for favicons in an unsupported format (#14980, #15070).
- List local directory entries by literal name (#15033).
- S3 gateway: stop storing and returning `x-amz-server-side-encryption`, and refuse bucket names that are not valid IDs (#14990, #15210).
- Sign `Content-Length` only on S3 storage requests that have a body (#14523). Fixes #14326.
- Keep the root directory when flushing the filesystem cache (#14515, #14059). Fixes #14007.
**Functions and sites**
- Keep function and site scopes within the scopes of the key that sets them (#15026).
- Reject control characters in execution header values (#15143).
- Keep the fallback session header out of function and site requests (#14925).
- Read deployment downloads through the file handle that was checked (#15022).
- Reserve the functions fallback domain for preview URLs, so rules can't claim it (#15025).
- Builds: reconcile missing build output callbacks, keep readiness across cache eviction, and clear the output of deployments deleted mid-build, so deployments no longer stay `building` (#14914, #15135, #14927).
- Retention: protect branch-pinned deployments, and retire deployments in small batches to stop worker OOMs (#15075, #15122).
- Require project read permission for private site previews (#14936).
- Bind screenshot keys to their project and rule, make screenshot capture deterministic, keep it within one HTTP timeout, and grant screenshots to the team a project moves to (#14977, #15031, #13315, #15077, #15157).
- Keep `{}` objects such as empty `prefs` as objects in event payloads instead of `[]` (#13650). Fixes #6360.
**VCS**
- Bind GitHub repository links to the project's installation (#14975).
- Strip only the leading branch namespace from push refs (#15035).
- Reject `.git` segments in repository root directories, and confine Gogs file writes to the checkout (#15036, #14993).
- Skip an unnecessary personal OAuth2 refresh for GitHub installations (#12972).
- Entire: report deployments, create repositories, fix token minting, checks and path triggers, follow a repository to the installation that pushes it, and request every permission an installation holds (#14883, #15042, #15006, #15012).
- Console: correct the Silent mode description and show exclusion paths correctly in the build trigger summary (#15043).
**Migrations**
- Cap a migration's authority to the scopes of the key that created it (#15145).
- Fix Appwrite-to-Appwrite migrations of projects with more than one database, where the first table arrived without its columns, indexes and rows (#15039).
- Confine Local destination writes to the export directory, and neutralise formula-like values in CSV exports (#14933, #14516). Fixes #14428.
- Retry migrations stranded by a killed worker (#14399). Fixes #14334.
- Report membership counts (#13598).
**Messaging, mails and webhooks**
- Map APNs results to the token each request was sent for (#15020).
- Retry a stale SMTP session once (#14213, #15211). Fixes #12623.
- Fix a 500 when updating a scheduled draft message that has no schedule (#15167).
- Resolve and check webhook destinations before connecting, and dial project SMTP only on checked addresses (#15037, #15028, #15032).
- Cap the decoded response size for OAuth2, webhook and migration requests (#15030).
- Render email template placeholders in a single pass (#15034).
- Detect disposable email providers that hand out a subdomain per mailbox, and refresh the free and disposable domain lists (#15192, #15212, #14942).
**Platform**
- Scope the proxy rules list cursor to the project (#15065).
- Bound abuse time-limit increments at the limit atomically (#14992).
- Queue: move retried jobs off the failed list atomically, and drop the old payload when a job is retried (#15000, #14524). Fixes #14491.
- Resolve a queued project by its sequence as well as its ID (#14991).
- Scheduler: keep its claim in Redis across restarts, and restart combined scheduler loops that stop (#14531, #15007). Fixes #14488.
- Time out unresponsive Redis connections (#14540). Fixes #14490.
- Treat a negative lock acquire timeout as wait-forever (#14521, #13756). Fixes #13719.
- Accept non-ASCII characters in URLs (#15080). Fixes #9717.
- DNS: bound compression pointers and decoded name length, and add a TCP write deadline (#14983, #14939).
- Choose TLS from the `wss` scheme rather than the port in the WebSocket client (#14984).
- Handle invalid queries in the GraphQL complexity callback (#15073).
- Specs: give hashed password params valid examples, and detect header cycles through media-type encodings (#15040, #14998).
- Pass `_APP_MEMORY_LIMIT` to the maintenance worker (#14926).
- Stop deep-cloning the user document on every request (#14110). Fixes #14087.
- Estimate large collection counts in the usage stats sweep, and defer maintenance on the deletes queue's wait time rather than its depth (#15163, #15159).
- Escape control characters in pretty logs, and keep Sentry events whose attributes hold invalid UTF-8 (#14958, #14995).
- Linear-time bot detection, URL dot-segment removal and MQTT user property building (#14966, #14967, #14968).
- Console: correct the file level security description in bucket settings (#14937). Fixes #14931.
### Under the hood
- `utopia-php/database` moves from 8.3 to 10.1, with an eviction-safe compare-and-swap cache protocol. Existing caches stay valid; no flush is needed (#15134, #15150).
- Open Runtimes Orchestrator 4.0.0; its Docker environment is unchanged (#14893).
- `appwrite/browser` 0.3.10 (#14930, #15014) and the bundled PHP SDK 32.2.0 for migrations (#15054, #15242).
- The bundled PostgreSQL now accepts 500 connections by default, set with `_APP_DB_MAX_CONNECTIONS` (#14987). Fixes #13517.
- Maintenance for projects accessed in the last 30 days now runs through the slot scheduler, spread across five-minute slices, instead of all at once every `_APP_MAINTENANCE_INTERVAL` (#15097).
- Realtime publishes connection usage off the close path, and reports telemetry for its pub/sub handler and worker uptime (#15001, #15113, #15114, #15116).
- Messaging send counters include the email template and SMS country, and spans carry `http.uri` (#15219, #14940).
- Actions are built on first use and registered under literal names, and the remaining account, GraphQL, locale, mock, project, projects and console versions controllers moved into modules with no API change (#15162, #15180, #15119, #15123, #15124, #15125, #15128, #15129, #15130).
- 2.7.0 maps to migration V25, the same class as 2.6.2, with new steps (see **Upgrade**).
### Removed
- The project `users` role no longer holds `rules.read` (#15013).
- The S3 gateway no longer stores or returns `x-amz-server-side-encryption` (#14990).
- No environment variables are removed.
### 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.7.0
```
### Upgrade
**Back up your data first.** Upgrading from 2.6.2 uses the same command with `--entrypoint="upgrade"`. Then run the migration:
```bash
cd appwrite
docker compose exec appwrite migrate
```
The V25 migration is idempotent and adds:
- `prefs` on projects; `alertsUnsubscribed` on users; the GeoIP attributes on sessions; and `folder` with its index on buckets created before 2.0.0 (#15120, #15079, #14329).
- `type` on transactions, backfilled to `legacy` for existing ones (#14350).
- `topicInternalId` indexes on the push ledger (#14867). Ledger entries are now bound to their topic, so entries written before the upgrade have no `topicInternalId` and are never replayed. Push messages a device had not yet received over MQTT when you upgrade are not delivered when it reconnects; the age-based prune removes those entries. New messages replay as before.
- The `transfers`, `monitors`, `monitorAlerts` and `reviews` collections, used by API features that have no self-hosted Console screen yet (#14722, #15079, #15216, #15038, #15044).
Check these before upgrading:
- **Server key**: nothing to change. Keep `_APP_OPENSSL_KEY_V1`. To rotate it later, recreate the containers after each configuration change with `docker compose up -d` (`docker compose restart` keeps the old environment): set `_APP_OPENSSL_KEY_V2` in `.env` and run `docker compose up -d`; set `_APP_OPENSSL_KEY_CURRENT=2` and run `docker compose up -d`; run `docker compose exec appwrite encryption` (a dry run that counts what's left), then `docker compose exec appwrite encryption --commit=true` (resume with `--after=<projectId>` if it stops); then set `_APP_OPENSSL_KEY_OLDEST=2` and run `docker compose up -d` to stop accepting V1 tokens. JWTs and resource tokens signed with V1 stop working at that point. Files already encrypted in storage are not re-encrypted, so keep `_APP_OPENSSL_KEY_V1` configured.
- **Realtime and MQTT clients**: a connection over 100 subscriptions, channels or queries now gets an error, and a user's 33rd connection on one worker is closed. Raise the realtime limits with the variables above if your app needs more.
- **Bulk writes**: bulk update, upsert and delete from client sessions now return 401. They need an API key or a console user.
- **API keys**: a key that creates a migration now needs the write scope of every resource it imports, and a key that configures a function or site can only add scopes it holds itself.
- **MFA**: turning MFA off now needs a recent MFA challenge, and a TOTP code can be used once.
- **PostgreSQL**: the bundled PostgreSQL starts with `max_connections=500`. Set `_APP_DB_MAX_CONNECTIONS` lower on small hosts.
- **Functions domain**: if you changed `_APP_DOMAIN_FUNCTIONS`, list the old domain in `_APP_DOMAIN_FUNCTIONS_FALLBACK` to keep serving preview URLs on it.
- **Maintenance**: `_APP_MAINTENANCE_INTERVAL` should be a multiple of 300 seconds. The default, 86400, is.
- **New optional variables**: `_APP_OPENSSL_KEY_V2`, `_APP_OPENSSL_KEY_CURRENT`, `_APP_OPENSSL_KEY_OLDEST`, `_APP_LIMIT_REALTIME_SUBSCRIPTIONS`, `_APP_LIMIT_REALTIME_CHANNELS`, `_APP_LIMIT_REALTIME_QUERIES`, `_APP_DB_MAX_CONNECTIONS` and `_APP_DOMAIN_FUNCTIONS_FALLBACK`.
### Contributors
Thank you to everyone who contributed to 2.7.0:
@SahilArate @Spandan-Mishra @ItsRoy69 @flcrom @ChiragAgg5k @adityaoberai @loks0n @atharvadeosthale @ArnabChatterjee20k @abnegate @HarshMN2345 @Meldiron @TorstenDittmann @callmegautam @eldadfux @lohanidamodar @imtia33 @fogelito @shimonewman @aishwaripahwa12 @jaysomani @levivannoort
**Full Changelog**: https://github.com/appwrite/appwrite/compare/2.6.2...2.7.0