diff --git a/src/routes/(console)/project-[region]-[project]/+layout.ts b/src/routes/(console)/project-[region]-[project]/+layout.ts
index 1bec6520ec..8da91fdd80 100644
--- a/src/routes/(console)/project-[region]-[project]/+layout.ts
+++ b/src/routes/(console)/project-[region]-[project]/+layout.ts
@@ -106,7 +106,7 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => {
// Track console access for cloud projects (fire-and-forget, backend has 6-day cooldown).
// Skip if paused — user must explicitly resume via the paused project modal.
- if (isCloud && browser && project.status !== 'paused') {
+ if (isCloud && browser && project.status !== 'paused' && scopes.includes('projects.write')) {
generateFingerprintToken()
.then((fingerprint) => {
sdk.forConsole.client.headers['X-Appwrite-Console-Fingerprint'] = fingerprint;
diff --git a/src/routes/(console)/project-[region]-[project]/settings/+page.svelte b/src/routes/(console)/project-[region]-[project]/settings/+page.svelte
index 7e63a523be..3961307590 100644
--- a/src/routes/(console)/project-[region]-[project]/settings/+page.svelte
+++ b/src/routes/(console)/project-[region]-[project]/settings/+page.svelte
@@ -18,14 +18,12 @@
import UpdateVariables from '../updateVariables.svelte';
import { page } from '$app/state';
import UpdateLabels from './updateLabels.svelte';
+ import type { PageData } from './$types';
+ import { Alert } from '@appwrite.io/pink-svelte';
- export let data;
-
- let teamId: string = null;
+ let { data }: { data: PageData } = $props();
onMount(() => {
- teamId ??= $project.teamId;
-
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
@@ -87,25 +85,31 @@
{#if $project}
- {#if $canWriteProjects}
-
-
-
-
-
-
-
-
+ {#if !$canWriteProjects}
+
+ You can open this settings area, but editing project-level settings requires the
+ projects.write scope.
+
{/if}
+
+
+
+
+
+
+
+
+
{/if}
diff --git a/src/routes/(console)/project-[region]-[project]/settings/+page.ts b/src/routes/(console)/project-[region]-[project]/settings/+page.ts
index 321f06adaa..9c2b7955fb 100644
--- a/src/routes/(console)/project-[region]-[project]/settings/+page.ts
+++ b/src/routes/(console)/project-[region]-[project]/settings/+page.ts
@@ -3,14 +3,20 @@ import { sdk } from '$lib/stores/sdk';
import { Query } from '@appwrite.io/console';
import type { PageLoad } from './$types';
+function isReadonlySettingsPermissionError(error: unknown): boolean {
+ const code = (error as { code?: number } | null)?.code;
+ return code === 401 || code === 403;
+}
+
export const load: PageLoad = async ({ depends, url, params }) => {
depends(Dependencies.PROJECT_VARIABLES);
depends(Dependencies.PROJECT_INSTALLATIONS);
+
const limit = PAGE_LIMIT;
const offset = Number(url.searchParams.get('offset') ?? 0);
const variablesOffset = Number(url.searchParams.get('variablesOffset') ?? 0);
const projectSdk = sdk.forProject(params.region, params.project);
- const [variables, installations] = await Promise.all([
+ const [variablesResult, installationsResult] = await Promise.allSettled([
projectSdk.projectApi.listVariables({
queries: [Query.limit(limit), Query.offset(variablesOffset)]
}),
@@ -19,6 +25,38 @@ export const load: PageLoad = async ({ depends, url, params }) => {
})
]);
+ const variables =
+ variablesResult.status === 'fulfilled'
+ ? variablesResult.value
+ : (() => {
+ // Read-only users can be blocked from write-adjacent settings APIs.
+ // Only silence those permission errors so genuine load failures still surface.
+ if (!isReadonlySettingsPermissionError(variablesResult.reason)) {
+ throw variablesResult.reason;
+ }
+
+ return {
+ total: 0,
+ variables: []
+ };
+ })();
+
+ const installations =
+ installationsResult.status === 'fulfilled'
+ ? installationsResult.value
+ : (() => {
+ // Read-only users can be blocked from write-adjacent settings APIs.
+ // Only silence those permission errors so genuine load failures still surface.
+ if (!isReadonlySettingsPermissionError(installationsResult.reason)) {
+ throw installationsResult.reason;
+ }
+
+ return {
+ total: 0,
+ installations: []
+ };
+ })();
+
return {
limit,
offset,
diff --git a/src/routes/(console)/project-[region]-[project]/settings/changeOrganization.svelte b/src/routes/(console)/project-[region]-[project]/settings/changeOrganization.svelte
index eb3fb4a205..5c4fc8a36d 100644
--- a/src/routes/(console)/project-[region]-[project]/settings/changeOrganization.svelte
+++ b/src/routes/(console)/project-[region]-[project]/settings/changeOrganization.svelte
@@ -4,6 +4,7 @@
import { organizationList } from '$lib/stores/organization';
import { project } from '../store';
import TransferProjectModal from './transferProjectModal.svelte';
+ import { canWriteProjects } from '$lib/stores/roles';
let teamId: string;
let showTransfer = false;
@@ -18,6 +19,7 @@
id="organization"
placeholder="Select destination"
label="Move to"
+ disabled={!$canWriteProjects}
bind:value={teamId}
options={$organizationList.teams
.filter((team) => team.$id !== $project.teamId)
@@ -30,7 +32,7 @@
diff --git a/src/routes/(console)/project-[region]-[project]/settings/deleteProject.svelte b/src/routes/(console)/project-[region]-[project]/settings/deleteProject.svelte
index 0af70f0083..a39f59bf68 100644
--- a/src/routes/(console)/project-[region]-[project]/settings/deleteProject.svelte
+++ b/src/routes/(console)/project-[region]-[project]/settings/deleteProject.svelte
@@ -11,6 +11,7 @@
import { project, projectRegion } from '../store';
import { organization } from '$lib/stores/organization';
import { Dependencies } from '$lib/constants';
+ import { canWriteProjects } from '$lib/stores/roles';
let error: string;
let showDelete = false;
let name: string = null;
@@ -59,7 +60,9 @@
-
+
diff --git a/src/routes/(console)/project-[region]-[project]/settings/updateInstallations.svelte b/src/routes/(console)/project-[region]-[project]/settings/updateInstallations.svelte
index 5d61cf6a46..ca13336779 100644
--- a/src/routes/(console)/project-[region]-[project]/settings/updateInstallations.svelte
+++ b/src/routes/(console)/project-[region]-[project]/settings/updateInstallations.svelte
@@ -30,6 +30,7 @@
import type { ComponentType } from 'svelte';
import { Link } from '$lib/elements';
import { regionalConsoleVariables, mcpTools } from '../store';
+ import { canWriteProjects } from '$lib/stores/roles';
export let total: number;
export let limit: number;
@@ -87,6 +88,7 @@
{
trackEvent(Click.SettingsInstallProviderClick);
@@ -131,6 +133,7 @@
type="button"
class="button is-text is-only-icon"
aria-label="more options"
+ disabled={!$canWriteProjects}
on:click={toggle}>
@@ -186,7 +189,11 @@
title="No installation was added to the project yet"
description="Add an installation to connect repositories">
-
+
Connect to GitHub
diff --git a/src/routes/(console)/project-[region]-[project]/settings/updateLabels.svelte b/src/routes/(console)/project-[region]-[project]/settings/updateLabels.svelte
index cd12d50fd3..ef87a01f63 100644
--- a/src/routes/(console)/project-[region]-[project]/settings/updateLabels.svelte
+++ b/src/routes/(console)/project-[region]-[project]/settings/updateLabels.svelte
@@ -11,6 +11,7 @@
import { project } from '../store';
import { Tag, Input, Layout, Icon } from '@appwrite.io/pink-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
+ import { canWriteProjects } from '$lib/stores/roles';
const alphaNumericRegExp = /^[a-zA-Z0-9]+$/;
let suggestedLabels = ['live', 'stage', 'internal'];
@@ -73,6 +74,7 @@
id="project-labels"
label="Labels"
placeholder="Select or type project labels"
+ disabled={!$canWriteProjects}
bind:tags={labels} />
{/key}
@@ -81,6 +83,8 @@
size="s"
selected={labels.includes(suggestedLabel)}
on:click={() => {
+ if (!$canWriteProjects) return;
+
if (!labels.includes(suggestedLabel)) {
labels = [...labels, suggestedLabel];
} else {
@@ -98,7 +102,7 @@
-
+
diff --git a/src/routes/(console)/project-[region]-[project]/settings/updateName.svelte b/src/routes/(console)/project-[region]-[project]/settings/updateName.svelte
index 7b9091cce7..4db3c2cf3b 100644
--- a/src/routes/(console)/project-[region]-[project]/settings/updateName.svelte
+++ b/src/routes/(console)/project-[region]-[project]/settings/updateName.svelte
@@ -58,22 +58,21 @@
-{#if $canWriteProjects}
-
diff --git a/src/routes/(console)/project-[region]-[project]/settings/updateProtocols.svelte b/src/routes/(console)/project-[region]-[project]/settings/updateProtocols.svelte
index f1098d22a6..e285400159 100644
--- a/src/routes/(console)/project-[region]-[project]/settings/updateProtocols.svelte
+++ b/src/routes/(console)/project-[region]-[project]/settings/updateProtocols.svelte
@@ -13,6 +13,7 @@
import { SvelteSet } from 'svelte/reactivity';
import { ProtocolId } from '@appwrite.io/console';
import { get } from 'svelte/store';
+ import { canWriteProjects } from '$lib/stores/roles';
let isUpdatingAllProtocols = $state(false);
let showUpdateProtocolDialog = $state(false);
@@ -139,20 +140,26 @@
+ disabled={!$canWriteProjects || shouldDisableEnableAllButton}>
+ Enable all
+
+ disabled={!$canWriteProjects || shouldDisableDisableAllButton}>
+ Disable all
+
@@ -167,7 +174,8 @@
description={protocolDescriptions[protocol.method]}
bind:value={protocol.value}
on:change={() => protocolUpdate(protocol)}
- disabled={apiProtocolUpdates.has(protocol.method)} />
+ disabled={!$canWriteProjects ||
+ apiProtocolUpdates.has(protocol.method)} />
{#if apiProtocolUpdates.has(protocol.method)}
@@ -196,7 +204,7 @@
{
+ if (!$canWriteProjects) return;
showUpdateServiceDialog = true;
updateServicesEnabledMode = false;
}}
- disabled={shouldDisableDisableAllButton}>Disable all
+ disabled={!$canWriteProjects || shouldDisableDisableAllButton}>
+ Disable all
+
@@ -163,7 +169,8 @@
label={service.label}
bind:value={service.value}
on:change={() => serviceUpdate(service)}
- disabled={apiServiceUpdates.has(service.method)} />
+ disabled={!$canWriteProjects ||
+ apiServiceUpdates.has(service.method)} />
{#if apiServiceUpdates.has(service.method)}
@@ -188,7 +195,7 @@
toggleAllServices(updateServicesEnabledMode)}>
{dialogDetails.actionButton}
diff --git a/src/routes/(console)/project-[region]-[project]/updateVariables.svelte b/src/routes/(console)/project-[region]-[project]/updateVariables.svelte
index 88a89c28e4..6e3d386204 100644
--- a/src/routes/(console)/project-[region]-[project]/updateVariables.svelte
+++ b/src/routes/(console)/project-[region]-[project]/updateVariables.svelte
@@ -64,6 +64,7 @@
export let backendPagination = false;
export let variablesOffset = 0;
export let variablesLimit = 10;
+ export let disabled = false;
let selectedVar: Models.Variable = null;
let showVariablesUpload = false;
@@ -400,7 +401,9 @@
{
+ if (disabled) return;
await ensureAllVariablesLoaded();
showEditorModal = true;
trackEvent(Click.VariablesUpdateClick, { source: analyticsSource });
@@ -409,7 +412,9 @@
{
+ if (disabled) return;
await ensureAllVariablesLoaded();
showVariablesUpload = true;
trackEvent(Click.VariablesUpdateClick, { source: analyticsSource });
@@ -420,7 +425,9 @@
{#if variableList.total}
{
+ if (disabled) return;
showVariablesModal = true;
trackEvent(Click.VariablesCreateClick, { source: 'project_settings' });
}}>
@@ -500,8 +507,10 @@
{
e.preventDefault();
+ if (disabled) return;
toggle(e);
}}>
@@ -573,7 +582,7 @@
{/if}
{:else}
- (showVariablesModal = true)}>
+ (showVariablesModal = true)}>
Create a {isGlobal ? 'global variable' : 'variable'} to get started
{/if}