Skip to content

Commit c5db0c7

Browse files
committed
feat: add realtime usage metrics and alerts for billing
1 parent 67531ae commit c5db0c7

11 files changed

Lines changed: 356 additions & 7 deletions

File tree

bun.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
},
2121
"dependencies": {
2222
"@ai-sdk/svelte": "^1.1.24",
23-
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@83fd10c",
23+
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@f151724",
2424
"@appwrite.io/pink-icons": "0.25.0",
2525
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3",
2626
"@appwrite.io/pink-legacy": "^1.0.3",
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<script lang="ts">
2+
import { base } from '$app/paths';
3+
import { browser } from '$app/environment';
4+
import { HeaderAlert } from '$lib/layout';
5+
import { organization, currentPlan } from '$lib/stores/organization';
6+
import { Button } from '$lib/elements/forms';
7+
8+
const DISMISS_KEY = 'realtimePricingDismissed';
9+
10+
let dismissed = browser && localStorage.getItem(DISMISS_KEY) === 'true';
11+
12+
function handleDismiss() {
13+
dismissed = true;
14+
if (browser) {
15+
localStorage.setItem(DISMISS_KEY, 'true');
16+
}
17+
}
18+
19+
$: href = $currentPlan?.usagePerProject
20+
? `${base}/organization-${$organization.$id}/billing`
21+
: `${base}/organization-${$organization.$id}/usage`;
22+
</script>
23+
24+
{#if $organization?.$id && !dismissed}
25+
<HeaderAlert
26+
type="info"
27+
title="Realtime pricing enforcement starting April 22nd"
28+
dismissible
29+
on:dismiss={handleDismiss}>
30+
<svelte:fragment>
31+
Starting April 22nd, realtime usage (connections, messages, and bandwidth) will be
32+
charged based on your plan's rates. Review your usage to avoid unexpected charges.
33+
</svelte:fragment>
34+
<svelte:fragment slot="buttons">
35+
<Button {href} text fullWidthMobile>
36+
<span class="text">View usage</span>
37+
</Button>
38+
</svelte:fragment>
39+
</HeaderAlert>
40+
{/if}

src/lib/components/billing/usageRates.svelte

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,7 @@
6767
{/each}
6868
{#each Object.entries(org.billingPlanDetails.usage) as [key, usage]}
6969
{@const limit = getPlanLimit(key)}
70-
{@const show = limit !== false}
71-
{#if show}
70+
{#if limit !== false}
7271
<Table.Row.Base {root}>
7372
<Table.Cell column="resource" {root}>{usage.name}</Table.Cell>
7473
<Table.Cell column="limit" {root}>
@@ -82,6 +81,18 @@
8281
</Table.Cell>
8382
{/if}
8483
</Table.Row.Base>
84+
{:else if usage.price > 0}
85+
<Table.Row.Base {root}>
86+
<Table.Cell column="resource" {root}>{usage.name}</Table.Cell>
87+
<Table.Cell column="limit" {root}>Pay-as-you-go</Table.Cell>
88+
{#if !isFree}
89+
<Table.Cell column="rate" {root}>
90+
{formatCurrency(usage.price)}/{abbreviateNumber(
91+
usage.value
92+
)}{usage.unit}
93+
</Table.Cell>
94+
{/if}
95+
</Table.Row.Base>
8596
{/if}
8697
{/each}
8798
</Table.Root>

src/lib/stores/billing.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ export type PlanServices =
191191
| 'platforms'
192192
| 'realtime'
193193
| 'realtimeAddon'
194+
| 'realtimeMessages'
194195
| 'storage'
195196
| 'storageAddon'
196197
| 'teams'
@@ -274,6 +275,7 @@ export function checkForUsageFees(plan: string, id: PlanServices) {
274275
case 'users':
275276
case 'executions':
276277
case 'realtime':
278+
case 'realtimeMessages':
277279
return true;
278280

279281
default:

src/routes/(console)/organization-[organization]/+layout.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Breadcrumbs from './breadcrumbs.svelte';
88
import Header from './header.svelte';
99
import { headerAlert } from '$lib/stores/headerAlert';
1010
import ProjectsAtRisk from '$lib/components/billing/alerts/projectsAtRisk.svelte';
11+
import RealtimePricing from '$lib/components/billing/alerts/realtimePricing.svelte';
1112
import { get } from 'svelte/store';
1213
import { preferences } from '$lib/stores/preferences';
1314
import { defaultRoles, defaultScopes } from '$lib/constants';
@@ -64,6 +65,15 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => {
6465
loadAvailableRegions(params.organization)
6566
]);
6667

68+
if (isCloud && new Date() < new Date('2026-04-22')) {
69+
headerAlert.add({
70+
show: true,
71+
component: RealtimePricing,
72+
id: 'realtimePricing',
73+
importance: 1
74+
});
75+
}
76+
6777
return {
6878
header: Header,
6979
breadcrumbs: Breadcrumbs,

src/routes/(console)/organization-[organization]/billing/planSummary.svelte

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,27 @@
361361
getResource(resources, 'GBHours'),
362362
currentPlan?.GBHours
363363
),
364+
createResourceRow(
365+
'realtime',
366+
'Realtime connections',
367+
getResource(resources, 'realtime'),
368+
currentPlan?.realtime
369+
),
370+
createResourceRow(
371+
'realtime-messages',
372+
'Realtime messages',
373+
getResource(resources, 'realtimeMessages'),
374+
currentPlan?.realtimeMessages
375+
),
376+
createRow({
377+
id: 'realtime-bandwidth',
378+
label: 'Realtime bandwidth',
379+
resource: getResource(resources, 'realtimeBandwidth'),
380+
usageFormatter: ({ value }) =>
381+
humanFileSize(value).value + humanFileSize(value).unit,
382+
priceFormatter: ({ amount }) => formatCurrency(amount),
383+
includeProgress: false
384+
}),
364385
createRow({
365386
id: 'sms',
366387
label: 'Phone OTP',

src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
const plan = data?.plan ?? undefined;
3535
3636
$: projects = data.organizationUsage.projects;
37+
$: orgUsage = data.organizationUsage;
3738
3839
let usageProjects: Record<string, UsageProjectInfo> = {};
3940
@@ -423,6 +424,143 @@
423424
</svelte:fragment>
424425
</CardGrid>
425426

427+
<CardGrid gap="none">
428+
<svelte:fragment slot="title">Realtime connections</svelte:fragment>
429+
Peak concurrent realtime connections across all projects in your organization.
430+
431+
<svelte:fragment slot="aside">
432+
{#if orgUsage.realtimeConnectionsTotal}
433+
{@const current = orgUsage.realtimeConnectionsTotal}
434+
{@const max = getServiceLimit('realtime', tier, plan)}
435+
<ProgressBarBig
436+
currentUnit="Connections"
437+
currentValue={formatNum(current)}
438+
maxValue={max ? `/ ${formatNum(max)} connections` : undefined}
439+
progressValue={current}
440+
progressMax={max}
441+
showBar={false} />
442+
<BarChart
443+
options={{
444+
yAxis: {
445+
axisLabel: {
446+
formatter: formatNum
447+
}
448+
}
449+
}}
450+
series={[
451+
{
452+
name: 'Realtime connections',
453+
data: [
454+
...(orgUsage.realtimeConnections ?? []).map((e) => [
455+
e.date,
456+
e.value
457+
])
458+
]
459+
}
460+
]} />
461+
{#if projects?.length > 0}
462+
<ProjectBreakdown {projects} metric="realtime" {usageProjects} />
463+
{/if}
464+
{:else}
465+
<Card isDashed>
466+
<Layout.Stack gap="xs" alignItems="center" justifyContent="center">
467+
<Icon icon={IconChartSquareBar} size="l" />
468+
<Typography.Text variant="m-600">No data to show</Typography.Text>
469+
</Layout.Stack>
470+
</Card>
471+
{/if}
472+
</svelte:fragment>
473+
</CardGrid>
474+
475+
<CardGrid gap="none">
476+
<svelte:fragment slot="title">Realtime messages</svelte:fragment>
477+
Total realtime messages sent to clients across all projects in your organization.
478+
479+
<svelte:fragment slot="aside">
480+
{#if orgUsage.realtimeMessagesTotal}
481+
{@const current = orgUsage.realtimeMessagesTotal}
482+
{@const max = getServiceLimit('realtimeMessages', tier, plan)}
483+
<ProgressBarBig
484+
currentUnit="Messages"
485+
currentValue={formatNum(current)}
486+
maxValue={max ? `/ ${formatNum(max)} messages used` : undefined}
487+
progressValue={current}
488+
progressMax={max}
489+
showBar={false} />
490+
<BarChart
491+
options={{
492+
yAxis: {
493+
axisLabel: {
494+
formatter: formatNum
495+
}
496+
}
497+
}}
498+
series={[
499+
{
500+
name: 'Realtime messages',
501+
data: [
502+
...(orgUsage.realtimeMessages ?? []).map((e) => [e.date, e.value])
503+
]
504+
}
505+
]} />
506+
{#if projects?.length > 0}
507+
<ProjectBreakdown {projects} metric="realtimeMessages" {usageProjects} />
508+
{/if}
509+
{:else}
510+
<Card isDashed>
511+
<Layout.Stack gap="xs" alignItems="center" justifyContent="center">
512+
<Icon icon={IconChartSquareBar} size="l" />
513+
<Typography.Text variant="m-600">No data to show</Typography.Text>
514+
</Layout.Stack>
515+
</Card>
516+
{/if}
517+
</svelte:fragment>
518+
</CardGrid>
519+
520+
<CardGrid>
521+
<svelte:fragment slot="title">Realtime bandwidth</svelte:fragment>
522+
Total realtime bandwidth consumed across all projects in your organization.
523+
524+
<svelte:fragment slot="aside">
525+
{#if orgUsage.realtimeBandwidthTotal}
526+
{@const current = orgUsage.realtimeBandwidthTotal}
527+
{@const currentHumanized = humanFileSize(current)}
528+
<ProgressBarBig
529+
currentUnit={currentHumanized.unit}
530+
currentValue={currentHumanized.value}
531+
progressValue={current}
532+
showBar={false} />
533+
<BarChart
534+
options={{
535+
yAxis: {
536+
axisLabel: {
537+
formatter: (value) =>
538+
humanFileSize(value).value + humanFileSize(value).unit
539+
}
540+
}
541+
}}
542+
series={[
543+
{
544+
name: 'Realtime bandwidth',
545+
data: [
546+
...(orgUsage.realtimeBandwidth ?? []).map((e) => [e.date, e.value])
547+
]
548+
}
549+
]} />
550+
{#if projects?.length > 0}
551+
<ProjectBreakdown {projects} metric="realtimeBandwidth" {usageProjects} />
552+
{/if}
553+
{:else}
554+
<Card isDashed>
555+
<Layout.Stack gap="xs" alignItems="center" justifyContent="center">
556+
<Icon icon={IconChartSquareBar} size="l" />
557+
<Typography.Text variant="m-600">No data to show</Typography.Text>
558+
</Layout.Stack>
559+
</Card>
560+
{/if}
561+
</svelte:fragment>
562+
</CardGrid>
563+
426564
<CardGrid gap="none">
427565
<svelte:fragment slot="title">Storage</svelte:fragment>
428566
Calculated for all your files, deployments, builds, databases and backups.

src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,13 @@ export const load: PageLoad = async ({ params, parent }) => {
3535
imageTransformations: null,
3636
imageTransformationsTotal: null,
3737
screenshotsGenerated: null,
38-
screenshotsGeneratedTotal: null
38+
screenshotsGeneratedTotal: null,
39+
realtimeConnections: null,
40+
realtimeConnectionsTotal: null,
41+
realtimeMessages: null,
42+
realtimeMessagesTotal: null,
43+
realtimeBandwidth: null,
44+
realtimeBandwidthTotal: null
3945
}
4046
};
4147
}

src/routes/(console)/organization-[organization]/usage/[[invoice]]/ProjectBreakdown.svelte

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
| 'databasesReads'
1818
| 'databasesWrites'
1919
| 'imageTransformations'
20-
| 'screenshotsGenerated';
20+
| 'screenshotsGenerated'
21+
| 'realtime'
22+
| 'realtimeMessages'
23+
| 'realtimeBandwidth';
2124
2225
type Estimate = 'authPhoneEstimate';
2326
@@ -97,9 +100,12 @@
97100
return formatNumberWithCommas(value);
98101
case 'executions':
99102
case 'users':
103+
case 'realtime':
104+
case 'realtimeMessages':
100105
return abbreviateNumber(value);
101106
case 'storage':
102107
case 'bandwidth':
108+
case 'realtimeBandwidth':
103109
return humanFileSize(value).value + humanFileSize(value).unit;
104110
}
105111
}

0 commit comments

Comments
 (0)