Merge pull request #2974 from appwrite/feat-custom-domain-limits-enforcement

feat: enforce custom domain plan limits in UI
This commit is contained in:
Harsh Mahajan
2026-04-15 16:21:54 +05:30
committed by GitHub
8 changed files with 186 additions and 61 deletions
+2 -3
View File
@@ -24,7 +24,6 @@
"deep-equal": "^2.2.3",
"echarts": "^5.6.0",
"flatted": "^3.4.2",
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@67539a6",
"ignore": "^6.0.2",
"nanoid": "^5.1.7",
"nanotar": "^0.3.0",
@@ -41,7 +40,7 @@
"@melt-ui/svelte": "^0.86.6",
"@playwright/test": "^1.58.2",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.55.0",
"@sveltejs/kit": "^2.57.1",
"@sveltejs/vite-plugin-svelte": "^5.1.1",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
@@ -435,7 +434,7 @@
"@sveltejs/adapter-static": ["@sveltejs/adapter-static@3.0.10", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew=="],
"@sveltejs/kit": ["@sveltejs/kit@2.55.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.6.4", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-MdFRjevVxmAknf2NbaUkDF16jSIzXMWd4Nfah0Qp8TtQVoSp3bV4jKt8mX7z7qTUTWvgSaxtR0EG5WJf53gcuA=="],
"@sveltejs/kit": ["@sveltejs/kit@2.57.1", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.6.4", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3 || ^6.0.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-VRdSbB96cI1EnRh09CqmnQqP/YJvET5buj8S6k7CxaJqBJD4bw4fRKDjcarAj/eX9k2eHifQfDH8NtOh+ZxxPw=="],
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@5.1.1", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", "debug": "^4.4.1", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.17", "vitefu": "^1.0.6" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.0.0" } }, "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ=="],
+1 -1
View File
@@ -55,7 +55,7 @@
"@melt-ui/svelte": "^0.86.6",
"@playwright/test": "^1.58.2",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.55.0",
"@sveltejs/kit": "^2.57.1",
"@sveltejs/vite-plugin-svelte": "^5.1.1",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
@@ -86,6 +86,18 @@
{formatNum(currentPlan.executions)} executions
</span>
</li>
{#if currentPlan.domains > 0}
<li class="list-item u-gap-4 u-cross-center">
<span class="icon-arrow-down u-color-text-danger" aria-hidden="true"></span>
<span class="text">
Limited to {currentPlan.domains} custom {pluralize(
currentPlan.domains,
'domain'
)}
per project
</span>
</li>
{/if}
</ul>
{:else}
<ul class="un-order-list">
@@ -105,12 +117,22 @@
<li>
Limited to {formatNum(currentPlan.executions)} executions
</li>
{#if currentPlan.domains > 0}
<li>
Limited to {currentPlan.domains} custom {pluralize(
currentPlan.domains,
'domain'
)} per project
</li>
{:else}
<li>Unlimited custom domains</li>
{/if}
</ul>
{/if}
{:else if planHasGroup(selectedTab, BillingPlanGroup.Pro)}
<Typography.Text>Everything in the Free plan, plus:</Typography.Text>
<ul class="un-order-list">
<li>Unlimited databases, buckets, functions</li>
<li>Unlimited databases, buckets, functions, and custom domains</li>
<li>Unlimited seats</li>
<li>{currentPlan.bandwidth}GB bandwidth</li>
<li>{currentPlan.storage}GB storage</li>
+2
View File
@@ -200,6 +200,7 @@ export type PlanServices =
| 'bandwidthAddon'
| 'buckets'
| 'databases'
| 'domains'
| 'executions'
| 'executionsAddon'
| 'fileSize'
@@ -316,6 +317,7 @@ export function checkForProjectLimitation(plan: string, id: PlanServices) {
switch (id) {
case 'databases':
case 'domains':
case 'functions':
case 'buckets':
case 'members': // Only applies to Free plan now
@@ -34,11 +34,11 @@
import { View } from '$lib/helpers/load';
import type { Models } from '@appwrite.io/console';
export let data;
let { data } = $props();
let showDelete = false;
let showRetry = false;
let selectedDomain: Models.Domain = null;
let showDelete = $state(false);
let showRetry = $state(false);
let selectedDomain = $state<Models.Domain | null>(null);
const isDomainVerified = (domain: Models.Domain) => {
return domain.nameservers.toLowerCase() === 'appwrite';
@@ -6,16 +6,26 @@
import Container from '$lib/layout/container.svelte';
import type { Models } from '@appwrite.io/console';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { Card, Empty, Icon, Layout } from '@appwrite.io/pink-svelte';
import { Card, Empty, Icon, Layout, Tooltip } from '@appwrite.io/pink-svelte';
import DeleteDomainModal from './deleteDomainModal.svelte';
import RetryDomainModal from './retryDomainModal.svelte';
import SearchQuery from '$lib/components/searchQuery.svelte';
import { app } from '$lib/stores/app';
import { Click, trackEvent } from '$lib/actions/analytics';
import {
BODY_TOOLTIP_MAX_WIDTH,
BODY_TOOLTIP_WRAPPER_STYLE_PRELINE
} from '$lib/helpers/tooltipContent';
import { isServiceLimited } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
import Table from './table.svelte';
let { data } = $props();
const isDomainLimitReached = $derived(
isServiceLimited('domains', $organization, data.proxyRules.total)
);
let showDelete = $state(false);
let showRetry = $state(false);
let selectedProxyRule: Models.ProxyRule = null;
@@ -24,16 +34,28 @@
<Container>
<Layout.Stack direction="row" justifyContent="space-between">
<SearchQuery placeholder="Search domain" />
<Button
href={`${base}/project-${page.params.region}-${page.params.project}/functions/function-${page.params.function}/domains/add-domain`}
on:click={() => {
trackEvent(Click.DomainCreateClick, {
source: 'functions_domain_overview'
});
}}>
<Icon icon={IconPlus} size="s" />
Add domain
</Button>
<Tooltip disabled={!isDomainLimitReached} maxWidth={BODY_TOOLTIP_MAX_WIDTH}>
<div>
<Button
disabled={isDomainLimitReached}
href={isDomainLimitReached
? undefined
: `${base}/project-${page.params.region}-${page.params.project}/functions/function-${page.params.function}/domains/add-domain`}
on:click={() => {
trackEvent(Click.DomainCreateClick, {
source: 'functions_domain_overview'
});
}}>
<Icon icon={IconPlus} size="s" />
Add domain
</Button>
</div>
<svelte:fragment slot="tooltip">
<div style={BODY_TOOLTIP_WRAPPER_STYLE_PRELINE}>
You have reached the maximum number of custom domains for your plan.
</div>
</svelte:fragment>
</Tooltip>
</Layout.Stack>
{#if data.proxyRules.total}
<Table proxyRules={data.proxyRules} organizationDomains={data.organizationDomains} />
@@ -70,12 +92,24 @@
size="s"
ariaLabel="add domain">Documentation</Button>
<Button
secondary
href={`${base}/project-${page.params.region}-${page.params.project}/functions/function-${page.params.function}/domains/add-domain`}
size="s">
Add domain
</Button>
<Tooltip disabled={!isDomainLimitReached} maxWidth={BODY_TOOLTIP_MAX_WIDTH}>
<div>
<Button
secondary
disabled={isDomainLimitReached}
href={isDomainLimitReached
? undefined
: `${base}/project-${page.params.region}-${page.params.project}/functions/function-${page.params.function}/domains/add-domain`}
size="s">
Add domain
</Button>
</div>
<svelte:fragment slot="tooltip">
<div style={BODY_TOOLTIP_WRAPPER_STYLE_PRELINE}>
You have reached the maximum number of custom domains for your plan.
</div>
</svelte:fragment>
</Tooltip>
</svelte:fragment>
</Empty>
</Card.Base>
@@ -5,13 +5,23 @@
import { Button } from '$lib/elements/forms';
import Container from '$lib/layout/container.svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { Card, Empty, Icon } from '@appwrite.io/pink-svelte';
import { Card, Empty, Icon, Tooltip } from '@appwrite.io/pink-svelte';
import { app } from '$lib/stores/app';
import { Click, trackEvent } from '$lib/actions/analytics';
import {
BODY_TOOLTIP_MAX_WIDTH,
BODY_TOOLTIP_WRAPPER_STYLE_PRELINE
} from '$lib/helpers/tooltipContent';
import { isServiceLimited } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
import Table from './table.svelte';
import { ResponsiveContainerHeader } from '$lib/layout';
let { data } = $props();
const isDomainLimitReached = $derived(
isServiceLimited('domains', $organization, data.rules.total)
);
</script>
<Container>
@@ -20,16 +30,28 @@
hideView
searchPlaceholder="Search by domain"
analyticsSource="settings_domain_overview">
<Button
href={`${base}/project-${page.params.region}-${page.params.project}/settings/domains/add-domain`}
on:click={() => {
trackEvent(Click.DomainCreateClick, {
source: 'settings_domain_overview'
});
}}>
<Icon icon={IconPlus} size="s" />
Add domain
</Button>
<Tooltip disabled={!isDomainLimitReached} maxWidth={BODY_TOOLTIP_MAX_WIDTH}>
<div>
<Button
disabled={isDomainLimitReached}
href={isDomainLimitReached
? undefined
: `${base}/project-${page.params.region}-${page.params.project}/settings/domains/add-domain`}
on:click={() => {
trackEvent(Click.DomainCreateClick, {
source: 'settings_domain_overview'
});
}}>
<Icon icon={IconPlus} size="s" />
Add domain
</Button>
</div>
<svelte:fragment slot="tooltip">
<div style={BODY_TOOLTIP_WRAPPER_STYLE_PRELINE}>
You have reached the maximum number of custom domains for your plan.
</div>
</svelte:fragment>
</Tooltip>
</ResponsiveContainerHeader>
{#if data.rules.total}
<Table domains={data.rules} organizationDomains={data.organizationDomains} />
@@ -64,12 +86,24 @@
size="s"
ariaLabel="add domain">Documentation</Button>
<Button
secondary
href={`${base}/project-${page.params.region}-${page.params.project}/settings/domains/add-domain`}
size="s">
Add domain
</Button>
<Tooltip disabled={!isDomainLimitReached} maxWidth={BODY_TOOLTIP_MAX_WIDTH}>
<div>
<Button
secondary
disabled={isDomainLimitReached}
href={isDomainLimitReached
? undefined
: `${base}/project-${page.params.region}-${page.params.project}/settings/domains/add-domain`}
size="s">
Add domain
</Button>
</div>
<svelte:fragment slot="tooltip">
<div style={BODY_TOOLTIP_WRAPPER_STYLE_PRELINE}>
You have reached the maximum number of custom domains for your plan.
</div>
</svelte:fragment>
</Tooltip>
</svelte:fragment>
</Empty>
</Card.Base>
@@ -5,28 +5,50 @@
import { Button } from '$lib/elements/forms';
import Container from '$lib/layout/container.svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { Card, Empty, Icon, Layout } from '@appwrite.io/pink-svelte';
import { Card, Empty, Icon, Layout, Tooltip } from '@appwrite.io/pink-svelte';
import SearchQuery from '$lib/components/searchQuery.svelte';
import { app } from '$lib/stores/app';
import { Click, trackEvent } from '$lib/actions/analytics';
import {
BODY_TOOLTIP_MAX_WIDTH,
BODY_TOOLTIP_WRAPPER_STYLE_PRELINE
} from '$lib/helpers/tooltipContent';
import { isServiceLimited } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
import Table from './table.svelte';
export let data;
let { data } = $props();
const isDomainLimitReached = $derived(
isServiceLimited('domains', $organization, data.proxyRules.total)
);
</script>
<Container>
<Layout.Stack direction="row" justifyContent="space-between">
<SearchQuery placeholder="Search domain" />
<Button
href={`${base}/project-${page.params.region}-${page.params.project}/sites/site-${page.params.site}/domains/add-domain`}
on:click={() => {
trackEvent(Click.DomainCreateClick, {
source: 'sites_domain_overview'
});
}}>
<Icon icon={IconPlus} size="s" />
Add domain
</Button>
<Tooltip disabled={!isDomainLimitReached} maxWidth={BODY_TOOLTIP_MAX_WIDTH}>
<div>
<Button
disabled={isDomainLimitReached}
href={isDomainLimitReached
? undefined
: `${base}/project-${page.params.region}-${page.params.project}/sites/site-${page.params.site}/domains/add-domain`}
on:click={() => {
trackEvent(Click.DomainCreateClick, {
source: 'sites_domain_overview'
});
}}>
<Icon icon={IconPlus} size="s" />
Add domain
</Button>
</div>
<svelte:fragment slot="tooltip">
<div style={BODY_TOOLTIP_WRAPPER_STYLE_PRELINE}>
You have reached the maximum number of custom domains for your plan.
</div>
</svelte:fragment>
</Tooltip>
</Layout.Stack>
{#if data.proxyRules.total}
@@ -62,12 +84,24 @@
size="s"
ariaLabel="add domain">Documentation</Button>
<Button
secondary
href={`${base}/project-${page.params.region}-${page.params.project}/sites/site-${page.params.site}/domains/add-domain`}
size="s">
Add domain
</Button>
<Tooltip disabled={!isDomainLimitReached} maxWidth={BODY_TOOLTIP_MAX_WIDTH}>
<div>
<Button
secondary
disabled={isDomainLimitReached}
href={isDomainLimitReached
? undefined
: `${base}/project-${page.params.region}-${page.params.project}/sites/site-${page.params.site}/domains/add-domain`}
size="s">
Add domain
</Button>
</div>
<svelte:fragment slot="tooltip">
<div style={BODY_TOOLTIP_WRAPPER_STYLE_PRELINE}>
You have reached the maximum number of custom domains for your plan.
</div>
</svelte:fragment>
</Tooltip>
</svelte:fragment>
</Empty>
</Card.Base>