X Tutup
Skip to content

feat: backup policy migration support#11409

Open
premtsd-code wants to merge 1616 commits intomainfrom
feat-backup-policy-migration
Open

feat: backup policy migration support#11409
premtsd-code wants to merge 1616 commits intomainfrom
feat-backup-policy-migration

Conversation

@premtsd-code
Copy link

Summary

  • Add policies.read and policies.write scopes to the migration worker's internal JWT so backup policies can be created on the destination project
  • Add backup policy resource to MigrationReport model and SDK spec Format

Test plan

  • Trigger an Appwrite-to-Appwrite migration with backup-policy in the resources list
  • Verify the migration completes without 401 scope errors
  • Verify backup policies are created on the destination project

fogelito and others added 30 commits February 15, 2026 13:47
Fix: cast redi ping response to boolean
…d column create routes

- Add encrypt parameter to Varchar, Text, Mediumtext, Longtext attribute create routes
- Add encrypt parameter to Varchar, Text, Mediumtext, Longtext column create routes
- Add encrypt rule to AttributeVarchar, AttributeText, AttributeMediumtext, AttributeLongtext response models
- Add encrypt rule to ColumnVarchar, ColumnText, ColumnMediumtext, ColumnLongtext response models
- Add plan injection and validation for encrypt feature in all routes
- Add size validation for encrypt on varchar route (variable size)
- Add encrypt filter handling in all routes

Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
chore: fix readme in agent skills sdk
…ttribute and column types

Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
…types

Add `encrypt` param to varchar, text, mediumtext, longtext attribute/column create routes
- Fix $sequence always cast to string (was incorrectly cast to int for numeric values) in Document.php and Row.php
- Add supportForMultipleFulltextIndexes, supportForAttributeResizing, supportForSchemas console variables for MongoDB adapter
- Fix testCreateIndexes to use getSupportForMultipleFulltextIndexes() instead of getSupportForRelationships() for fulltext index error branching
- Fix testAttributeRowWidthLimit/testColumnRowWidthLimit to skip on databases without schema support (MongoDB)
- Fix testAttributeUpdateStringResize/testColumnUpdateStringResize to skip on databases without attribute resizing support (MongoDB)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Mails worker span logs were missing project.id, project.sequence,
project.region, and project.database because setProject was never
called on queueForMails. This adds setProject in the shared API
controller and in workers (Webhooks, Migrations) that trigger mails.
Also injects project into the Mails worker action.
Set project on queueForMails for span log context
abnegate and others added 25 commits February 24, 2026 19:07
With 120+ jobs starting simultaneously, transient failures are common
due to resource contention. Adding retry_wait_seconds: 300 to all
php-retry action usages gives the CI environment time to settle
before retrying failed tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When setupAttributes() or setupIndexes() timed out during
waitForAllAttributes/waitForAllIndexes, the cache was never populated.
Subsequent test methods calling setup again would find empty cache,
reuse the same collection IDs (from collection cache), and attempt to
re-create existing attributes/indexes, resulting in 409 Conflict errors.

Fix: populate the static cache immediately after successful creation
(202 assertions), before the wait calls. This ensures retries skip
creation and go straight to waiting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix: add missing sites bandwidth to project usage
Same cache-before-wait pattern as DatabasesBase to prevent
cascading 409 failures when attribute polling times out.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	composer.lock
…d-method

Update composer.lock and enhance error logging in HTTP requests
Feat fix for missing null safety on console redirects
# Conflicts:
#	composer.lock
#	src/Appwrite/Platform/Tasks/SDKs.php
…ect-usage

Revert "Fix: add missing sites bandwidth to project usage"
* Upgrade `utopia-php/vcs`

* fix

* bump
- Add migration resource enum mappings in Format.php for Appwrite, Firebase, Supabase, and NHost
- Add backup-policy field to MigrationReport model
- Add backups.read/write scopes to migration API key
- Point utopia-php/migration and utopia-php/abuse to dev branches with SDK v20 support
… JWT

The migration worker's internal API key was missing the scopes needed
to create backup policies on the destination project.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 25, 2026

📝 Walkthrough

Walkthrough

This PR implements a comprehensive update to the Appwrite framework, including documentation additions, GitHub Actions workflow upgrades (v4→v6 for checkout and related actions), environment variable additions and modifications, Dockerfile updates with new base image versions and artifacts, extensive configuration changes across collections and schemas (adding labels attributes, removing fulltext indices, introducing new platform-level settings for sites and functions), removal of three entire API controller files (avatars, health, storage), and a broad refactoring of remaining API controllers to replace App framework usage with Http equivalents, migrate parameter definitions to use dynamic UID length factories, and update dependency injection patterns. Additional changes include CLI refactoring to centralize resource registration, collection schema modifications for platform/projects structures with new resource-type-based indexing, and email template HTML restructuring.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • loks0n
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: backup policy migration support' accurately and concisely summarizes the main change: adding backup policy migration capability to the system.
Description check ✅ Passed The description clearly relates to the changeset, explaining the addition of migration scopes for backup policies and the model updates needed to support backup policy migration.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-backup-policy-migration

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

Security Scan Results for PR

Docker Image Scan Results

Package Version Vulnerability Severity
libecpg 18.1-r0 CVE-2026-2004 HIGH
libecpg 18.1-r0 CVE-2026-2005 HIGH
libecpg 18.1-r0 CVE-2026-2006 HIGH
libecpg 18.1-r0 CVE-2026-2007 HIGH
libecpg-dev 18.1-r0 CVE-2026-2004 HIGH
libecpg-dev 18.1-r0 CVE-2026-2005 HIGH
libecpg-dev 18.1-r0 CVE-2026-2006 HIGH
libecpg-dev 18.1-r0 CVE-2026-2007 HIGH
libheif 1.20.2-r1 CVE-2025-68431 HIGH
libpng 1.6.54-r0 CVE-2026-25646 HIGH
libpng-dev 1.6.54-r0 CVE-2026-25646 HIGH
libpq 18.1-r0 CVE-2026-2004 HIGH
libpq 18.1-r0 CVE-2026-2005 HIGH
libpq 18.1-r0 CVE-2026-2006 HIGH
libpq 18.1-r0 CVE-2026-2007 HIGH
libpq-dev 18.1-r0 CVE-2026-2004 HIGH
libpq-dev 18.1-r0 CVE-2026-2005 HIGH
libpq-dev 18.1-r0 CVE-2026-2006 HIGH
libpq-dev 18.1-r0 CVE-2026-2007 HIGH
postgresql18-dev 18.1-r0 CVE-2026-2004 HIGH
postgresql18-dev 18.1-r0 CVE-2026-2005 HIGH
postgresql18-dev 18.1-r0 CVE-2026-2006 HIGH
postgresql18-dev 18.1-r0 CVE-2026-2007 HIGH

Source Code Scan Results

🎉 No vulnerabilities found!

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (9)
app/cli.php (1)

206-233: ⚠️ Potential issue | 🟠 Major

Bug: $database captured by value — connection reuse never works.

The $database variable on line 209 is captured by value (use (..., $database, ...)), so reassignment on line 216 only affects the local copy. On the next invocation the closure still sees $database === null, making the early-return optimisation on lines 210–213 dead code. Every call allocates a fresh adapter/connection.

Compare with getProjectDB (line 147) which correctly uses &$databases.

🐛 Proposed fix
-    return function (?Document $project = null) use ($pools, $cache, $database, $authorization) {
+    return function (?Document $project = null) use ($pools, $cache, &$database, $authorization) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/cli.php` around lines 206 - 233, The closure assigned to
$setResource('getLogsDB') captures $database by value so reassignments inside
the closure don't persist; change the capture to a reference (use (&$database,
...) instead of use (..., $database, ...)) so the newly constructed Database
instance stored at line where $database is assigned persists across invocations
and the early-return block (checking $database !== null) works as intended;
mirror the pattern used by getProjectDB to ensure connection reuse.
app/controllers/general.php (1)

1662-1662: ⚠️ Potential issue | 🔴 Critical

Path traversal guard is a no-op due to operator precedence — pre-existing security bug.

!\substr(…) === $base is parsed as (!substr(…)) === $base, which is always false. The check never prevents directory traversal. While this line isn't new in this PR, it's a critical security flaw worth fixing.

Proposed fix
-        if (!\substr($absolute, 0, \strlen($base)) === $base) {
+        if (\substr($absolute, 0, \strlen($base)) !== $base) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/general.php` at line 1662, The path traversal check uses
incorrect operator precedence: `!\substr($absolute, 0, \strlen($base)) ===
$base` is always false. Replace this with a proper string-prefix comparison such
as `\substr($absolute, 0, \strlen($base)) !== $base` or `\strncmp($absolute,
$base, \strlen($base)) !== 0` (or `strpos($absolute, $base) !== 0`) so the
condition correctly detects when $absolute does not start with $base; update the
if that references \substr, $absolute and $base accordingly.
app/config/templates/site.php (1)

253-259: ⚠️ Potential issue | 🟠 Major

Duplicate outputDirectory key — likely meant to be fallbackFile.

The getFramework override array for Vitepress has outputDirectory defined twice (Lines 255 and 258). PHP silently uses the last value, so '404.html' on Line 255 is discarded. This looks like it was intended to be 'fallbackFile' => '404.html'. The same pattern repeats for Vuepress (Line 278) and Docusaurus (Line 301).

Proposed fix for all three templates

Vitepress (Line 255):

-                'outputDirectory' => '404.html',
+                'fallbackFile' => '404.html',

Vuepress (Line 278):

-                'outputDirectory' => '404.html',
+                'fallbackFile' => '404.html',

Docusaurus (Line 301):

-                'outputDirectory' => '404.html',
+                'fallbackFile' => '404.html',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/config/templates/site.php` around lines 253 - 259, Replace the duplicated
'outputDirectory' => '404.html' entry inside the getFramework('VITE', ...)
override with 'fallbackFile' => '404.html' so the Vitepress template uses a
fallback file instead of silently overwriting outputDirectory; apply the same
change to the corresponding getFramework override entries for Vuepress and
Docusaurus where the duplicate 'outputDirectory' is present (replace that
duplicate key with 'fallbackFile' => '404.html').
app/config/locale/templates/email-base.tpl (1)

13-35: ⚠️ Potential issue | 🟡 Minor

Dark mode selectors have lower specificity than the new .main-scoped light mode selectors.

Light mode button/link styles are now scoped to .main a.button (Lines 71, 82-83), but the dark mode media query on Lines 21-25 still targets unscoped a.button. Since .main a.button has higher specificity than a.button, the dark mode overrides may fail to apply even with !important in some email clients that partially support specificity rules.

Consider scoping the dark mode selectors to .main as well for consistency:

Proposed fix
 `@media` (prefers-color-scheme: dark ) {
-    body {
+    .main {
         color: `#616b7c` !important;
         background-color: `#ffffff` !important;
     }
-    a {
+    .main a {
         color: currentColor !important;
     }
-    a.button {
+    .main a.button {
         color: `#ffffff` !important;
         background-color: `#2D2D31` !important;
         border-color: `#414146` !important;
     }
-    h1, h2, h3 {
+    .main h1, .main h2, .main h3 {
         color: `#373b4d` !important;
     }
-    h4 {
+    .main h4 {
         color: `#4f5769` !important;
     }
-    p.security-phrase:not(:empty), hr {
+    .main p.security-phrase:not(:empty), .main hr {
         border-color: `#e8e9f0` !important;
     }
 }

Also applies to: 67-85

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/config/locale/templates/email-base.tpl` around lines 13 - 35, The
dark-mode media query selectors target unscoped elements (e.g., a.button, a,
.button) while the light-mode rules are scoped under the .main container (e.g.,
.main a.button), causing specificity conflicts; update the dark-mode selectors
inside the `@media` (prefers-color-scheme: dark) block to mirror the .main scoping
(e.g., .main a.button, .main a, .main h1/h2/h3, .main p.security-phrase, .main
hr) so they have matching specificity to the light-mode rules and retain the
intended overrides across email clients.
app/config/templates/function.php (1)

1530-1539: ⚠️ Potential issue | 🟠 Major

Duplicate template ID can break deterministic template selection.

Both entries use id: "generate-with-replicate" (see Line 1531 and Line 1655). IDs should be unique to avoid collisions in lookups, filtering, and SDK/UI mapping.

Also applies to: 1653-1664

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/config/templates/function.php` around lines 1530 - 1539, The template ID
"generate-with-replicate" is duplicated (seen in the block with 'id' =>
'generate-with-replicate' and again later), which can cause selection/collision
issues; update one of the template entries to a unique identifier (e.g.,
'generate-with-replicate-v2' or similar) and propagate that change wherever that
template ID is referenced (SDK/UI mapping, lookups, or filters). Locate the
entries by the 'id' => 'generate-with-replicate' symbol and the surrounding
metadata (e.g., 'name' => 'Generate with Replicate', 'useCases' =>
[FunctionUseCases::AI]) to ensure you change the correct block and update any
dependent code or documentation that references the old ID.
app/controllers/api/vcs.php (1)

637-645: ⚠️ Potential issue | 🟠 Major

Add missing ->inject('request') to the PATCH endpoint.

The action closure at line 645 declares Request $request as a parameter and uses it at line 702 when calling $createGitDeployments, but the dependency is not declared in the inject chain. The POST endpoint at /v1/vcs/github/events (line 487) explicitly includes ->inject('request') and follows the same pattern, confirming this is required for the framework to properly resolve the dependency.

Proposed fix
    ->inject('gitHub')
+   ->inject('request')
    ->inject('response')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/api/vcs.php` around lines 637 - 645, The PATCH endpoint's DI
chain is missing ->inject('request') while the action closure declares Request
$request and passes it to $createGitDeployments; add ->inject('request') into
the inject chain (e.g., alongside ->inject('gitHub')->inject('response')...
before ->action) so the framework can resolve the Request parameter used in the
action and when calling $createGitDeployments.
app/controllers/api/migrations.php (1)

453-457: ⚠️ Potential issue | 🔴 Critical

Duplicate setProject() and missing setPlatform() and setUser() on CSV import queue trigger.

Line 456 calls ->setProject($project) twice, and more critically, both ->setPlatform($platform) and ->setUser($user) are missing. The Migration event's preparePayload() expects both platform and user in the worker payload. While $platform is injected into this endpoint, 'user' is not injected at all, which is a blocking issue—compare with the Appwrite (lines 97–102) and Firebase (lines 166–171) endpoints which both include user injection and call ->setUser($user).

Proposed fix
         $queueForMigrations
             ->setMigration($migration)
             ->setProject($project)
-            ->setProject($project)
+            ->setPlatform($platform)
             ->trigger();

Also add ->inject('user') to the endpoint declaration so ->setUser($user) can be called.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/api/migrations.php` around lines 453 - 457, The CSV import
queue trigger currently calls ->setProject($project) twice and omits
->setPlatform($platform) and ->setUser($user); update the endpoint to include
->inject('user') so a $user is available, replace the duplicate ->setProject
call with ->setPlatform($platform), and add ->setUser($user) before ->trigger()
(refer to the methods setProject, setPlatform, setUser and the trigger call to
locate the code and to the endpoint declaration to add inject('user')).
app/controllers/api/account.php (1)

706-709: ⚠️ Potential issue | 🔴 Critical

Fix invalid sessionId assignment after bulk session deletion.

Line 708 uses $session outside the loop. If no sessions exist, this can crash; if sessions exist, it can report the wrong session ID (last iterated, not the one used for payload/deletes).

🐛 Proposed fix
+        $eventSessionId = null;
         foreach ($sessions as $session) {/** `@var` Document $session */
             $dbForProject->deleteDocument('sessions', $session->getId());
@@
             if ($proofForToken->verify($store->getProperty('secret', ''), $session->getAttribute('secret'))) {
                 $session->setAttribute('current', true);
+                $eventSessionId = $session->getId();
@@
         $queueForEvents
-            ->setParam('userId', $user->getId())
-            ->setParam('sessionId', $session->getId());
+            ->setParam('userId', $user->getId());
+
+        if ($eventSessionId !== null) {
+            $queueForEvents->setParam('sessionId', $eventSessionId);
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/api/account.php` around lines 706 - 709, The code sets
$queueForEvents->setParam('sessionId', $session->getId()) using $session outside
the loop which can be undefined or the wrong session; change it to use the
actual session id that corresponds to the payload/delete operation (e.g. use the
$sessionId variable coming from the request/payload or set the param inside the
loop using the loop-scoped $session->getId()), and if there may be no session
ensure you pass null or skip setting 'sessionId' to avoid referencing an
undefined $session; update the call sites around $queueForEvents, setParam, and
any loop that deletes sessions accordingly.
app/controllers/api/messaging.php (1)

2504-2510: ⚠️ Potential issue | 🟠 Major

Use the cursor Document directly in topic pagination.

$dbForProject->getDocument('topics', $topicId) returns a single Document; indexing with [0] is incorrect here and can break cursor pagination.

✅ Proposed fix
-            $cursor->setValue($cursorDocument[0]);
+            $cursor->setValue($cursorDocument);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/api/messaging.php` around lines 2504 - 2510, The code treats
$cursorDocument (returned by $dbForProject->getDocument('topics', $topicId))
like a collection and uses $cursorDocument[0], which is wrong; update the cursor
assignment to use the Document directly by passing $cursorDocument into
$cursor->setValue (keep the existing existence check using
$cursorDocument->isEmpty()), i.e., replace the indexed access with
$cursor->setValue($cursorDocument) so topic pagination uses the actual Document
returned by getDocument.
🧹 Nitpick comments (10)
app/cli.php (1)

144-204: Optional: extract duplicated shared-tables configuration.

The shared-tables logic (explode env, check host, set sharedTables/tenant/namespace) is duplicated between the reuse path (lines 161–173) and the new-connection path (lines 182–194). A small helper would eliminate this.

♻️ Sketch
$configureSharedTables = function (Database $database, DSN $dsn, Document $project) {
    $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
    if (\in_array($dsn->getHost(), $sharedTables)) {
        $database
            ->setSharedTables(true)
            ->setTenant((int)$project->getSequence())
            ->setNamespace($dsn->getParam('namespace'));
    } else {
        $database
            ->setSharedTables(false)
            ->setTenant(null)
            ->setNamespace('_' . $project->getSequence());
    }
};

Then call $configureSharedTables($database, $dsn, $project); in both branches.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/cli.php` around lines 144 - 204, The duplicated shared-tables
configuration in the getProjectDB resource (the explode/check/in_array +
setSharedTables/setTenant/setNamespace logic present in both the reuse branch
and the new-connection branch) should be extracted into a small helper closure
(e.g., $configureSharedTables) that accepts (Database $database, DSN $dsn,
Document $project) and performs the
explode(System::getEnv('_APP_DATABASE_SHARED_TABLES','')), in_array check and
the two set* branches; then replace the duplicated blocks by calling
$configureSharedTables($database, $dsn, $project) in both the reuse path (where
$database is taken from $databases[$dsn->getHost()]) and the new-connection path
(after creating $database and storing it in $databases).
app/controllers/general.php (4)

65-65: $utopia parameter is unused in router().

Static analysis (PHPMD) confirms $utopia is never referenced inside the function body. If it's not needed, removing it would clean up an already long (18-param) signature and all its call sites.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/general.php` at line 65, The router() function has an unused
$utopia parameter; remove $utopia from the function signature in the router
declaration and then update all call sites that pass a $utopia argument to stop
passing it (or adjust argument order accordingly). Search for references to the
router function and update invocations, unit tests, and any dependency
injection/container bindings that list the $utopia parameter so the parameter
list and calls remain consistent; ensure no other code inside router still
expects $utopia before committing.

1559-1592: Consider extracting shared router() injection list to reduce duplication.

The same ~18 .inject() calls and identical argument lists appear in at least 4 route handlers (init, options, robots.txt, humans.txt). Every time the router() signature changes (as happened in this PR), all sites must be updated in lockstep. Extracting the common injections or wrapping the router call in a higher-order helper would reduce this maintenance burden.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/general.php` around lines 1559 - 1592, Extract the repeated
injection list and router invocation into a shared helper to avoid duplicated
.inject(...) chains and long action signatures: create a function (e.g.,
registerRouterWrappedRoute or routerInjectedAction) that accepts the Http route
builder (or route path) and sets the common injections (the ~18 symbols used
now) once, then installs an action which calls router(...) with the shared
argument list; update handlers like Http::get('/robots.txt'), the init and
options and humans.txt routes to call this helper instead of repeating the
.inject(...) calls and the full action signature so future router signature
changes are made in one place.

1507-1507: Inconsistent development-mode check: Http::isDevelopment() vs $utopia->isDevelopment().

Line 1507 uses the static Http::isDevelopment() while line 1555 uses the instance method $utopia->isDevelopment(). They likely return the same value, but using one form consistently would be cleaner.

Also applies to: 1555-1555

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/general.php` at line 1507, The code uses two different checks
for development mode (static Http::isDevelopment() at the $output assignment and
instance $utopia->isDevelopment() later); make them consistent by picking one
approach and updating the other call to match (e.g., replace
Http::isDevelopment() with $utopia->isDevelopment(), or vice versa) so both
checks use the same symbol (Http::isDevelopment() or $utopia->isDevelopment())
throughout the method.

959-967: Migration host bypasses HTTPS enforcement — ensure it's restricted to internal/trusted values.

Adding _APP_MIGRATION_HOST to $localHosts causes it to skip the forced-HTTPS redirect (line 1014) and receive a null cookie domain. This is fine for internal service-to-service calls but could weaken security if misconfigured. Consider documenting that this env var must only be set to a trusted internal hostname.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/general.php` around lines 959 - 967, The migration host from
System::getEnv('_APP_MIGRATION_HOST') is being treated as a local host (added to
$localHosts) which disables HTTPS enforcement via the $isLocalHost check;
restrict adding the migration host by validating it against a trusted allowlist
or explicit opt-in before pushing it into $localHosts — for example, only append
$migrationHost (and $migrationHost.':'.$request->getPort()) if it matches a
configured TRUSTED_MIGRATION_HOSTS list or if an explicit flag like
_APP_MIGRATION_HOST_TRUSTED is set to true; update the logic around
System::getEnv('_APP_MIGRATION_HOST'), $localHosts and $isLocalHost accordingly
and document the new requirement so only internal/trusted values can bypass
HTTPS.
app/config/cors.php (1)

10-48: Remove Set-Cookie and add Authorization to the CORS allowedHeaders.

Set-Cookie is a response-only header and should not appear in request headers. More importantly, the Authorization header is missing but is used in the codebase for bearer token authentication (e.g., in executor service). Browser preflight requests will fail for endpoints that require bearer auth if this header is not explicitly allowed.

Current issue in app/config/cors.php (lines 10-48)
'allowedHeaders' => [
    'Accept',
    'Origin',
    'Cookie',
    'Set-Cookie',  // ← Remove: response header, not request header
    // ... other headers ...
    // Missing: 'Authorization', required for bearer token auth
],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/config/cors.php` around lines 10 - 48, In the 'allowedHeaders' array in
app/config/cors.php, remove the response-only header 'Set-Cookie' and add the
'Authorization' header so browser preflight requests allow bearer tokens; update
the 'allowedHeaders' entry (the array assigned to the 'allowedHeaders' key) to
drop 'Set-Cookie' and include 'Authorization' alongside the other headers
referenced for SDK and server-to-server requests (this affects endpoints that
use bearer auth, e.g., the executor service).
app/config/templates/site.php (1)

28-31: Minor: PHPDoc tag should be @return, not @var.

@var describes a property; @return describes a method's return type.

Proposed fix
     /**
-     * `@var` array<string>
+     * `@return` array<string>
      */
     public static function getAll(): array
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/config/templates/site.php` around lines 28 - 31, The PHPDoc above the
public static function getAll(): array incorrectly uses the `@var` tag; change it
to `@return` to correctly document the method's return type. Locate the docblock
for the getAll method and replace the `@var` string annotation with `@return`
array<string> (matching the declared return type) so the docblock accurately
reflects the method signature.
app/controllers/shared/api.php (1)

298-320: Role suffix extraction is fragile for hyphenated role names, though current roles don't contain hyphens.

Line 313 uses substr($adminRole, strrpos($adminRole, '-') + 1) to extract the role suffix from project-{projectId}-{role} strings. This approach finds the last hyphen, which works for current role names (owner, developer), but would fail if a role name itself contained hyphens (e.g., project-x-team-lead would extract lead instead of team-lead). While no hyphenated roles exist in the codebase today, consider using a more explicit extraction strategy based on the known format to make the code more robust against future role naming changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/shared/api.php` around lines 298 - 320, The suffix extraction
for project-scoped roles is fragile: instead of using substr($adminRole,
strrpos($adminRole, '-') + 1) inside the match branch that handles
project-scoped roles, strip the known prefix ("project-" plus the project id and
the following hyphen) to obtain the full role name (e.g., remove
"project-{$projectId}-" from $adminRole) so hyphenated role names (like
"team-lead") are preserved; update the code in the block that computes $role
(the match on str_starts_with($adminRole, 'project-')), ensure
$roles[$role]['scopes'] and $authorization->addRole($role) continue to work with
the new extraction, and fall back to the existing branch for team-wide roles.
app/config/variables.php (1)

473-479: Treat _APP_REDIS_PASS as a secret field in metadata.

Line 479 uses an empty filter while other password fields in this file use password. Keeping this consistent helps prevent accidental plaintext exposure in tooling that honors these filters.

🔐 Proposed update
             [
                 'name' => '_APP_REDIS_PASS',
                 'description' => 'Redis server password. This is an optional variable. Default value is an empty string.',
                 'introduction' => '0.7',
                 'default' => '',
                 'required' => false,
                 'question' => '',
-                'filter' => ''
+                'filter' => 'password'
             ],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/config/variables.php` around lines 473 - 479, The _APP_REDIS_PASS
variable metadata currently has an empty 'filter' and should be treated as a
secret like other password fields: update the metadata for the '_APP_REDIS_PASS'
entry (the associative array with keys 'name' => '_APP_REDIS_PASS',
'description', 'default', etc.) to set 'filter' => 'password' so tooling will
treat it as secret; keep 'required' => false and other fields unchanged to
preserve behavior.
app/config/platform.php (1)

7-14: Trim comma-separated domain/schema tokens before storing them.

Line 9, Line 13, and Line 27 currently keep raw tokens. Values like "sites.example.com, preview.example.com" can preserve whitespace and produce invalid host/schema matches.

💡 Proposed refactor
 $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
 if (\str_contains($sitesDomain, ',')) {
-    $sitesDomain = explode(',', $sitesDomain)[0];
+    $sitesDomain = \trim(explode(',', $sitesDomain)[0]);
+} else {
+    $sitesDomain = \trim($sitesDomain);
 }
 $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
 if (\str_contains($functionsDomain, ',')) {
-    $functionsDomain = explode(',', $functionsDomain)[0];
+    $functionsDomain = \trim(explode(',', $functionsDomain)[0]);
+} else {
+    $functionsDomain = \trim($functionsDomain);
 }
@@
-    'schemas' => \array_filter(\explode(',', System::getEnv('_APP_CONSOLE_SCHEMA', ''))),
+    'schemas' => \array_values(\array_filter(\array_map('trim', \explode(',', System::getEnv('_APP_CONSOLE_SCHEMA', ''))))),

Also applies to: 27-27

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/config/platform.php` around lines 7 - 14, The code stores comma-separated
env tokens into $sitesDomain and $functionsDomain without trimming, which can
leave leading/trailing whitespace and break host/schema comparisons; update the
assignments to trim the environment value and the selected token: use
$sitesDomain = trim(System::getEnv('_APP_DOMAIN_SITES', '')); and when splitting
use $sitesDomain = trim(explode(',', $sitesDomain)[0]); do the same for
$functionsDomain, and apply the same trim pattern to the other similar
assignment referenced (the token on line 27) so all stored domain/schema
variables are whitespace-trimmed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.claude/skills/pr-workflow/SKILL.md:
- Around line 5-8: Update the hard-coded guidance that says "Appwrite does NOT
use `main` as the primary development branch" so it doesn't force contributors
to never target main; instead, change the wording in SKILL.md to a generic,
accurate instruction that tells contributors to check the repository's current
target branch (e.g., consult CONTRIBUTING or the repo's branch naming/versioning
policy) and to open PRs against the appropriate version or default branch
(examples: `1.8.x`, `1.9.x`, or the repo default). Locate and edit the exact
text block that contains the sentence "Appwrite does NOT use `main` as the
primary development branch" and the surrounding lines referred to in the comment
(the sections around lines 5–8, 17–25, and 29–31) to make the guidance
conditional and repository-agnostic. Ensure the new text removes the absolute
"never target main" phrasing and replaces it with concise, actionable guidance
to verify the correct base branch before creating a PR.

In @.env:
- Around line 42-45: The .env now sets COMPOSE_PROFILES=mongodb and
_APP_DB_ADAPTER=mongodb pointing to _APP_DB_HOST=mongodb:_APP_DB_PORT=27017 but
docker-compose.yml still only defines the mariadb service, so running
docker-compose will fail; either revert the .env to the mariadb defaults (set
COMPOSE_PROFILES to empty or remove, and set _APP_DB_ADAPTER to mariadb with
_APP_DB_HOST=mariadb and _APP_DB_PORT=3306) or complete the migration by
updating docker-compose.yml to replace the mariadb service with a mongodb
service and ensure all service dependencies and env references now point to the
mongodb service name; make sure to remove or update the "Change default to
'mongodb' after next release" TODO consistently across config and code.

In @.github/workflows/specs.yml:
- Around line 81-83: The copy can silently skip if the destination directory
doesn't exist; before the cp command inside the if [ "${VERSION}" = "latest" ]
block, ensure the target directory "specs/latest" exists (mkdir -p
"specs/latest") and remove the trailing "|| true" so failures surface; update
the block around VERSION and SPECS_DIR to create the directory first, then run
cp ${GITHUB_WORKSPACE}/${SPECS_DIR}/*latest*.json "specs/latest/".
- Around line 55-69: The workflow uses the repository-scoped GH_TOKEN env (env:
GH_TOKEN) for cross-repo operations (see git clone
"https://x-access-token:${GH_TOKEN}@github.com/appwrite/specs.git" with TARGET).
Replace GH_TOKEN with a repository-scoped secret that has access to
appwrite/specs (e.g., SPECS_REPO_TOKEN stored in repository secrets or
organization secrets), update the env entries and all other occurrences (the
other block at lines ~98–109) to use SPECS_REPO_TOKEN, and ensure the token has
repo write permissions and the workflow includes appropriate permissions
(contents: write) so clone/push/PR actions to appwrite/specs succeed.

In @.github/workflows/tests.yml:
- Around line 295-297: The workflow is using GitHub Actions expression
interpolation in the php-retry action command for container env vars
(_APP_DB_ADAPTER, _APP_DB_HOST, _APP_DB_PORT); instead replace the `${{ env.*
}}` interpolation with runtime shell variable pass-through by either using
Docker's name-only `-e _APP_DB_ADAPTER -e _APP_DB_HOST -e _APP_DB_PORT` or
referencing the shell variables directly (`-e
_APP_DB_ADAPTER="${_APP_DB_ADAPTER}"` etc.) in the php-retry command so the
matrix-set values from GITHUB_ENV propagate reliably at execution time.

In `@AGENTS.md`:
- Around line 65-73: Remove the stray orphaned path
`docs/specs/authentication.drawio.svg` from the "Pull Request Guidelines" ->
"Before Submitting" checklist (it currently appears as an isolated line after
the checklist items); edit the AGENTS.md content near the "Before Submitting"
heading to delete that lone path so the checklist reads cleanly and
consistently.

In `@app/config/collections/platform.php`:
- Around line 1941-1964: The code appends attributes to
$platformCollections['teams'] and $platformCollections['users'] without checking
those keys exist, which can silently fail if merge order changes; update the
block that adds the 'keys' attribute (the entries with ID::custom('keys') and
filters 'subQueryOrganizationKeys' and 'subQueryAccountKeys') to first assert or
initialize the target collections—e.g., check
isset($platformCollections['teams']) and isset($platformCollections['users'])
and throw or create the collection array before pushing attributes—so the
mutation is explicit and fails fast if the collections are missing.

In `@app/config/collections/projects.php`:
- Around line 571-579: The schema currently allows negative deploymentRetention
because 'signed' => true for the field with '$id' =>
ID::custom('deploymentRetention'); update both occurrences of that field to set
'signed' => false and add a schema-level minimum constraint (e.g., 'minimum' =>
0) or equivalent validation rule so the integer cannot be negative; change the
entries for the deploymentRetention field (the array containing '$id' =>
ID::custom('deploymentRetention'), 'type' => Database::VAR_INTEGER) accordingly.

In `@app/config/sdks.php`:
- Around line 273-310: Add the missing 'package' key to the two SDK array
entries identified by 'key' => 'agent-skills' and 'key' => 'cursor-plugin';
follow the same naming convention used by other SDK entries (e.g., the package
string used elsewhere for console SDKs) and place it alongside 'name',
'version', 'url', etc., so each entry includes a 'package' field consistent with
the rest of the sdks structure.

In `@app/controllers/api/projects.php`:
- Around line 1104-1109: The handler type for the 'scopes' parameter must be
made nullable to match the route validator: change the action signatures that
currently use array $scopes to ?array $scopes (the handlers defined as function
(..., array $scopes, ?string $expire, ...) in the key endpoints), and in both
endpoints referenced update any downstream use of $scopes to safely handle null
(e.g. treat null as an empty array or check for null before iterating) so
runtime TypeError is avoided; apply this change to both key-related action
handlers mentioned in the review.

In `@app/controllers/general.php`:
- Around line 1137-1142: The ownership check using str_ends_with($domain->get(),
$appwriteDomain) can false-positive match subdomains (e.g.,
notfunctions.example.com vs functions.example.com); update the foreach block
that iterates $appwriteDomains to only mark owner as 'Appwrite' when the domain
exactly equals $appwriteDomain or when it ends with a dot plus the appwrite
domain (use $domain->get() === $appwriteDomain || str_ends_with($domain->get(),
'.' . $appwriteDomain)); adjust the condition in the loop that references
$appwriteDomains, $domain->get(), and $appwriteDomain accordingly.
- Around line 105-116: The subdomain check using str_ends_with($host,
$denyDomain) can falsely match unrelated domains (e.g., "notexample.com");
update the second condition inside the foreach over $denyDomains to check for a
dot-prefixed suffix boundary, e.g. use str_ends_with($host, '.' . $denyDomain)
(or normalize $denyDomain to avoid double dots) so only true subdomains match;
keep the existing exact-match branch (if ($host === $denyDomain)) as-is and
throw the same AppwriteException when the dot-prefixed check passes.

In `@app/controllers/mock.php`:
- Line 193: Update the call in mock controller so
Config::getParam('projectScopes') provides a defensive default; replace the
current call used to set $scopes with a call that passes an empty array as the
second argument (i.e., use Config::getParam('projectScopes', [])) so array_keys
always receives an array and $scopes is safe; locate the usage in the mock
controller where $scopes is assigned and update it accordingly.

In `@CHANGES.md`:
- Around line 7-98: The 1.8.1 release notes in CHANGES.md are missing the
backup-policy migration change; update the 1.8.1 section to mention the
migration behavior change that adds policies.read / policies.write scope support
and the new backup-policy report resource so users know to expect the migration;
insert a succinct bullet under the 1.8.1 Fixes/Miscellaneous area referencing
"backup-policy migration: add policies.read/policies.write scopes and
backup-policy report resource" and ensure the phrasing matches surrounding
bullets for consistency.

In `@CONTRIBUTING.md`:
- Line 254: Replace the incorrect ASCII pipe character with the box-drawing
vertical bar in the file tree block: locate the line containing "├── DSN"
(currently preceded by "|") in CONTRIBUTING.md and change the leading "|" to "│"
so the tree uses consistent box-drawing characters and renders correctly.

In `@Dockerfile`:
- Around line 40-42: Remove the development-only artifacts from the base image:
delete the three COPY directives that add ./dev, mongo-init.js, and
mongo-entrypoint.sh from the base stage so they are not inherited by production;
then add those three COPY directives into the development stage (and remove any
duplicate COPY ./dev there) so the files are present only for the development
image. Locate the COPY lines referencing "./dev", "mongo-init.js", and
"mongo-entrypoint.sh" in the Dockerfile and move them from the base stage to the
development stage (ensuring no duplicate COPY ./dev remains).

---

Outside diff comments:
In `@app/cli.php`:
- Around line 206-233: The closure assigned to $setResource('getLogsDB')
captures $database by value so reassignments inside the closure don't persist;
change the capture to a reference (use (&$database, ...) instead of use (...,
$database, ...)) so the newly constructed Database instance stored at line where
$database is assigned persists across invocations and the early-return block
(checking $database !== null) works as intended; mirror the pattern used by
getProjectDB to ensure connection reuse.

In `@app/config/locale/templates/email-base.tpl`:
- Around line 13-35: The dark-mode media query selectors target unscoped
elements (e.g., a.button, a, .button) while the light-mode rules are scoped
under the .main container (e.g., .main a.button), causing specificity conflicts;
update the dark-mode selectors inside the `@media` (prefers-color-scheme: dark)
block to mirror the .main scoping (e.g., .main a.button, .main a, .main
h1/h2/h3, .main p.security-phrase, .main hr) so they have matching specificity
to the light-mode rules and retain the intended overrides across email clients.

In `@app/config/templates/function.php`:
- Around line 1530-1539: The template ID "generate-with-replicate" is duplicated
(seen in the block with 'id' => 'generate-with-replicate' and again later),
which can cause selection/collision issues; update one of the template entries
to a unique identifier (e.g., 'generate-with-replicate-v2' or similar) and
propagate that change wherever that template ID is referenced (SDK/UI mapping,
lookups, or filters). Locate the entries by the 'id' =>
'generate-with-replicate' symbol and the surrounding metadata (e.g., 'name' =>
'Generate with Replicate', 'useCases' => [FunctionUseCases::AI]) to ensure you
change the correct block and update any dependent code or documentation that
references the old ID.

In `@app/config/templates/site.php`:
- Around line 253-259: Replace the duplicated 'outputDirectory' => '404.html'
entry inside the getFramework('VITE', ...) override with 'fallbackFile' =>
'404.html' so the Vitepress template uses a fallback file instead of silently
overwriting outputDirectory; apply the same change to the corresponding
getFramework override entries for Vuepress and Docusaurus where the duplicate
'outputDirectory' is present (replace that duplicate key with 'fallbackFile' =>
'404.html').

In `@app/controllers/api/account.php`:
- Around line 706-709: The code sets $queueForEvents->setParam('sessionId',
$session->getId()) using $session outside the loop which can be undefined or the
wrong session; change it to use the actual session id that corresponds to the
payload/delete operation (e.g. use the $sessionId variable coming from the
request/payload or set the param inside the loop using the loop-scoped
$session->getId()), and if there may be no session ensure you pass null or skip
setting 'sessionId' to avoid referencing an undefined $session; update the call
sites around $queueForEvents, setParam, and any loop that deletes sessions
accordingly.

In `@app/controllers/api/messaging.php`:
- Around line 2504-2510: The code treats $cursorDocument (returned by
$dbForProject->getDocument('topics', $topicId)) like a collection and uses
$cursorDocument[0], which is wrong; update the cursor assignment to use the
Document directly by passing $cursorDocument into $cursor->setValue (keep the
existing existence check using $cursorDocument->isEmpty()), i.e., replace the
indexed access with $cursor->setValue($cursorDocument) so topic pagination uses
the actual Document returned by getDocument.

In `@app/controllers/api/migrations.php`:
- Around line 453-457: The CSV import queue trigger currently calls
->setProject($project) twice and omits ->setPlatform($platform) and
->setUser($user); update the endpoint to include ->inject('user') so a $user is
available, replace the duplicate ->setProject call with
->setPlatform($platform), and add ->setUser($user) before ->trigger() (refer to
the methods setProject, setPlatform, setUser and the trigger call to locate the
code and to the endpoint declaration to add inject('user')).

In `@app/controllers/api/vcs.php`:
- Around line 637-645: The PATCH endpoint's DI chain is missing
->inject('request') while the action closure declares Request $request and
passes it to $createGitDeployments; add ->inject('request') into the inject
chain (e.g., alongside ->inject('gitHub')->inject('response')... before
->action) so the framework can resolve the Request parameter used in the action
and when calling $createGitDeployments.

In `@app/controllers/general.php`:
- Line 1662: The path traversal check uses incorrect operator precedence:
`!\substr($absolute, 0, \strlen($base)) === $base` is always false. Replace this
with a proper string-prefix comparison such as `\substr($absolute, 0,
\strlen($base)) !== $base` or `\strncmp($absolute, $base, \strlen($base)) !== 0`
(or `strpos($absolute, $base) !== 0`) so the condition correctly detects when
$absolute does not start with $base; update the if that references \substr,
$absolute and $base accordingly.

---

Nitpick comments:
In `@app/cli.php`:
- Around line 144-204: The duplicated shared-tables configuration in the
getProjectDB resource (the explode/check/in_array +
setSharedTables/setTenant/setNamespace logic present in both the reuse branch
and the new-connection branch) should be extracted into a small helper closure
(e.g., $configureSharedTables) that accepts (Database $database, DSN $dsn,
Document $project) and performs the
explode(System::getEnv('_APP_DATABASE_SHARED_TABLES','')), in_array check and
the two set* branches; then replace the duplicated blocks by calling
$configureSharedTables($database, $dsn, $project) in both the reuse path (where
$database is taken from $databases[$dsn->getHost()]) and the new-connection path
(after creating $database and storing it in $databases).

In `@app/config/cors.php`:
- Around line 10-48: In the 'allowedHeaders' array in app/config/cors.php,
remove the response-only header 'Set-Cookie' and add the 'Authorization' header
so browser preflight requests allow bearer tokens; update the 'allowedHeaders'
entry (the array assigned to the 'allowedHeaders' key) to drop 'Set-Cookie' and
include 'Authorization' alongside the other headers referenced for SDK and
server-to-server requests (this affects endpoints that use bearer auth, e.g.,
the executor service).

In `@app/config/platform.php`:
- Around line 7-14: The code stores comma-separated env tokens into $sitesDomain
and $functionsDomain without trimming, which can leave leading/trailing
whitespace and break host/schema comparisons; update the assignments to trim the
environment value and the selected token: use $sitesDomain =
trim(System::getEnv('_APP_DOMAIN_SITES', '')); and when splitting use
$sitesDomain = trim(explode(',', $sitesDomain)[0]); do the same for
$functionsDomain, and apply the same trim pattern to the other similar
assignment referenced (the token on line 27) so all stored domain/schema
variables are whitespace-trimmed.

In `@app/config/templates/site.php`:
- Around line 28-31: The PHPDoc above the public static function getAll(): array
incorrectly uses the `@var` tag; change it to `@return` to correctly document the
method's return type. Locate the docblock for the getAll method and replace the
`@var` string annotation with `@return` array<string> (matching the declared return
type) so the docblock accurately reflects the method signature.

In `@app/config/variables.php`:
- Around line 473-479: The _APP_REDIS_PASS variable metadata currently has an
empty 'filter' and should be treated as a secret like other password fields:
update the metadata for the '_APP_REDIS_PASS' entry (the associative array with
keys 'name' => '_APP_REDIS_PASS', 'description', 'default', etc.) to set
'filter' => 'password' so tooling will treat it as secret; keep 'required' =>
false and other fields unchanged to preserve behavior.

In `@app/controllers/general.php`:
- Line 65: The router() function has an unused $utopia parameter; remove $utopia
from the function signature in the router declaration and then update all call
sites that pass a $utopia argument to stop passing it (or adjust argument order
accordingly). Search for references to the router function and update
invocations, unit tests, and any dependency injection/container bindings that
list the $utopia parameter so the parameter list and calls remain consistent;
ensure no other code inside router still expects $utopia before committing.
- Around line 1559-1592: Extract the repeated injection list and router
invocation into a shared helper to avoid duplicated .inject(...) chains and long
action signatures: create a function (e.g., registerRouterWrappedRoute or
routerInjectedAction) that accepts the Http route builder (or route path) and
sets the common injections (the ~18 symbols used now) once, then installs an
action which calls router(...) with the shared argument list; update handlers
like Http::get('/robots.txt'), the init and options and humans.txt routes to
call this helper instead of repeating the .inject(...) calls and the full action
signature so future router signature changes are made in one place.
- Line 1507: The code uses two different checks for development mode (static
Http::isDevelopment() at the $output assignment and instance
$utopia->isDevelopment() later); make them consistent by picking one approach
and updating the other call to match (e.g., replace Http::isDevelopment() with
$utopia->isDevelopment(), or vice versa) so both checks use the same symbol
(Http::isDevelopment() or $utopia->isDevelopment()) throughout the method.
- Around line 959-967: The migration host from
System::getEnv('_APP_MIGRATION_HOST') is being treated as a local host (added to
$localHosts) which disables HTTPS enforcement via the $isLocalHost check;
restrict adding the migration host by validating it against a trusted allowlist
or explicit opt-in before pushing it into $localHosts — for example, only append
$migrationHost (and $migrationHost.':'.$request->getPort()) if it matches a
configured TRUSTED_MIGRATION_HOSTS list or if an explicit flag like
_APP_MIGRATION_HOST_TRUSTED is set to true; update the logic around
System::getEnv('_APP_MIGRATION_HOST'), $localHosts and $isLocalHost accordingly
and document the new requirement so only internal/trusted values can bypass
HTTPS.

In `@app/controllers/shared/api.php`:
- Around line 298-320: The suffix extraction for project-scoped roles is
fragile: instead of using substr($adminRole, strrpos($adminRole, '-') + 1)
inside the match branch that handles project-scoped roles, strip the known
prefix ("project-" plus the project id and the following hyphen) to obtain the
full role name (e.g., remove "project-{$projectId}-" from $adminRole) so
hyphenated role names (like "team-lead") are preserved; update the code in the
block that computes $role (the match on str_starts_with($adminRole,
'project-')), ensure $roles[$role]['scopes'] and $authorization->addRole($role)
continue to work with the new extraction, and fall back to the existing branch
for team-wide roles.

Comment on lines +5 to +8
**Important:** Appwrite does NOT use `main` as the primary development branch.

PRs should target the **current version branch** (e.g., `1.8.x`, `1.9.x`), not `main`.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Hard-coded “never target main” guidance is likely incorrect for this repo workflow.

This instruction conflicts with the current PR targeting behavior and can cause contributors/agents to open PRs against the wrong base branch.

🛠️ Proposed documentation fix
-**Important:** Appwrite does NOT use `main` as the primary development branch.
-
-PRs should target the **current version branch** (e.g., `1.8.x`, `1.9.x`), not `main`.
+**Important:** Always target the repository’s currently documented base branch strategy.
+
+PRs should target the branch specified in the project’s contribution/release workflow
+(this may be `main` or a version branch like `1.8.x`, depending on release phase).
@@
-# Correct - targets the version branch
-gh pr create --base 1.8.x --title "Your PR title" --body "..."
+# Example - target the branch defined by current maintainer workflow
+gh pr create --base <target-branch> --title "Your PR title" --body "..."
@@
-# Wrong - do not target main
-gh pr create --base main ...
+# Only wrong when maintainer workflow says otherwise
+gh pr create --base <incorrect-branch> ...

Also applies to: 17-25, 29-31

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/pr-workflow/SKILL.md around lines 5 - 8, Update the
hard-coded guidance that says "Appwrite does NOT use `main` as the primary
development branch" so it doesn't force contributors to never target main;
instead, change the wording in SKILL.md to a generic, accurate instruction that
tells contributors to check the repository's current target branch (e.g.,
consult CONTRIBUTING or the repo's branch naming/versioning policy) and to open
PRs against the appropriate version or default branch (examples: `1.8.x`,
`1.9.x`, or the repo default). Locate and edit the exact text block that
contains the sentence "Appwrite does NOT use `main` as the primary development
branch" and the surrounding lines referred to in the comment (the sections
around lines 5–8, 17–25, and 29–31) to make the guidance conditional and
repository-agnostic. Ensure the new text removes the absolute "never target
main" phrasing and replaces it with concise, actionable guidance to verify the
correct base branch before creating a PR.

Comment on lines +42 to +45
COMPOSE_PROFILES=mongodb
_APP_DB_ADAPTER=mongodb
_APP_DB_HOST=mongodb
_APP_DB_PORT=27017
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, locate docker-compose files and understand the DB setup
echo "=== Locating docker-compose files ==="
fd -t f 'docker-compose' | head -20

echo ""
echo "=== Checking DB references in docker-compose files ==="
fd -t f 'docker-compose' -x cat {} \; 2>/dev/null | rg -n 'mariadb|mongodb|mysql|postgres' -i -C2 | head -100

Repository: appwrite/appwrite

Length of output: 2319


🏁 Script executed:

#!/bin/bash
# Check what DB adapters are supported/configured in the codebase
echo "=== DB_ADAPTER references in config/source files ==="
rg -t php -t js -t yaml 'DB_ADAPTER|db_adapter' -C2 | head -150

echo ""
echo "=== Check README or docs for DB defaults ==="
fd -t f '(README|INSTALL|SETUP|DATABASE)' -i | xargs cat 2>/dev/null | rg -i 'database|db.*adapter|mariadb|mongodb' -C2 | head -100

Repository: appwrite/appwrite

Length of output: 13685


🏁 Script executed:

#!/bin/bash
# Check the current .env file structure to understand the context
echo "=== Viewing .env file around the DB configuration ==="
cat -n .env | sed -n '30,50p'

Repository: appwrite/appwrite

Length of output: 878


MongoDB switch in .env breaks the main docker-compose setup — must revert or complete the migration.

The .env (lines 42-45) now configures _APP_DB_ADAPTER=mongodb and points to mongodb:27017, but docker-compose.yml still defines only mariadb as the database service and all containers depend on it. The COMPOSE_PROFILES=mongodb setting has no effect since no conditional mongodb service exists in the compose file.

This configuration will fail when developers run docker-compose up on main. Either revert the .env to mariadb defaults, or complete the docker-compose migration by removing the mariadb service and adding the mongodb service. The codebase still lists mariadb as the default (with a TODO comment "Change default to 'mongodb' after next release"), indicating this migration is incomplete.

🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 42-42: [UnorderedKey] The COMPOSE_PROFILES key should go before the _APP_COMPRESSION_ENABLED key

(UnorderedKey)


[warning] 43-43: [UnorderedKey] The _APP_DB_ADAPTER key should go before the _APP_DNS key

(UnorderedKey)


[warning] 44-44: [UnorderedKey] The _APP_DB_HOST key should go before the _APP_DNS key

(UnorderedKey)


[warning] 45-45: [UnorderedKey] The _APP_DB_PORT key should go before the _APP_DNS key

(UnorderedKey)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.env around lines 42 - 45, The .env now sets COMPOSE_PROFILES=mongodb and
_APP_DB_ADAPTER=mongodb pointing to _APP_DB_HOST=mongodb:_APP_DB_PORT=27017 but
docker-compose.yml still only defines the mariadb service, so running
docker-compose will fail; either revert the .env to the mariadb defaults (set
COMPOSE_PROFILES to empty or remove, and set _APP_DB_ADAPTER to mariadb with
_APP_DB_HOST=mariadb and _APP_DB_PORT=3306) or complete the migration by
updating docker-compose.yml to replace the mariadb service with a mongodb
service and ensure all service dependencies and env references now point to the
mongodb service name; make sure to remove or update the "Change default to
'mongodb' after next release" TODO consistently across config and code.

Comment on lines +55 to +69
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ inputs.version }}"
MESSAGE="${{ inputs.message }}"
GIT_BRANCH="feat-${VERSION}-specs"
SPECS_DIR="./app/config/specs"
EXAMPLES_DIR="./docs/examples/${VERSION}"
TARGET="/tmp/specs-repo"

sudo chown -R $USER:$USER "${SPECS_DIR}"

# Clone the specs repo
git clone "https://x-access-token:${GH_TOKEN}@github.com/appwrite/specs.git" "${TARGET}"
cd "${TARGET}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

GitHub Actions GITHUB_TOKEN cross-repository write push commits pull requests same organization

💡 Result:

  • GITHUB_TOKEN is repo-scoped. It’s a GitHub App installation access token created for each job, and its permissions are limited to the repository that contains the workflow—so it cannot push commits, open PRs, or write to a different repository, even if the other repo is in the same organization. [1]
  • To write cross-repository (push/PR) you must use a different credential that has access to the target repo, e.g. a fine-grained PAT, classic PAT, or a GitHub App installation token granted to that repo. [2]
  • Within the same repository, commits/PRs via GITHUB_TOKEN require granting write scopes in the workflow (and org/repo defaults may be read-only for newer repos/orgs): set e.g. permissions: contents: write and (for PRs) pull-requests: write. [3][4]
  • If you’re creating PRs from Actions, ensure the repo/org setting “Allow GitHub Actions to create and approve pull requests” is enabled. [5]

Sources
[1] https://docs.github.com/en/actions/concepts/security/github_token
[2] https://docs.github.com/en/actions/tutorials/authenticate-with-github_token (see “Granting additional permissions”)
[3] https://docs.github.com/en/actions/tutorials/authenticate-with-github_token (see “Modifying the permissions for the GITHUB_TOKEN”)
[4] https://github.blog/changelog/2023-02-02-github-actions-updating-the-default-github_token-permissions-to-read-only
[5] https://github.com/marketplace/actions/create-pull-request (Workflow permissions section)


Use a repository-scoped token for cross-repo operations.

The GITHUB_TOKEN at line 56 is limited to the repository containing the workflow and cannot push commits or create pull requests in appwrite/specs, even within the same organization. A separate token with explicit access to the target repository is required.

🔧 Suggested fix
       - name: Push to appwrite/specs and create PR
         if: inputs.push
         env:
-          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          GH_TOKEN: ${{ secrets.SPECS_REPO_TOKEN }}
         run: |

Also applies to: 98–109

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/specs.yml around lines 55 - 69, The workflow uses the
repository-scoped GH_TOKEN env (env: GH_TOKEN) for cross-repo operations (see
git clone "https://x-access-token:${GH_TOKEN}@github.com/appwrite/specs.git"
with TARGET). Replace GH_TOKEN with a repository-scoped secret that has access
to appwrite/specs (e.g., SPECS_REPO_TOKEN stored in repository secrets or
organization secrets), update the env entries and all other occurrences (the
other block at lines ~98–109) to use SPECS_REPO_TOKEN, and ensure the token has
repo write permissions and the workflow includes appropriate permissions
(contents: write) so clone/push/PR actions to appwrite/specs succeed.

Comment on lines +81 to +83
if [ "${VERSION}" = "latest" ]; then
cp ${GITHUB_WORKSPACE}/${SPECS_DIR}/*latest*.json "specs/latest/" 2>/dev/null || true
fi
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Ensure specs/latest exists before copy to avoid silent skips.

On Line 82, copy can fail if specs/latest/ is missing, and || true hides it.

🔧 Suggested fix
           # Copy latest specs if version is latest
           if [ "${VERSION}" = "latest" ]; then
+            mkdir -p "specs/latest/"
             cp ${GITHUB_WORKSPACE}/${SPECS_DIR}/*latest*.json "specs/latest/" 2>/dev/null || true
           fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/specs.yml around lines 81 - 83, The copy can silently skip
if the destination directory doesn't exist; before the cp command inside the if
[ "${VERSION}" = "latest" ] block, ensure the target directory "specs/latest"
exists (mkdir -p "specs/latest") and remove the trailing "|| true" so failures
surface; update the block around VERSION and SPECS_DIR to create the directory
first, then run cp ${GITHUB_WORKSPACE}/${SPECS_DIR}/*latest*.json
"specs/latest/".

Comment on lines +295 to +297
-e _APP_DB_ADAPTER="${{ env._APP_DB_ADAPTER }}" \
-e _APP_DB_HOST="${{ env._APP_DB_HOST }}" \
-e _APP_DB_PORT="${{ env._APP_DB_PORT }}" \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the workflow file around the mentioned lines
fd tests.yml --type f | head -5

Repository: appwrite/appwrite

Length of output: 43


🏁 Script executed:

# Get the full context of the workflow file
wc -l .github/workflows/tests.yml

Repository: appwrite/appwrite

Length of output: 92


🏁 Script executed:

# Read the relevant sections - around lines 235-249 and 295-297
sed -n '230,300p' .github/workflows/tests.yml | cat -n

Repository: appwrite/appwrite

Length of output: 3786


🌐 Web query:

GitHub Actions environment variable $GITHUB_ENV set-env interpolation reliability ${{ env.VAR }}

💡 Result:

Setting an env var via the environment file (echo "VAR=value" >> "$GITHUB_ENV") is reliable only for subsequent steps in the same job: the step that writes to GITHUB_ENV cannot read the new value (neither as $VAR nor as ${{ env.VAR }}) because the runner applies the file after the step finishes. [1]

Also, remember that ${{ ... }} is an expression/context interpolation performed by GitHub Actions before a step is sent to / executed on the runner, while $VAR is shell interpolation done by the runner during the run: script. [2] This is why mixing “set in run:” + “use as ${{ env.VAR }}” in the same step is not reliable.

Practical rules:

  • Same step: compute/use with shell variables (VAR=...; echo "$VAR"), and optionally persist for later with >> $GITHUB_ENV. [1]
  • Later steps (same job): you can use either $VAR or ${{ env.VAR }} and it will work. (Commonly demonstrated/confirmed in practice.) [1]
  • Need the value in if: / other expression-only fields: use a step output via $GITHUB_OUTPUT, then reference ${{ steps.<id>.outputs.<name> }} (since if: is evaluated by GitHub Actions, not the shell). [2]

Example pattern (safe):

- name: Compute and persist
  id: v
  run: |
    VERSION="$(date +%Y%m%d)"
    echo "VERSION=$VERSION" >> "$GITHUB_ENV"
    echo "version=$VERSION" >> "$GITHUB_OUTPUT"

- name: Use in shell
  run: echo "$VERSION"

- name: Use in expressions
  if: ${{ steps.v.outputs.version != '' }}
  run: echo "ok"

Sources: GitHub Docs on GITHUB_ENV behavior (available to subsequent steps only) [1], and on contexts/expression interpolation vs runner/shell interpolation and where each can be used [2].


🌐 Web query:

GitHub Actions docker -e VAR pass-through environment variable matrix

💡 Result:

To pass a matrix value into docker run -e in GitHub Actions, set an env var from the matrix, then pass it to Docker.

Minimal pattern (recommended)

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        var: [foo, bar]   # matrix.var
    steps:
      - name: Run container with VAR
        env:
          VAR: ${{ matrix.var }}
        run: |
          docker run --rm -e VAR="$VAR" myimage:latest

GitHub matrix values are available as the matrix context (e.g., ${{ matrix.var }}), and env: sets runner environment variables you can use inside run: steps. [1][2][3]

Shorter Docker pass-through (host → container)

Because Docker lets you specify an env name without a value to propagate the host value into the container, you can do:

- env:
    VAR: ${{ matrix.var }}
  run: docker run --rm -e VAR myimage:latest

Docker documents this “name-only -e VAR propagates current host value” behavior. [4]

Sources:
[1] https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs
[2] https://docs.github.com/en/actions/concepts/workflows-and-actions/contexts
[3] https://docs.github.com/actions/reference/environment-variables
[4] https://docs.docker.com/engine/containers/run/


Use runtime shell variable pass-through, not ${{ env.* }} interpolation in action command parameters.

Lines 295–297 receive matrix-dependent values from lines 235–249 via $GITHUB_ENV. Instead of relying on expression interpolation (-e _APP_DB_ADAPTER="${{ env._APP_DB_ADAPTER }}"), pass the variables via shell reference or Docker's name-only -e syntax to ensure reliable propagation through the php-retry action's command execution context.

✅ Safer command wiring
             docker compose exec -T \
               -e _APP_DATABASE_SHARED_TABLES="" \
               -e _APP_DATABASE_SHARED_TABLES_V1="" \
-              -e _APP_DB_ADAPTER="${{ env._APP_DB_ADAPTER }}" \
-              -e _APP_DB_HOST="${{ env._APP_DB_HOST }}" \
-              -e _APP_DB_PORT="${{ env._APP_DB_PORT }}" \
+              -e _APP_DB_ADAPTER \
+              -e _APP_DB_HOST \
+              -e _APP_DB_PORT \
               -e _APP_DB_SCHEMA=appwrite \
               -e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
               appwrite vendor/bin/paratest --processes $(nproc) $FUNCTIONAL_FLAG "$SERVICE_PATH" --exclude-group abuseEnabled --exclude-group screenshots --exclude-group ciIgnore --log-junit tests/e2e/Services/${{ matrix.service }}/junit.xml
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
-e _APP_DB_ADAPTER="${{ env._APP_DB_ADAPTER }}" \
-e _APP_DB_HOST="${{ env._APP_DB_HOST }}" \
-e _APP_DB_PORT="${{ env._APP_DB_PORT }}" \
docker compose exec -T \
-e _APP_DATABASE_SHARED_TABLES="" \
-e _APP_DATABASE_SHARED_TABLES_V1="" \
-e _APP_DB_ADAPTER \
-e _APP_DB_HOST \
-e _APP_DB_PORT \
-e _APP_DB_SCHEMA=appwrite \
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
appwrite vendor/bin/paratest --processes $(nproc) $FUNCTIONAL_FLAG "$SERVICE_PATH" --exclude-group abuseEnabled --exclude-group screenshots --exclude-group ciIgnore --log-junit tests/e2e/Services/${{ matrix.service }}/junit.xml
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/tests.yml around lines 295 - 297, The workflow is using
GitHub Actions expression interpolation in the php-retry action command for
container env vars (_APP_DB_ADAPTER, _APP_DB_HOST, _APP_DB_PORT); instead
replace the `${{ env.* }}` interpolation with runtime shell variable
pass-through by either using Docker's name-only `-e _APP_DB_ADAPTER -e
_APP_DB_HOST -e _APP_DB_PORT` or referencing the shell variables directly (`-e
_APP_DB_ADAPTER="${_APP_DB_ADAPTER}"` etc.) in the php-retry command so the
matrix-set values from GITHUB_ENV propagate reliably at execution time.

Comment on lines +1137 to +1142
foreach ($appwriteDomains as $appwriteDomain) {
if (\str_ends_with($domain->get(), $appwriteDomain)) {
$owner = 'Appwrite';
break;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same str_ends_with subdomain-matching issue as the deny-domains block.

Line 1138 uses str_ends_with($domain->get(), $appwriteDomain) without a dot prefix. A domain like notfunctions.example.com could be incorrectly marked as Appwrite-owned if $appwriteDomain is functions.example.com. The impact here is lower (wrong ownership tag), but it's worth fixing for consistency.

Proposed fix
-                   if (\str_ends_with($domain->get(), $appwriteDomain)) {
+                   if ($domain->get() === $appwriteDomain || \str_ends_with($domain->get(), '.' . $appwriteDomain)) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/general.php` around lines 1137 - 1142, The ownership check
using str_ends_with($domain->get(), $appwriteDomain) can false-positive match
subdomains (e.g., notfunctions.example.com vs functions.example.com); update the
foreach block that iterates $appwriteDomains to only mark owner as 'Appwrite'
when the domain exactly equals $appwriteDomain or when it ends with a dot plus
the appwrite domain (use $domain->get() === $appwriteDomain ||
str_ends_with($domain->get(), '.' . $appwriteDomain)); adjust the condition in
the loop that references $appwriteDomains, $domain->get(), and $appwriteDomain
accordingly.

}

$scopes = array_keys(Config::getParam('scopes'));
$scopes = array_keys(Config::getParam('projectScopes'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find projectScopes and scopes configuration/usage across the codebase
echo "=== Searching for projectScopes usage ==="
rg -n "projectScopes" app config src --type-not min -C 2 || echo "No projectScopes found"

echo -e "\n=== Searching for Config::getParam/setParam patterns ==="
rg -n "Config::(get|set)Param" app config src --type-not min -C 1 || echo "No Config param methods found"

echo -e "\n=== Searching for legacy 'scopes' param usage ==="
rg -n "getParam\\(['\"]scopes['\"]\\)|'scopes'|\"scopes\"" app config src --type-not min -C 1 || echo "No legacy scopes found"

echo -e "\n=== Checking config files ==="
find app config -name "*.php" -type f | head -20 | xargs -I {} sh -c 'echo "File: {}" && head -50 {} | rg -n "projectScopes|scopes|getParam|setParam" || true'

Repository: appwrite/appwrite

Length of output: 1456


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Checking if app/controllers/mock.php exists ==="
if [ -f "app/controllers/mock.php" ]; then
  echo "File exists"
  echo "=== Line 193 context ==="
  sed -n '190,195p' app/controllers/mock.php
else
  echo "File not found at app/controllers/mock.php"
  find . -name "mock.php" -type f 2>/dev/null | head -5
fi

echo -e "\n=== All Config::getParam calls in entire repository ==="
rg "Config::getParam" -n --max-count 30

echo -e "\n=== Searching for 'scopes' in Config context ==="
rg "scopes" -n -B2 -A2 --max-count 20

Repository: appwrite/appwrite

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for projectScopes initialization/definition ==="
rg "projectScopes" -B3 -A3 --type-not min

echo -e "\n=== Check if Config::getParam has a default for projectScopes ==="
rg "projectScopes" -n --type-not min | head -20

Repository: appwrite/appwrite

Length of output: 164


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Find projectScopes usage pattern comparison ==="
rg "projectScopes" -n -B2 -A2 | head -100

echo -e "\n=== Check if projectScopes is set in Config anywhere ==="
rg "setParam.*projectScopes|projectScopes.*=" -n

echo -e "\n=== Check config files for scopes definitions ==="
find app/config -name "*.php" -type f -exec grep -l "projectScopes\|scopes" {} \;

Repository: appwrite/appwrite

Length of output: 6497


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check if scopes/project.php config file exists ==="
if [ -f "app/config/scopes/project.php" ]; then
  echo "File exists. Content:"
  cat app/config/scopes/project.php
else
  echo "File NOT found"
fi

echo -e "\n=== Check Config class for setParam/getParam method ==="
find . -name "Config.php" -o -name "config.php" | grep -i config | head -5 | xargs -I {} sh -c 'echo "=== {} ===" && head -100 {}'

echo -e "\n=== Verify all projectScopes usages to see if inconsistency exists ==="
rg "Config::getParam\(['\"]projectScopes" -n

Repository: appwrite/appwrite

Length of output: 14686


Add default empty array to ensure defensive coding consistency.

array_keys(Config::getParam('projectScopes')) at line 193 lacks a default value. While projectScopes is properly initialized in app/init/configs.php, other usages like Screenshot.php:193 defensively provide a default: Config::getParam('projectScopes', []). Update to match this safe pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/mock.php` at line 193, Update the call in mock controller so
Config::getParam('projectScopes') provides a defensive default; replace the
current call used to set $scopes with a call that passes an empty array as the
second argument (i.e., use Config::getParam('projectScopes', [])) so array_keys
always receives an array and $scopes is safe; locate the usage in the mock
controller where $scopes is assigned and update it accordingly.

Comment on lines +7 to +98
* Add branch deployments support in [#10486](https://github.com/appwrite/appwrite/pull/10486)
* Add TanStack Start sites support in [#10681](https://github.com/appwrite/appwrite/pull/10681)
* Add Next.js standalone support in [#10747](https://github.com/appwrite/appwrite/pull/10747)
* Add Resend integration in [#10690](https://github.com/appwrite/appwrite/pull/10690)
* Add option to enable/disable image transformations per-bucket in [#10722](https://github.com/appwrite/appwrite/pull/10722)
* Add operators support in [#10735](https://github.com/appwrite/appwrite/pull/10735) and [#10800](https://github.com/appwrite/appwrite/pull/10800)
* Add function and sites stats in [#10786](https://github.com/appwrite/appwrite/pull/10786)
* Add disable count feature in [#10668](https://github.com/appwrite/appwrite/pull/10668)
* Add ElevenLabs site template in [#10782](https://github.com/appwrite/appwrite/pull/10782)
* Add suggested environment variables in [#10795](https://github.com/appwrite/appwrite/pull/10795)
* Update GeoDB database in [#10890](https://github.com/appwrite/appwrite/pull/10890)
* Update Flutter default build runtime in [#10807](https://github.com/appwrite/appwrite/pull/10807)
* Upgrade runtimes in [#10804](https://github.com/appwrite/appwrite/pull/10804)

### Fixes

* Fix duplicate document error while creating file in [#10891](https://github.com/appwrite/appwrite/pull/10891)
* Fix "Update external deployment (authorize)" throwing 500 error due to invalid query in [#10888](https://github.com/appwrite/appwrite/pull/10888)
* Fix error setting user password in [#10889](https://github.com/appwrite/appwrite/pull/10889)
* Fix error generating email MFA challenges in [#10884](https://github.com/appwrite/appwrite/pull/10884)
* Fix file token expiry in [#10877](https://github.com/appwrite/appwrite/pull/10877)
* Fix TanStack Nitro default in [#10860](https://github.com/appwrite/appwrite/pull/10860)
* Fix TanStack builds in [#10767](https://github.com/appwrite/appwrite/pull/10767)
* Fix nullable validation in [#10819](https://github.com/appwrite/appwrite/pull/10819) and [#10778](https://github.com/appwrite/appwrite/pull/10778)
* Fix WebP library in [#10738](https://github.com/appwrite/appwrite/pull/10738)
* Fix batch writes in [#10812](https://github.com/appwrite/appwrite/pull/10812)
* Fix error handler error in [#10719](https://github.com/appwrite/appwrite/pull/10719)
* Fix Next 16 compatibility in [#10713](https://github.com/appwrite/appwrite/pull/10713)
* Fix stats usage memory leak in [#10683](https://github.com/appwrite/appwrite/pull/10683)
* Fix author URL in template deployments in [#10535](https://github.com/appwrite/appwrite/pull/10535)
* Fix VCS lock deletion in [#10691](https://github.com/appwrite/appwrite/pull/10691)

### Miscellaneous

* Add CSV export functionality in [#10546](https://github.com/appwrite/appwrite/pull/10546), [#10750](https://github.com/appwrite/appwrite/pull/10750), [#10813](https://github.com/appwrite/appwrite/pull/10813), and [#10847](https://github.com/appwrite/appwrite/pull/10847)
* Add JWT disposition in [#10867](https://github.com/appwrite/appwrite/pull/10867)
* Add screenshots endpoint in [#10675](https://github.com/appwrite/appwrite/pull/10675)
* Add screenshot endpoint stats in [#10706](https://github.com/appwrite/appwrite/pull/10706)
* Add users attributes in [#10688](https://github.com/appwrite/appwrite/pull/10688)
* Add max build duration environment variable in [#10674](https://github.com/appwrite/appwrite/pull/10674)
* Add custom realtime logger in [#10871](https://github.com/appwrite/appwrite/pull/10871)
* Add logs in [#10869](https://github.com/appwrite/appwrite/pull/10869)
* Improve MFA docs endpoint order in [#10793](https://github.com/appwrite/appwrite/pull/10793)
* Auth refactor in [#10758](https://github.com/appwrite/appwrite/pull/10758), [#10837](https://github.com/appwrite/appwrite/pull/10837), [#10682](https://github.com/appwrite/appwrite/pull/10682), and [#10667](https://github.com/appwrite/appwrite/pull/10667)
* Bump assistant to 0.8.4 in [#10887](https://github.com/appwrite/appwrite/pull/10887)
* Bump database to 3.1.5 in [#10766](https://github.com/appwrite/appwrite/pull/10766)
* Bump Utopia DNS in [#10761](https://github.com/appwrite/appwrite/pull/10761)
* Update domains to 0.8.3 in [#10658](https://github.com/appwrite/appwrite/pull/10658)
* Update domains to 0.9.1 in [#10678](https://github.com/appwrite/appwrite/pull/10678)
* Update Apple Swift to 13.3.0 in [#10679](https://github.com/appwrite/appwrite/pull/10679)
* Update Apple Swift in [#10663](https://github.com/appwrite/appwrite/pull/10663)
* Update CLI to 10.2.2 in [#10672](https://github.com/appwrite/appwrite/pull/10672)
* Update to CLI 12.0.0 in [#10853](https://github.com/appwrite/appwrite/pull/10853)
* Update docs examples to use Permission class in [#10707](https://github.com/appwrite/appwrite/pull/10707)
* Update SDK examples docs in [#10855](https://github.com/appwrite/appwrite/pull/10855)
* Release Python SDK in [#10762](https://github.com/appwrite/appwrite/pull/10762)
* Release Flutter 20.3.2 in [#10838](https://github.com/appwrite/appwrite/pull/10838)
* Release Flutter/Dart add screenshot examples in [#10811](https://github.com/appwrite/appwrite/pull/10811)
* Release PHP CLI in [#10791](https://github.com/appwrite/appwrite/pull/10791)
* Release SDKs in [#10817](https://github.com/appwrite/appwrite/pull/10817)
* Update SDKs in [#10694](https://github.com/appwrite/appwrite/pull/10694), [#10729](https://github.com/appwrite/appwrite/pull/10729), and [#10744](https://github.com/appwrite/appwrite/pull/10744)
* Update SDK generator in [#10743](https://github.com/appwrite/appwrite/pull/10743)
* Update database in [#10664](https://github.com/appwrite/appwrite/pull/10664)
* Update README file in [#10763](https://github.com/appwrite/appwrite/pull/10763)
* SDK release documentation in [#10745](https://github.com/appwrite/appwrite/pull/10745)
* SDK release runtime config in [#10765](https://github.com/appwrite/appwrite/pull/10765)
* Sync specs in [#10789](https://github.com/appwrite/appwrite/pull/10789)
* Sync 1.8.0 in [#10677](https://github.com/appwrite/appwrite/pull/10677)
* Add workflow for issue triage in [#10718](https://github.com/appwrite/appwrite/pull/10718)
* Add issue auto-labeler in [#10700](https://github.com/appwrite/appwrite/pull/10700)
* Add AI moderator repo in [#10717](https://github.com/appwrite/appwrite/pull/10717)
* Browser bump in [#10850](https://github.com/appwrite/appwrite/pull/10850)
* Template type enum override in [#10848](https://github.com/appwrite/appwrite/pull/10848)
* VCS reference type in [#10852](https://github.com/appwrite/appwrite/pull/10852)
* Index scope description in [#10851](https://github.com/appwrite/appwrite/pull/10851)
* Config for environment in [#10833](https://github.com/appwrite/appwrite/pull/10833)
* Format instance in [#10830](https://github.com/appwrite/appwrite/pull/10830)
* Replace sleep in webhooks service in [#10656](https://github.com/appwrite/appwrite/pull/10656)
* Update email composer in [#10720](https://github.com/appwrite/appwrite/pull/10720)
* Update facts on GitHub sites and functions in [#10593](https://github.com/appwrite/appwrite/pull/10593) and [#10771](https://github.com/appwrite/appwrite/pull/10771)
* Fix wrong user type in [#10875](https://github.com/appwrite/appwrite/pull/10875)
* Fix limit and offset computation in [#10880](https://github.com/appwrite/appwrite/pull/10880)
* Fix enum examples in [#10828](https://github.com/appwrite/appwrite/pull/10828)
* Fix response models multi-methods in [#10815](https://github.com/appwrite/appwrite/pull/10815)
* Fix undefined variable in [#10654](https://github.com/appwrite/appwrite/pull/10654)
* Fix undefined sequence in [#10652](https://github.com/appwrite/appwrite/pull/10652)
* Fix description in [#10702](https://github.com/appwrite/appwrite/pull/10702)
* Fix warning in builds worker in [#10705](https://github.com/appwrite/appwrite/pull/10705)
* Fix sites create deployment docs in [#10566](https://github.com/appwrite/appwrite/pull/10566)
* Fix test dependencies projects in [#10655](https://github.com/appwrite/appwrite/pull/10655)
* Fix list sites test in [#10726](https://github.com/appwrite/appwrite/pull/10726)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Include the backup-policy migration change in 1.8.1 notes.

The new 1.8.1 section does not mention this PR’s migration behavior change (policies.read / policies.write scope support and backup-policy report resource), which makes the release notes incomplete for migration users.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CHANGES.md` around lines 7 - 98, The 1.8.1 release notes in CHANGES.md are
missing the backup-policy migration change; update the 1.8.1 section to mention
the migration behavior change that adds policies.read / policies.write scope
support and the new backup-policy report resource so users know to expect the
migration; insert a succinct bullet under the 1.8.1 Fixes/Miscellaneous area
referencing "backup-policy migration: add policies.read/policies.write scopes
and backup-policy report resource" and ensure the phrasing matches surrounding
bullets for consistency.

│ ├── Auth
│ ├── Detector
│ ├── Docker
| ├── DSN
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix tree character typo in the file structure block.

Line 254 uses | instead of , which breaks the tree rendering consistency.

Suggested fix
-|       ├── DSN
+│       ├── DSN
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| ├── DSN
├── DSN
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CONTRIBUTING.md` at line 254, Replace the incorrect ASCII pipe character with
the box-drawing vertical bar in the file tree block: locate the line containing
"├── DSN" (currently preceded by "|") in CONTRIBUTING.md and change the leading
"|" to "│" so the tree uses consistent box-drawing characters and renders
correctly.

Comment on lines +40 to +42
COPY ./dev /usr/src/code/dev
COPY ./mongo-init.js /usr/src/code/mongo-init.js
COPY ./mongo-entrypoint.sh /usr/src/code/mongo-entrypoint.sh
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find and examine mongo-init.js and mongo-entrypoint.sh for sensitive content
fd -t f 'mongo-init.js|mongo-entrypoint.sh' --exec head -50 {}

Repository: appwrite/appwrite

Length of output: 1024


🏁 Script executed:

# Also check the Dockerfile structure to confirm base/production/development stage inheritance
fd -t f 'Dockerfile' --exec cat -n {} \;

Repository: appwrite/appwrite

Length of output: 5678


Move MongoDB and dev artifacts to development stage only.

Lines 40-42 copy ./dev, mongo-init.js, and mongo-entrypoint.sh into the base stage, which the production stage inherits (line 99). These are not used in production—the production stage runs only php app/http.php (line 109)—and add unnecessary bloat to the production image. Additionally, ./dev is copied again redundantly in the development stage (line 114).

Move these three COPY directives from the base stage to the development stage (after line 114), or remove them from base entirely if they are not needed in development either.

Proposed fix

Remove from base stage (lines 40-42):

 # Add Source Code
 COPY ./app /usr/src/code/app
 COPY ./public /usr/src/code/public
 COPY ./bin /usr/local/bin
 COPY ./src /usr/src/code/src
-COPY ./dev /usr/src/code/dev
-COPY ./mongo-init.js /usr/src/code/mongo-init.js
-COPY ./mongo-entrypoint.sh /usr/src/code/mongo-entrypoint.sh

Add to development stage (after line 114):

 COPY ./docs /usr/src/code/docs
 COPY ./dev /usr/src/code/dev
+COPY ./mongo-init.js /usr/src/code/mongo-init.js
+COPY ./mongo-entrypoint.sh /usr/src/code/mongo-entrypoint.sh

(Remove the redundant COPY ./dev from line 114 if consolidating into a single set of development-only files.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` around lines 40 - 42, Remove the development-only artifacts from
the base image: delete the three COPY directives that add ./dev, mongo-init.js,
and mongo-entrypoint.sh from the base stage so they are not inherited by
production; then add those three COPY directives into the development stage (and
remove any duplicate COPY ./dev there) so the files are present only for the
development image. Locate the COPY lines referencing "./dev", "mongo-init.js",
and "mongo-entrypoint.sh" in the Dockerfile and move them from the base stage to
the development stage (ensuring no duplicate COPY ./dev remains).

@github-actions
Copy link

✨ Benchmark results

  • Requests per second: 2,362
  • Requests with 200 status code: 425,128
  • P99 latency: 0.073704006

⚡ Benchmark Comparison

Metric This PR Latest version
RPS 2,362 1,288
200 425,128 231,936
P99 0.073704006 0.174587269

@github-actions
Copy link

github-actions bot commented Feb 25, 2026

🔄 PHP-Retry Summary

Flaky tests detected across commits:

Commit c64ae28 - 3 flaky tests
Test Retries Total Time Details
UsageTest::testDatabaseStatsCollectionsAPI 1 10.30s Logs
TablesDBCustomClientTest::testOneToManyRelationship 1 241.43s Logs
RealtimeCustomClientTest::testChannelDatabaseCollectionPermissions 1 30.19s Logs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

X Tutup