From 0ff7ed0c0247243db9398bb1098b59bf27086898 Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Tue, 28 Jul 2026 18:44:58 +0300 Subject: [PATCH 01/13] chore(supabase): stop publishing unused Capgo cloud edge functions Capgo cloud traffic already runs on Cloudflare Workers; only `triggers` must stay on Supabase for pg_net queue_consumer. Keep full deploy for self-hosting. Co-authored-by: Cursor --- .github/workflows/build_and_deploy.yml | 6 +- README.md | 12 +++- package.json | 4 +- scripts/supabase-cloud-functions.ts | 62 +++++++++++++++++++++ tests/supabase-cloud-functions.unit.test.ts | 31 +++++++++++ 5 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 scripts/supabase-cloud-functions.ts create mode 100644 tests/supabase-cloud-functions.unit.test.ts diff --git a/.github/workflows/build_and_deploy.yml b/.github/workflows/build_and_deploy.yml index cae6dcce41..6f5557153b 100644 --- a/.github/workflows/build_and_deploy.yml +++ b/.github/workflows/build_and_deploy.yml @@ -92,7 +92,11 @@ jobs: - name: Update functions env: SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_TOKEN }} - run: supabase functions deploy + # Capgo cloud only publishes functions still needed on Supabase (pg_net entry). + # Self-hosting keeps deploying all functions via `supabase functions deploy`. + run: | + mapfile -t FUNCTIONS < <(bun scripts/supabase-cloud-functions.ts list) + supabase functions deploy "${FUNCTIONS[@]}" read_replica_schema: needs: changes diff --git a/README.md b/README.md index f562511515..e888c4e570 100644 --- a/README.md +++ b/README.md @@ -77,8 +77,10 @@ We support both deployments for practical reasons: In production, we route most traffic through Cloudflare Workers for cost and scale, while Supabase remains the reference backend and the default for -self-hosted deployments. Private endpoints and trigger/CRON workloads still run -on Supabase in production. +self-hosted deployments. Capgo cloud still publishes the `triggers` Supabase +function so Postgres `pg_net` can reach `queue_consumer`; cron/trigger work is +then forwarded to Cloudflare when configured. Private, public, plugin, and files +endpoints are not published on Capgo cloud Supabase anymore. ## Project structure (self-hosting map) @@ -454,6 +456,12 @@ Push the functions to the cloud: supabase functions deploy ``` +Capgo cloud production only publishes the allowlisted Supabase functions in +`scripts/supabase-cloud-functions.ts` (currently `triggers`, for `pg_net` / +queue-consumer entry). Public API, plugin, private, and files traffic runs on +Cloudflare Workers. Self-hosted installs should keep deploying every function +with `supabase functions deploy` as shown above. + ### Environment Variables for Self-Hosted Deployments By default, the configuration uses Capgo production values from [configs.json](configs.json). For self-hosted deployments, you **must override** all configuration values using environment variables. diff --git a/package.json b/package.json index aca9b28052..efc93e2e28 100644 --- a/package.json +++ b/package.json @@ -184,8 +184,8 @@ "deploy:cloudflare_env:plugin_me:prod": "bunx wrangler secret bulk internal/cloudflare/.env.prod --config cloudflare_workers/plugin/wrangler.jsonc --env=prod_me", "deploy:cloudflare_env:plugin_hk:prod": "bunx wrangler secret bulk internal/cloudflare/.env.prod --config cloudflare_workers/plugin/wrangler.jsonc --env=prod_hk", "deploy:cloudflare_env:plugin_jp:prod": "bunx wrangler secret bulk internal/cloudflare/.env.prod --config cloudflare_workers/plugin/wrangler.jsonc --env=prod_jp", - "deploy:supabase:prod": "bunx supabase functions deploy --project-ref xvwzpoazmxkqosrdewyv", - "deploy:supabase:preprod": "bunx supabase functions deploy --project-ref ibwjdnhknbkcqfbabwei", + "deploy:supabase:prod": "bunx supabase functions deploy $(bun scripts/supabase-cloud-functions.ts deploy-args) --project-ref xvwzpoazmxkqosrdewyv", + "deploy:supabase:preprod": "bunx supabase functions deploy $(bun scripts/supabase-cloud-functions.ts deploy-args) --project-ref ibwjdnhknbkcqfbabwei", "deploy:supabase_env:prod": "bunx supabase secrets set --project-ref xvwzpoazmxkqosrdewyv --env-file internal/cloudflare/.env.prod", "deploy:supabase_env:preprod": "bunx supabase secrets set --project-ref ibwjdnhknbkcqfbabwei --env-file internal/cloudflare/.env.preprod", "deploy:supabase_env:dev": "bunx supabase secrets set --project-ref aucsybvnhavogdmzwtcw --env-file internal/cloudflare/.env.alpha", diff --git a/scripts/supabase-cloud-functions.ts b/scripts/supabase-cloud-functions.ts new file mode 100644 index 0000000000..61fae30f25 --- /dev/null +++ b/scripts/supabase-cloud-functions.ts @@ -0,0 +1,62 @@ +import { readdirSync } from 'node:fs' +import { join } from 'node:path' +import process from 'node:process' + +/** + * Capgo cloud (prod/preprod/alpha) only publishes Supabase Edge Functions that + * still receive traffic from Postgres (pg_net) or other non-Cloudflare callers. + * + * Public API, plugin, private, and files traffic runs on Cloudflare Workers. + * Self-hosted installs keep deploying every function under supabase/functions/. + */ +export const CAPGO_CLOUD_SUPABASE_FUNCTIONS = [ + 'triggers', +] as const + +export type CapgoCloudSupabaseFunction = typeof CAPGO_CLOUD_SUPABASE_FUNCTIONS[number] + +const SKIP_FUNCTION_DIRS = new Set([ + '_backend', + 'shared', + 'plugin_runtime', +]) + +export function listLocalSupabaseFunctions(functionsDir = join(process.cwd(), 'supabase', 'functions')): string[] { + return readdirSync(functionsDir, { withFileTypes: true }) + .filter(entry => entry.isDirectory() && !entry.name.startsWith('.') && !SKIP_FUNCTION_DIRS.has(entry.name)) + .map(entry => entry.name) + .sort() +} + +export function listCapgoCloudSkippedSupabaseFunctions(localFunctions = listLocalSupabaseFunctions()): string[] { + const keep = new Set(CAPGO_CLOUD_SUPABASE_FUNCTIONS) + return localFunctions.filter(name => !keep.has(name)) +} + +export function buildCapgoCloudSupabaseDeployArgs( + functions: readonly string[] = CAPGO_CLOUD_SUPABASE_FUNCTIONS, +): string[] { + if (functions.length === 0) + throw new Error('CAPGO_CLOUD_SUPABASE_FUNCTIONS must not be empty') + return [...functions] +} + +if (import.meta.main) { + const mode = process.argv[2] ?? 'deploy-args' + if (mode === 'list') { + for (const name of CAPGO_CLOUD_SUPABASE_FUNCTIONS) + console.log(name) + process.exit(0) + } + if (mode === 'skip-list') { + for (const name of listCapgoCloudSkippedSupabaseFunctions()) + console.log(name) + process.exit(0) + } + if (mode === 'deploy-args') { + console.log(buildCapgoCloudSupabaseDeployArgs().join(' ')) + process.exit(0) + } + console.error(`Unknown mode: ${mode}. Use deploy-args | list | skip-list`) + process.exit(1) +} diff --git a/tests/supabase-cloud-functions.unit.test.ts b/tests/supabase-cloud-functions.unit.test.ts new file mode 100644 index 0000000000..26c4afbe41 --- /dev/null +++ b/tests/supabase-cloud-functions.unit.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest' +import { + buildCapgoCloudSupabaseDeployArgs, + CAPGO_CLOUD_SUPABASE_FUNCTIONS, + listCapgoCloudSkippedSupabaseFunctions, + listLocalSupabaseFunctions, +} from '../scripts/supabase-cloud-functions.ts' + +describe('supabase cloud function allowlist', () => { + it('keeps only triggers for Capgo cloud deploys', () => { + expect([...CAPGO_CLOUD_SUPABASE_FUNCTIONS]).toEqual(['triggers']) + expect(buildCapgoCloudSupabaseDeployArgs()).toEqual(['triggers']) + }) + + it('skips every local function except the Capgo cloud allowlist', () => { + const local = listLocalSupabaseFunctions() + expect(local).toContain('triggers') + expect(local).toContain('updates') + expect(local).toContain('stats') + expect(local).toContain('private') + + const skipped = listCapgoCloudSkippedSupabaseFunctions(local) + expect(skipped).not.toContain('triggers') + expect(skipped).toContain('updates') + expect(skipped).toContain('stats') + expect(skipped).toContain('channel_self') + expect(skipped).toContain('private') + expect(skipped).toContain('files') + expect(skipped.length).toBe(local.length - CAPGO_CLOUD_SUPABASE_FUNCTIONS.length) + }) +}) From ce7774a46320e22790d26a22c3fa7747fc9f4ee7 Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Tue, 28 Jul 2026 19:02:39 +0300 Subject: [PATCH 02/13] docs(supabase): clarify Capgo cloud vs self-host function deploy Address review feedback: show allowlisted Capgo Cloud deploy commands and cover empty/multi-target allowlist deploy arg cases. Co-authored-by: Cursor --- README.md | 22 ++++++++++++++------- tests/supabase-cloud-functions.unit.test.ts | 8 ++++++++ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e888c4e570..2c08e8261e 100644 --- a/README.md +++ b/README.md @@ -450,17 +450,25 @@ Seed the secret for functions: supabase secrets set --env-file supabase/functions/.env ``` -Push the functions to the cloud: +### Deploy Capgo Cloud Supabase functions + +Capgo Cloud (prod / preprod / alpha) only publishes allowlisted Supabase +functions from `scripts/supabase-cloud-functions.ts` (currently `triggers`, for +`pg_net` / queue-consumer entry). Public API, plugin, private, and files traffic +runs on Cloudflare Workers. ```bash -supabase functions deploy +bun run deploy:supabase:prod +# or: bunx supabase functions deploy $(bun scripts/supabase-cloud-functions.ts deploy-args) --project-ref ``` -Capgo cloud production only publishes the allowlisted Supabase functions in -`scripts/supabase-cloud-functions.ts` (currently `triggers`, for `pg_net` / -queue-consumer entry). Public API, plugin, private, and files traffic runs on -Cloudflare Workers. Self-hosted installs should keep deploying every function -with `supabase functions deploy` as shown above. +### Deploy self-hosted Supabase functions + +Self-hosted installs should keep deploying every function: + +```bash +supabase functions deploy +``` ### Environment Variables for Self-Hosted Deployments diff --git a/tests/supabase-cloud-functions.unit.test.ts b/tests/supabase-cloud-functions.unit.test.ts index 26c4afbe41..a59cf927cd 100644 --- a/tests/supabase-cloud-functions.unit.test.ts +++ b/tests/supabase-cloud-functions.unit.test.ts @@ -12,6 +12,14 @@ describe('supabase cloud function allowlist', () => { expect(buildCapgoCloudSupabaseDeployArgs()).toEqual(['triggers']) }) + it('rejects an empty explicit deploy list', () => { + expect(() => buildCapgoCloudSupabaseDeployArgs([])).toThrow(/must not be empty/) + }) + + it('passes through multiple explicit deploy targets', () => { + expect(buildCapgoCloudSupabaseDeployArgs(['triggers', 'ok'])).toEqual(['triggers', 'ok']) + }) + it('skips every local function except the Capgo cloud allowlist', () => { const local = listLocalSupabaseFunctions() expect(local).toContain('triggers') From 62b13d7135eec3b26c4434488a2ed9d8d22d0cd4 Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Tue, 28 Jul 2026 19:16:42 +0300 Subject: [PATCH 03/13] fix(supabase): keep functions.invoke targets on Capgo cloud allowlist supabase.functions.invoke always uses SUPABASE_URL, so console/CLI still need private, apikey, app, bundle, channel, files, organization, statistics, and webhooks published alongside triggers. Co-authored-by: Cursor --- .github/workflows/build_and_deploy.yml | 2 +- README.md | 15 +++++----- scripts/supabase-cloud-functions.ts | 23 ++++++++++++--- tests/supabase-cloud-functions.unit.test.ts | 32 +++++++++++++++------ 4 files changed, 52 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build_and_deploy.yml b/.github/workflows/build_and_deploy.yml index 6f5557153b..1f9fe2c743 100644 --- a/.github/workflows/build_and_deploy.yml +++ b/.github/workflows/build_and_deploy.yml @@ -92,7 +92,7 @@ jobs: - name: Update functions env: SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_TOKEN }} - # Capgo cloud only publishes functions still needed on Supabase (pg_net entry). + # Capgo cloud publishes allowlisted functions only (pg_net + functions.invoke). # Self-hosting keeps deploying all functions via `supabase functions deploy`. run: | mapfile -t FUNCTIONS < <(bun scripts/supabase-cloud-functions.ts list) diff --git a/README.md b/README.md index 2c08e8261e..cd47f9572f 100644 --- a/README.md +++ b/README.md @@ -77,10 +77,10 @@ We support both deployments for practical reasons: In production, we route most traffic through Cloudflare Workers for cost and scale, while Supabase remains the reference backend and the default for -self-hosted deployments. Capgo cloud still publishes the `triggers` Supabase -function so Postgres `pg_net` can reach `queue_consumer`; cron/trigger work is -then forwarded to Cloudflare when configured. Private, public, plugin, and files -endpoints are not published on Capgo cloud Supabase anymore. +self-hosted deployments. Capgo cloud still publishes the Supabase functions used +by Postgres `pg_net` (`triggers`) and by `supabase.functions.invoke` from the +console/CLI (those always target `SUPABASE_URL`, not Cloudflare). Plugin hot +paths and other unused Capgo cloud endpoints are not published on Supabase. ## Project structure (self-hosting map) @@ -453,9 +453,10 @@ supabase secrets set --env-file supabase/functions/.env ### Deploy Capgo Cloud Supabase functions Capgo Cloud (prod / preprod / alpha) only publishes allowlisted Supabase -functions from `scripts/supabase-cloud-functions.ts` (currently `triggers`, for -`pg_net` / queue-consumer entry). Public API, plugin, private, and files traffic -runs on Cloudflare Workers. +functions from `scripts/supabase-cloud-functions.ts`: `triggers` for `pg_net`, +plus every function still reached by console/CLI `supabase.functions.invoke` +(`private`, `apikey`, `app`, `bundle`, `channel`, `files`, `organization`, +`statistics`, `webhooks`). Plugin hot paths and unused ops endpoints are skipped. ```bash bun run deploy:supabase:prod diff --git a/scripts/supabase-cloud-functions.ts b/scripts/supabase-cloud-functions.ts index 61fae30f25..db6d95c63e 100644 --- a/scripts/supabase-cloud-functions.ts +++ b/scripts/supabase-cloud-functions.ts @@ -3,14 +3,29 @@ import { join } from 'node:path' import process from 'node:process' /** - * Capgo cloud (prod/preprod/alpha) only publishes Supabase Edge Functions that - * still receive traffic from Postgres (pg_net) or other non-Cloudflare callers. + * Capgo cloud (prod/preprod/alpha) Supabase Edge Functions that must stay + * published on sb.capgo.app. * - * Public API, plugin, private, and files traffic runs on Cloudflare Workers. - * Self-hosted installs keep deploying every function under supabase/functions/. + * Keep: + * - `triggers` — Postgres pg_net calls /functions/v1/triggers/queue_consumer/sync + * - console/CLI `supabase.functions.invoke(...)` targets (SDK always uses + * SUPABASE_URL, not api.capgo.app) + * + * Skip (Cloudflare or unused on Capgo cloud Supabase): + * plugin hot paths, device public API, build, notifications, ops probes, etc. + * Self-hosted installs still deploy every function under supabase/functions/. */ export const CAPGO_CLOUD_SUPABASE_FUNCTIONS = [ + 'apikey', + 'app', + 'bundle', + 'channel', + 'files', + 'organization', + 'private', + 'statistics', 'triggers', + 'webhooks', ] as const export type CapgoCloudSupabaseFunction = typeof CAPGO_CLOUD_SUPABASE_FUNCTIONS[number] diff --git a/tests/supabase-cloud-functions.unit.test.ts b/tests/supabase-cloud-functions.unit.test.ts index a59cf927cd..81218c8227 100644 --- a/tests/supabase-cloud-functions.unit.test.ts +++ b/tests/supabase-cloud-functions.unit.test.ts @@ -7,9 +7,20 @@ import { } from '../scripts/supabase-cloud-functions.ts' describe('supabase cloud function allowlist', () => { - it('keeps only triggers for Capgo cloud deploys', () => { - expect([...CAPGO_CLOUD_SUPABASE_FUNCTIONS]).toEqual(['triggers']) - expect(buildCapgoCloudSupabaseDeployArgs()).toEqual(['triggers']) + it('keeps SDK invoke + pg_net Capgo cloud functions', () => { + expect([...CAPGO_CLOUD_SUPABASE_FUNCTIONS]).toEqual([ + 'apikey', + 'app', + 'bundle', + 'channel', + 'files', + 'organization', + 'private', + 'statistics', + 'triggers', + 'webhooks', + ]) + expect(buildCapgoCloudSupabaseDeployArgs()).toEqual([...CAPGO_CLOUD_SUPABASE_FUNCTIONS]) }) it('rejects an empty explicit deploy list', () => { @@ -20,20 +31,25 @@ describe('supabase cloud function allowlist', () => { expect(buildCapgoCloudSupabaseDeployArgs(['triggers', 'ok'])).toEqual(['triggers', 'ok']) }) - it('skips every local function except the Capgo cloud allowlist', () => { + it('skips plugin/ops functions not used via supabase.functions.invoke', () => { const local = listLocalSupabaseFunctions() expect(local).toContain('triggers') + expect(local).toContain('private') expect(local).toContain('updates') expect(local).toContain('stats') - expect(local).toContain('private') const skipped = listCapgoCloudSkippedSupabaseFunctions(local) - expect(skipped).not.toContain('triggers') + for (const keep of CAPGO_CLOUD_SUPABASE_FUNCTIONS) + expect(skipped).not.toContain(keep) + expect(skipped).toContain('updates') expect(skipped).toContain('stats') expect(skipped).toContain('channel_self') - expect(skipped).toContain('private') - expect(skipped).toContain('files') + expect(skipped).toContain('updates_debug') + expect(skipped).toContain('device') + expect(skipped).toContain('build') + expect(skipped).toContain('ok') + expect(skipped).toContain('queue_health') expect(skipped.length).toBe(local.length - CAPGO_CLOUD_SUPABASE_FUNCTIONS.length) }) }) From 061a4ba6b29c34d11a1cadbede3e3781603ee73e Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Wed, 29 Jul 2026 02:43:35 +0300 Subject: [PATCH 04/13] refactor(api): route console and CLI invokes through Cloudflare Stop Capgo cloud console traffic on sb.capgo.app edge functions by calling api.capgo.app instead. Migrate CLI the same way while keeping bundle/channel/files/private published until 2026-10-28 for older CLIs. Co-authored-by: Cursor --- README.md | 13 +- cli/src/bundle/upload.ts | 54 ++++--- cli/src/channel/delete.ts | 19 +-- cli/src/channel/set.ts | 24 +-- cli/src/utils.ts | 143 ++++++++++++++++-- scripts/supabase-cloud-functions.ts | 38 +++-- src/components/dashboard/AppAccess.vue | 11 +- .../dashboard/AppOnboardingFlow.vue | 7 +- src/components/dashboard/AppSetting.vue | 3 +- .../dashboard/InviteTeammateModal.vue | 3 +- src/components/dashboard/StepsApp.vue | 3 +- src/components/dashboard/Usage.vue | 6 +- .../permissions/ChannelAccessPanel.vue | 9 +- src/components/tables/AccessTable.vue | 9 +- src/pages/ApiKeys.vue | 9 +- .../app/[app].channel.[channel].devices.vue | 3 +- src/pages/invitation.vue | 5 +- src/pages/onboarding/organization.vue | 5 +- src/pages/settings/account/ChangePassword.vue | 20 +-- .../settings/account/ManageTwoFactor.vue | 3 +- src/pages/settings/organization/Members.vue | 9 +- src/pages/settings/organization/Plans.vue | 3 +- src/pages/settings/organization/Security.vue | 3 +- src/pages/settings/organization/index.vue | 3 +- src/services/apikeys.ts | 5 +- src/services/capgoApi.ts | 95 ++++++++++++ src/services/chartDataService.ts | 3 +- src/services/emailOtp.ts | 3 +- src/services/logAs.ts | 3 +- src/services/stripe.ts | 11 +- src/services/supabase.ts | 6 +- src/stores/webhooks.ts | 20 ++- src/utils/invites.ts | 3 +- src/utils/onboardingAppCreateHelpers.ts | 3 +- tests/supabase-cloud-functions.unit.test.ts | 40 ++--- 35 files changed, 418 insertions(+), 179 deletions(-) create mode 100644 src/services/capgoApi.ts diff --git a/README.md b/README.md index cd47f9572f..9b86763d5e 100644 --- a/README.md +++ b/README.md @@ -453,10 +453,15 @@ supabase secrets set --env-file supabase/functions/.env ### Deploy Capgo Cloud Supabase functions Capgo Cloud (prod / preprod / alpha) only publishes allowlisted Supabase -functions from `scripts/supabase-cloud-functions.ts`: `triggers` for `pg_net`, -plus every function still reached by console/CLI `supabase.functions.invoke` -(`private`, `apikey`, `app`, `bundle`, `channel`, `files`, `organization`, -`statistics`, `webhooks`). Plugin hot paths and unused ops endpoints are skipped. +functions from `scripts/supabase-cloud-functions.ts`: + +- `triggers` forever (`pg_net` queue consumer) +- `bundle`, `channel`, `files`, `private` until **2026-10-28** (old Capgo CLI + still called these via `supabase.functions.invoke` on `sb.capgo.app`; new + CLI uses `api.capgo.app` / `files.capgo.app`) + +Console traffic uses Cloudflare (`VITE_API_HOST`). Plugin hot paths and unused +ops endpoints are skipped on Capgo cloud Supabase. ```bash bun run deploy:supabase:prod diff --git a/cli/src/bundle/upload.ts b/cli/src/bundle/upload.ts index e3c71a9b76..50dfda36fe 100644 --- a/cli/src/bundle/upload.ts +++ b/cli/src/bundle/upload.ts @@ -23,7 +23,7 @@ import { confirmWithRememberedChoice } from '../promptPreferences' import { showReplicationProgress } from '../replicationProgress' import { formatTable } from '../terminal-table' import { usesAlwaysDirectUpdate } from '../updaterConfig' -import { baseKeyV2, BROTLI_MIN_UPDATER_VERSION_V5, BROTLI_MIN_UPDATER_VERSION_V6, BROTLI_MIN_UPDATER_VERSION_V7, canPromptInteractively, checkCompatibilityCloud, checkPlanValidUpload, checkRemoteCliMessages, createSupabaseClient, deletedFailedVersion, deltaManifestTooLargeMessage, findRoot, findSavedKey, formatError, getAppId, getBundleVersion, getCompatibilityDetails, getConfig, getInstalledVersion, getLocalConfig, getLocalDependencies, getOrganizationId, getPMAndCommand, getRemoteChecksums, getRemoteFileConfig, hasCliPermission, isCompatible, isDeprecatedPluginVersion, MAX_MANIFEST_ENTRIES, regexSemver, resolveUserIdFromApiKey, sendEvent, setVersionManifest, updateConfigUpdater, updateOrCreateChannel, updateOrCreateVersion, UPLOAD_TIMEOUT, uploadTUS, uploadUrl, zipFile } from '../utils' +import { baseKeyV2, BROTLI_MIN_UPDATER_VERSION_V5, BROTLI_MIN_UPDATER_VERSION_V6, BROTLI_MIN_UPDATER_VERSION_V7, canPromptInteractively, checkCompatibilityCloud, checkPlanValidUpload, checkRemoteCliMessages, createSupabaseClient, deletedFailedVersion, deltaManifestTooLargeMessage, findRoot, findSavedKey, formatError, getAppId, getBundleVersion, getCompatibilityDetails, getConfig, getInstalledVersion, getLocalConfig, getLocalDependencies, getOrganizationId, getPMAndCommand, getRemoteChecksums, getRemoteFileConfig, hasCliPermission, invokeCapgoCliApi, isCompatible, isDeprecatedPluginVersion, MAX_MANIFEST_ENTRIES, regexSemver, resolveUserIdFromApiKey, sendEvent, setVersionManifest, updateConfigUpdater, updateOrCreateChannel, updateOrCreateVersion, UPLOAD_TIMEOUT, uploadTUS, uploadUrl, zipFile } from '../utils' import { getVersionSuggestions, interactiveVersionBump } from '../versionHelpers' import { maybePromptBuilderCta, shouldBlockIncompatibleUpload } from './builder-cta' import { checkIndexPosition, searchInDirectory } from './check' @@ -596,7 +596,7 @@ async function uploadBundleToCapgoCloud(apikey: string, supabase: SupabaseType, if (options.verbose) log.info(`[Verbose] Using standard upload (non-TUS), getting presigned URL...`) - const url = await uploadUrl(supabase, appid, bundle) + const url = await uploadUrl(apikey, appid, bundle, options) if (!url) { log.error(`Cannot get upload url`) if (options.verbose) @@ -672,7 +672,7 @@ async function uploadBundleToCapgoCloud(apikey: string, supabase: SupabaseType, log.info(`[Verbose] Cleaning up failed version from database...`) try { - await deletedFailedVersion(supabase, appid, bundle) + await deletedFailedVersion(apikey, appid, bundle, options) if (options.verbose) log.info(`[Verbose] Failed version cleaned up`) } @@ -874,20 +874,24 @@ async function formatFunctionInvokeError(error: unknown): Promise { } async function promoteExistingChannel( - supabase: SupabaseType, + apikey: string, appid: string, versionId: number, targetChannel: UploadTargetChannel, localConfig: localConfigType, displayBundleUrl: boolean, + options?: { supaHost?: string, supaAnon?: string }, ): Promise { - const { error } = await supabase.functions.invoke('bundle', { + const { error } = await invokeCapgoCliApi('bundle', { + apikey, method: 'PUT', - body: JSON.stringify({ + body: { app_id: appid, version_id: versionId, channel_id: targetChannel.id, - }), + }, + supaHost: options?.supaHost, + supaAnon: options?.supaAnon, }) if (error) @@ -917,8 +921,8 @@ async function setVersionInChannel( targetChannel: UploadTargetChannel | null, requireChannelAssignment = false, selfAssign?: boolean, + cliHost?: { supaHost?: string, supaAnon?: string }, ): Promise { - const canPromoteTargetChannel = targetChannel !== null && await hasCliPermission(supabase, apikey, 'channel.promote_bundle', { appId: appid, channelId: targetChannel.id }) const canCreateChannel = targetChannel === null @@ -938,12 +942,12 @@ async function setVersionInChannel( const canUpdateChannelSettings = await hasCliPermission(supabase, apikey, 'channel.update_settings', { appId: appid, channelId: targetChannel.id }) if (!canUpdateChannelSettings) { log.warn('Cannot enable device self-assign because this API key lacks channel.update_settings') - return promoteExistingChannel(supabase, appid, versionId, targetChannel, localConfig, displayBundleUrl) + return promoteExistingChannel(apikey, appid, versionId, targetChannel, localConfig, displayBundleUrl, cliHost) } } if (!selfAssign) - return promoteExistingChannel(supabase, appid, versionId, targetChannel, localConfig, displayBundleUrl) + return promoteExistingChannel(apikey, appid, versionId, targetChannel, localConfig, displayBundleUrl, cliHost) const { error: dbError3, data } = await updateOrCreateChannel(supabase, { name: channel, @@ -969,14 +973,17 @@ async function setVersionInChannel( // The channel endpoint creates the preview channel, receives its scoped // lifecycle binding, and promotes this bundle in one transaction. if (canCreateChannel) { - const { error, data } = await supabase.functions.invoke('channel', { + const { error, data } = await invokeCapgoCliApi('channel', { + apikey, method: 'POST', - body: JSON.stringify({ + body: { app_id: appid, channel, version: bundle, ...(selfAssign ? { allow_device_self_set: true } : {}), - }), + }, + supaHost: cliHost?.supaHost, + supaAnon: cliHost?.supaAnon, }) if (error) { uploadFail(`Cannot create channel and set its bundle because this API key does not have the required RBAC permission. ${await formatFunctionInvokeError(error)}`) @@ -1031,6 +1038,7 @@ async function setRolloutVersionInChannel( rolloutPercentageBps: number, rolloutCacheTtlSeconds?: number, selfAssign?: boolean, + cliHost?: { supaHost?: string, supaAnon?: string }, ): Promise { if (!targetChannel) uploadFail(`Cannot set rollout, channel ${channel} must already exist with a stable bundle`) @@ -1048,9 +1056,10 @@ async function setRolloutVersionInChannel( uploadFail('Cannot set rollout because this API key lacks channel.update_settings for the target channel') const shouldResumeSameRollout = targetChannel.rollout_version === versionId && rolloutPercentageBps > 0 - const { error: rolloutError } = await supabase.functions.invoke('channel', { + const { error: rolloutError } = await invokeCapgoCliApi('channel', { + apikey, method: 'POST', - body: JSON.stringify({ + body: { app_id: appid, channel, rolloutVersion: bundle, @@ -1059,7 +1068,9 @@ async function setRolloutVersionInChannel( ...(shouldResumeSameRollout ? { rolloutPaused: false } : {}), ...(rolloutCacheTtlSeconds != null ? { rolloutCacheTtlSeconds } : {}), ...(selfAssign ? { allow_device_self_set: true } : {}), - }), + }, + supaHost: cliHost?.supaHost, + supaAnon: cliHost?.supaAnon, }) if (rolloutError) @@ -1514,7 +1525,6 @@ export async function uploadBundleInternal(preAppid: string, options: OptionsUpl if (options.delta && manifest.length > MAX_MANIFEST_ENTRIES) uploadFail(deltaManifestTooLargeMessage(manifest.length)) - if (options.verbose) log.info(`[Verbose] Creating version record in database...`) @@ -1589,7 +1599,7 @@ export async function uploadBundleInternal(preAppid: string, options: OptionsUpl } catch (error) { try { - await deletedFailedVersion(supabase, appid, bundle) + await deletedFailedVersion(apikey, appid, bundle, options) } catch (cleanupError) { uploadFail(`Cannot upload bundle to S3 ${formatError(error)}. Cleanup of the incomplete version also failed (${formatError(cleanupError)}); delete bundle ${bundle} manually before retrying.`) @@ -1665,11 +1675,11 @@ export async function uploadBundleInternal(preAppid: string, options: OptionsUpl log.info(`[Verbose] Writing ${finalManifest.length} manifest rows via set_manifest...`) try { - await setVersionManifest(supabase, appid, bundle, finalManifest) + await setVersionManifest(apikey, appid, bundle, finalManifest, options) } catch (error) { try { - await deletedFailedVersion(supabase, appid, bundle) + await deletedFailedVersion(apikey, appid, bundle, options) } catch (cleanupError) { uploadFail(`Cannot set bundle manifest ${formatError(error)}. Cleanup of the incomplete version also failed (${formatError(cleanupError)}); delete bundle ${bundle} manually before retrying.`) @@ -1729,8 +1739,8 @@ export async function uploadBundleInternal(preAppid: string, options: OptionsUpl ? uploadTargetChannels.get(targetChannel) ?? null : await findUploadTargetChannel(supabase, appid, targetChannel) const targetChannelVersionSet = rolloutPercentageBps != null - ? await setRolloutVersionInChannel(supabase, apikey, !!options.bundleUrl, bundle, targetChannel, appid, localConfig, uploadTargetChannel, rolloutPercentageBps, options.rolloutCacheTtlSeconds, options.selfAssign) - : await setVersionInChannel(supabase, apikey, !!options.bundleUrl, bundle, targetChannel, userId, orgId, appid, localConfig, uploadTargetChannel, channelAssignmentRequired, options.selfAssign) + ? await setRolloutVersionInChannel(supabase, apikey, !!options.bundleUrl, bundle, targetChannel, appid, localConfig, uploadTargetChannel, rolloutPercentageBps, options.rolloutCacheTtlSeconds, options.selfAssign, options) + : await setVersionInChannel(supabase, apikey, !!options.bundleUrl, bundle, targetChannel, userId, orgId, appid, localConfig, uploadTargetChannel, channelAssignmentRequired, options.selfAssign, options) if (targetChannelVersionSet) channelVersionSet.add(targetChannel) if (options.verbose) diff --git a/cli/src/channel/delete.ts b/cli/src/channel/delete.ts index 3041d7ad66..1f8d4f7f78 100644 --- a/cli/src/channel/delete.ts +++ b/cli/src/channel/delete.ts @@ -3,15 +3,7 @@ import { intro, log, outro } from '@clack/prompts' import { check2FAComplianceForApp, checkAppExistsAndHasPermissionOrgErr } from '../api/app' import { delChannel, findBundleIdByChannelName, findChannel } from '../api/channels' import { deleteAppVersion } from '../api/versions' -import { - createSupabaseClient, - findSavedKey, - formatError, - getAppId, - getConfig, - hasCliPermission, - sendEvent, -} from '../utils' +import { createSupabaseClient, findSavedKey, formatError, getAppId, getConfig, hasCliPermission, invokeCapgoCliApi, sendEvent } from '../utils' export async function deleteChannelInternal(channelId: string, appId: string, options: ChannelDeleteOptions, silent = false) { if (!silent) @@ -65,13 +57,16 @@ export async function deleteChannelInternal(channelId: string, appId: string, op if (!silent) log.info(`Deleting preview channel ${appId}#${channelId} and its bundle from Capgo`) - const { error } = await supabase.functions.invoke('channel', { + const { error } = await invokeCapgoCliApi('channel', { + apikey: options.apikey!, method: 'DELETE', - body: JSON.stringify({ + body: { app_id: appId, channel: channelId, delete_bundle: true, - }), + }, + supaHost: options.supaHost, + supaAnon: options.supaAnon, }) if (error) { const message = `Cannot delete preview channel and bundle: ${formatError(error)}` diff --git a/cli/src/channel/set.ts b/cli/src/channel/set.ts index 2991122b59..b3b94f803b 100644 --- a/cli/src/channel/set.ts +++ b/cli/src/channel/set.ts @@ -7,20 +7,7 @@ import { findChannel } from '../api/channels' import { sendUpdateNotificationsForChannels } from '../notifications/send-update' import { printPreviewQrForResolvedTarget, resolveChannelPreviewTarget } from '../preview/qr' import { formatTable } from '../terminal-table' -import { - checkCompatibilityNativePackages, - checkPlanValid, - createSupabaseClient, - findSavedKey, - getAppId, - getBundleVersion, - getCompatibilityDetails, - getConfig, - isCompatible, - resolveUserIdFromApiKey, - sendEvent, - updateOrCreateChannel, -} from '../utils' +import { checkCompatibilityNativePackages, checkPlanValid, createSupabaseClient, findSavedKey, getAppId, getBundleVersion, getCompatibilityDetails, getConfig, invokeCapgoCliApi, isCompatible, resolveUserIdFromApiKey, sendEvent, updateOrCreateChannel } from '../utils' /** * Display a compatibility table for the given packages @@ -609,13 +596,16 @@ export async function setChannelInternal(channel: string, appId: string, options } if (hasStableBundlePromotion && !hasSettingsUpdate) { - const { error } = await supabase.functions.invoke('bundle', { + const { error } = await invokeCapgoCliApi('bundle', { + apikey: options.apikey!, method: 'PUT', - body: JSON.stringify({ + body: { app_id: appId, version_id: channelPayload.version, channel_id: existingChannel.id, - }), + }, + supaHost: options.supaHost, + supaAnon: options.supaAnon, }) if (error) { if (!silent) diff --git a/cli/src/utils.ts b/cli/src/utils.ts index db41319f7b..52ff527168 100644 --- a/cli/src/utils.ts +++ b/cli/src/utils.ts @@ -28,8 +28,8 @@ import { findMonorepoRoot, findNXMonorepoRoot, isMonorepo, isNXMonorepo } from ' import { getChecksum } from './checksum' import { loadConfig, loadConfigForWrite, writeConfig } from './config' import { isTruthyEnvValue } from './posthog' -import { safeParseSchema } from './schemas/schema_validation' import { nativePackageSchema } from './schemas/common' +import { safeParseSchema } from './schemas/schema_validation' import { formatApiErrorForCli, parseSecurityPolicyError } from './utils/security_policy_errors' export const baseKey = '.capgo_key' @@ -745,18 +745,117 @@ export function formatCapgoApiErrorBody(body: unknown): string { return [record.error, record.message, record.status].filter(Boolean).join(' | ') } -/** Resolve Capgo public API base URL for CLI mutations (app create/update, etc.). */ +/** Capgo-managed Supabase hosts (cloud). Self-host uses a different origin. */ +export function isCapgoManagedSupabaseHost(supaHost?: string): boolean { + if (!supaHost) + return false + const host = normalizeSupabaseHost(supaHost).toLowerCase() + return host.includes('sb.capgo.app') + || host.includes('xvwzpoazmxkqosrdewyv') + || host.includes('ibwjdnhknbkcqfbabwei') + || host.includes('aucsybvnhavogdmzwtcw') +} + +/** + * Resolve Capgo public API base URL for CLI mutations (app create/update, etc.). + * Capgo cloud uses api.capgo.app. Self-host with only localSupa (default localApi) + * keeps /functions/v1 on that Supabase host. + */ export function resolveConfiguredCapgoPublicApiHost(config: { hostApi: string + hostFilesApi?: string supaHost?: string supaKey?: string }): string { - if (config.supaHost && config.supaKey && config.hostApi === defaultApiHost) + if ( + config.supaHost + && config.supaKey + && config.hostApi === defaultApiHost + && !isCapgoManagedSupabaseHost(config.supaHost) + ) { return `${normalizeSupabaseHost(config.supaHost)}/functions/v1` + } return config.hostApi } +export interface CapgoCliInvokeOptions { + apikey: string + method?: string + body?: unknown + /** Capgo cloud files worker; ignored when resolving to self-host /functions/v1 */ + useFilesHost?: boolean + supaHost?: string + supaAnon?: string +} + +/** + * Invoke Capgo HTTP APIs formerly reached via supabase.functions.invoke. + * Capgo cloud -> hostApi / hostFilesApi. Self-host -> /functions/v1. + */ +export async function invokeCapgoCliApi( + path: string, + options: CapgoCliInvokeOptions, +): Promise<{ data: T | null, error: Error | null }> { + const method = (options.method ?? 'POST').toUpperCase() + let base: string + if (options.supaHost && options.supaAnon) { + base = `${normalizeSupabaseHost(options.supaHost)}/functions/v1` + } + else { + const localConfig = await getRemoteConfig(true) + if ( + localConfig.supaHost + && localConfig.supaKey + && localConfig.hostApi === defaultApiHost + && !isCapgoManagedSupabaseHost(localConfig.supaHost) + ) { + base = `${normalizeSupabaseHost(localConfig.supaHost)}/functions/v1` + } + else if (options.useFilesHost) { + base = localConfig.hostFilesApi + } + else { + base = localConfig.hostApi + } + } + + const url = `${base.replace(/\/+$/, '')}/${path.replace(/^\//, '')}` + try { + const response = await fetch(url, { + method, + headers: { + 'Content-Type': 'application/json', + 'Authorization': options.apikey, + 'capgkey': options.apikey, + }, + body: method === 'GET' || method === 'HEAD' + ? undefined + : (typeof options.body === 'string' ? options.body : JSON.stringify(options.body ?? {})), + }) + + const contentType = response.headers.get('content-type') ?? '' + const payload = contentType.includes('application/json') + ? await response.json().catch(() => null) + : await response.text().catch(() => null) + + if (!response.ok) { + return { + data: null, + error: new FunctionsHttpError(response), + } + } + + return { data: payload as T, error: null } + } + catch (error) { + return { + data: null, + error: error instanceof Error ? error : new Error(String(error)), + } + } +} + export async function resolveCapgoPublicApiHost( options?: { supaHost?: string, supaAnon?: string }, silent = true, @@ -1287,20 +1386,24 @@ export async function updateOrCreateVersion(supabase: SupabaseClient, .eq('name', update.name) } -export async function uploadUrl(supabase: SupabaseClient, appId: string, name: string): Promise { +export async function uploadUrl(apikey: string, appId: string, name: string, options?: { supaHost?: string, supaAnon?: string }): Promise { const data = { app_id: appId, name, version: 0, } try { - const pathUploadLink = 'files/upload_link' - const res = await supabase.functions.invoke(pathUploadLink, { body: JSON.stringify(data) }) + const res = await invokeCapgoCliApi<{ url?: string }>('files/upload_link', { + apikey, + body: data, + useFilesHost: true, + supaHost: options?.supaHost, + supaAnon: options?.supaAnon, + }) if (res.error) { - // Handle error case if (res.error instanceof FunctionsHttpError) { - const errorBody = await res.error.context.json() + const errorBody = await res.error.context.json().catch(() => ({})) log.error(`Upload URL error: ${errorBody.status || JSON.stringify(errorBody)}`) } else { @@ -1309,7 +1412,7 @@ export async function uploadUrl(supabase: SupabaseClient, appId: strin return '' } - return res.data.url + return res.data?.url ?? '' } catch (error) { log.error(`Cannot get upload url ${formatError(error)}`) @@ -1486,13 +1589,18 @@ export async function uploadTUS(apikey: string, data: Buffer, orgId: string, app }) } -export async function deletedFailedVersion(supabase: SupabaseClient, appId: string, name: string): Promise { +export async function deletedFailedVersion(apikey: string, appId: string, name: string, options?: { supaHost?: string, supaAnon?: string }): Promise { const data = { app_id: appId, name, } - const pathFailed = 'private/delete_failed_version' - const res = await supabase.functions.invoke(pathFailed, { body: JSON.stringify(data), method: 'DELETE' }) + const res = await invokeCapgoCliApi('private/delete_failed_version', { + apikey, + method: 'DELETE', + body: data, + supaHost: options?.supaHost, + supaAnon: options?.supaAnon, + }) if (res.error) { if (res.error instanceof FunctionsHttpError) { @@ -1514,18 +1622,23 @@ export interface VersionManifestEntry { * Prefer this over writing app_versions.manifest jsonb — old CLIs still use that legacy path. */ export async function setVersionManifest( - supabase: SupabaseClient, + apikey: string, appId: string, name: string, manifest: VersionManifestEntry[], + options?: { supaHost?: string, supaAnon?: string }, ): Promise { const data = { app_id: appId, name, manifest, } - const pathSetManifest = 'private/set_manifest' - const res = await supabase.functions.invoke(pathSetManifest, { body: JSON.stringify(data) }) + const res = await invokeCapgoCliApi('private/set_manifest', { + apikey, + body: data, + supaHost: options?.supaHost, + supaAnon: options?.supaAnon, + }) if (res.error) { if (res.error instanceof FunctionsHttpError) { diff --git a/scripts/supabase-cloud-functions.ts b/scripts/supabase-cloud-functions.ts index db6d95c63e..a21eb9b59e 100644 --- a/scripts/supabase-cloud-functions.ts +++ b/scripts/supabase-cloud-functions.ts @@ -6,26 +6,38 @@ import process from 'node:process' * Capgo cloud (prod/preprod/alpha) Supabase Edge Functions that must stay * published on sb.capgo.app. * - * Keep: - * - `triggers` — Postgres pg_net calls /functions/v1/triggers/queue_consumer/sync - * - console/CLI `supabase.functions.invoke(...)` targets (SDK always uses - * SUPABASE_URL, not api.capgo.app) + * Forever: + * - `triggers` — Postgres pg_net → /functions/v1/triggers/queue_consumer/sync + * + * Deprecated (CLI still used supabase.functions.invoke for these until the + * Capgo CLI migration ships and old CLIs age out). Keep Capgo-cloud publish + * until 2026-10-28 (~3 months after console+CLI migrate to api.capgo.app / + * files.capgo.app). After that date remove them from this allowlist and delete + * the Capgo cloud Supabase deployments. + * - `bundle`, `channel`, `files`, `private` + * + * Console-only invoke targets were migrated to VITE_API_HOST (Cloudflare) and + * are no longer published on Capgo cloud Supabase: + * - apikey, app, organization, statistics, webhooks * - * Skip (Cloudflare or unused on Capgo cloud Supabase): - * plugin hot paths, device public API, build, notifications, ops probes, etc. * Self-hosted installs still deploy every function under supabase/functions/. */ -export const CAPGO_CLOUD_SUPABASE_FUNCTIONS = [ - 'apikey', - 'app', +export const CAPGO_CLOUD_SUPABASE_FUNCTIONS_CLI_DEPRECATION_UNTIL = '2026-10-28' + +export const CAPGO_CLOUD_SUPABASE_FUNCTIONS_FOREVER = [ + 'triggers', +] as const + +export const CAPGO_CLOUD_SUPABASE_FUNCTIONS_CLI_DEPRECATION = [ 'bundle', 'channel', 'files', - 'organization', 'private', - 'statistics', - 'triggers', - 'webhooks', +] as const + +export const CAPGO_CLOUD_SUPABASE_FUNCTIONS = [ + ...CAPGO_CLOUD_SUPABASE_FUNCTIONS_FOREVER, + ...CAPGO_CLOUD_SUPABASE_FUNCTIONS_CLI_DEPRECATION, ] as const export type CapgoCloudSupabaseFunction = typeof CAPGO_CLOUD_SUPABASE_FUNCTIONS[number] diff --git a/src/components/dashboard/AppAccess.vue b/src/components/dashboard/AppAccess.vue index d2cb57553c..296a226986 100644 --- a/src/components/dashboard/AppAccess.vue +++ b/src/components/dashboard/AppAccess.vue @@ -13,6 +13,7 @@ import DataTable from '~/components/DataTable.vue' import RoleSelect from '~/components/forms/RoleSelect.vue' import SearchInput from '~/components/forms/SearchInput.vue' import RoleSelectionModal from '~/components/modals/RoleSelectionModal.vue' +import { invokeCapgoApi } from '~/services/capgoApi' import { formatLocalDate } from '~/services/date' import { checkPermissions } from '~/services/permissions' import { useSupabase } from '~/services/supabase' @@ -151,7 +152,7 @@ async function fetchAppRoleBindings() { isLoading.value = true try { - const { data, error } = await supabase.functions.invoke(`private/role_bindings/${ownerOrg.value}`, { + const { data, error } = await invokeCapgoApi(`private/role_bindings/${ownerOrg.value}`, { method: 'GET', }) @@ -295,7 +296,7 @@ async function fetchAvailableGroups() { return try { - const { data, error } = await supabase.functions.invoke(`private/groups/${ownerOrg.value}`, { + const { data, error } = await invokeCapgoApi(`private/groups/${ownerOrg.value}`, { method: 'GET', }) @@ -331,7 +332,7 @@ async function assignRole() { isLoading.value = true try { - const { error } = await supabase.functions.invoke('private/role_bindings', { + const { error } = await invokeCapgoApi('private/role_bindings', { method: 'POST', body: { principal_type: assignRoleForm.value.principal_type, @@ -377,7 +378,7 @@ async function handleEditRoleConfirm(newRoleName: string) { isEditingRole.value = true try { - const { error: updateError } = await supabase.functions.invoke(`private/role_bindings/${editRoleBinding.value.id}`, { + const { error: updateError } = await invokeCapgoApi(`private/role_bindings/${editRoleBinding.value.id}`, { method: 'PATCH', body: { role_name: newRoleName }, }) @@ -412,7 +413,7 @@ async function removeRoleBinding(bindingId: string) { isLoading.value = true try { - const { error } = await supabase.functions.invoke(`private/role_bindings/${bindingId}`, { + const { error } = await invokeCapgoApi(`private/role_bindings/${bindingId}`, { method: 'DELETE', }) diff --git a/src/components/dashboard/AppOnboardingFlow.vue b/src/components/dashboard/AppOnboardingFlow.vue index b2ec358d54..b972ad87db 100644 --- a/src/components/dashboard/AppOnboardingFlow.vue +++ b/src/components/dashboard/AppOnboardingFlow.vue @@ -23,6 +23,7 @@ import IconStore from '~icons/lucide/store' import IconTerminal from '~icons/lucide/terminal' import IconUsers from '~icons/lucide/users-round' import { createDefaultApiKey, findUsablePlainApiKey } from '~/services/apikeys' +import { invokeCapgoApi } from '~/services/capgoApi' import { pushEvent } from '~/services/posthog' import { createSignedImageUrl, getImmediateImageUrl } from '~/services/storage' import { getLocalConfig, isLocal, useSupabase } from '~/services/supabase' @@ -412,7 +413,7 @@ async function importStoreMetadata() { const requestedRun = ++storeImportRun isImportingStore.value = true try { - const { data, error } = await supabase.functions.invoke('app/store-metadata', { + const { data, error } = await invokeCapgoApi('app/store-metadata', { method: 'POST', body: { url: requestedUrl }, }) @@ -630,7 +631,7 @@ async function createOrganizationAndApp() { isSubmitting.value = true try { - const { data, error } = await supabase.functions.invoke('organization', { + const { data, error } = await invokeCapgoApi('organization', { method: 'POST', body: { name: orgName, @@ -796,7 +797,7 @@ async function seedDemoData() { isSeedingDemo.value = true try { - const { data, error } = await supabase.functions.invoke('app/demo', { + const { data, error } = await invokeCapgoApi('app/demo', { method: 'POST', body: { owner_org: currentOrg.value.gid, diff --git a/src/components/dashboard/AppSetting.vue b/src/components/dashboard/AppSetting.vue index 78a5510aab..24076f782e 100644 --- a/src/components/dashboard/AppSetting.vue +++ b/src/components/dashboard/AppSetting.vue @@ -13,6 +13,7 @@ import transfer from '~icons/mingcute/transfer-horizontal-line?raw&width=36&heig import gearSix from '~icons/ph/gear-six?raw' import iconName from '~icons/ph/user?raw' import Toggle from '~/components/Toggle.vue' +import { invokeCapgoApi } from '~/services/capgoApi' import { checkPermissions } from '~/services/permissions' import { createSignedImageUrl, getImmediateImageUrl } from '~/services/storage' import { useSupabase } from '~/services/supabase' @@ -412,7 +413,7 @@ async function importIconFromStore() { isImportingStoreIcon.value = true try { - const { data, error } = await supabase.functions.invoke('app/store-metadata', { + const { data, error } = await invokeCapgoApi('app/store-metadata', { method: 'POST', body: { url: storeImportUrl.value }, }) diff --git a/src/components/dashboard/InviteTeammateModal.vue b/src/components/dashboard/InviteTeammateModal.vue index 79d8b7544d..194b7195ca 100644 --- a/src/components/dashboard/InviteTeammateModal.vue +++ b/src/components/dashboard/InviteTeammateModal.vue @@ -3,6 +3,7 @@ import { computed, ref, watch } from 'vue' import { useI18n } from 'vue-i18n' import { toast } from 'vue-sonner' import VueTurnstile from 'vue-turnstile' +import { invokeCapgoApi } from '~/services/capgoApi' import { useSupabase } from '~/services/supabase' import { sendEvent } from '~/services/tracking' import { useDialogV2Store } from '~/stores/dialogv2' @@ -315,7 +316,7 @@ async function handleFullDetailsSubmit() { isInviting.value = true try { - const { error } = await supabase.functions.invoke('private/invite_new_user_to_org', { + const { error } = await invokeCapgoApi('private/invite_new_user_to_org', { body: { email, org_id: orgId, diff --git a/src/components/dashboard/StepsApp.vue b/src/components/dashboard/StepsApp.vue index 36f12a7351..4d0bceea4f 100644 --- a/src/components/dashboard/StepsApp.vue +++ b/src/components/dashboard/StepsApp.vue @@ -8,6 +8,7 @@ import IconChevronDown from '~icons/lucide/chevron-down' import IconLoader from '~icons/lucide/loader-2' import InviteTeammateModal from '~/components/dashboard/InviteTeammateModal.vue' import { createDefaultApiKey, findUsablePlainApiKey } from '~/services/apikeys' +import { invokeCapgoApi } from '~/services/capgoApi' import { pushEvent } from '~/services/posthog' import { getLocalConfig, isLocal, useSupabase } from '~/services/supabase' import { sendEvent } from '~/services/tracking' @@ -167,7 +168,7 @@ async function createDemoApp() { }).catch() pushEvent('user:onboarding-create-demo-app', config.supaHost, { org_id: orgId }) - const { data, error } = await supabase.functions.invoke('app/demo', { + const { data, error } = await invokeCapgoApi('app/demo', { method: 'POST', body: { owner_org: orgId, diff --git a/src/components/dashboard/Usage.vue b/src/components/dashboard/Usage.vue index fa0cf0e222..b2ffad83b0 100644 --- a/src/components/dashboard/Usage.vue +++ b/src/components/dashboard/Usage.vue @@ -10,6 +10,7 @@ import BanknotesIcon from '~icons/heroicons/banknotes' import CalendarDaysIcon from '~icons/heroicons/calendar-days' import ChartBarIcon from '~icons/heroicons/chart-bar' import InformationInfo from '~icons/heroicons/information-circle' +import { invokeCapgoApi } from '~/services/capgoApi' import { bytesToGb, getDaysBetweenDates } from '~/services/conversion' import { CHART_REFRESH_POLL_MS, @@ -25,7 +26,7 @@ import { } from '~/services/dashboardRefresh' import { formatLocalDate, formatLocalDateTime, formatUtcDateTimeAsLocal } from '~/services/date' import { DEMO_APP_NAMES, generateDemoBandwidthData, generateDemoMauData, generateDemoStorageData } from '~/services/demoChartData' -import { getPlans, useSupabase } from '~/services/supabase' +import { getPlans } from '~/services/supabase' import { useDashboardAppsStore } from '~/stores/dashboardApps' import { useDialogV2Store } from '~/stores/dialogv2' import { useMainStore } from '~/stores/main' @@ -479,9 +480,8 @@ async function getAppStats(rangeStart: Date, rangeEnd: Date) { } } - const supabase = useSupabase() const dateRange = `?from=${rangeStart.toISOString()}&to=${rangeEnd.toISOString()}&noAccumulate=true` - const response = await supabase.functions.invoke(`statistics/app/${props.appId}/${dateRange}`, { + const response = await invokeCapgoApi(`statistics/app/${props.appId}/${dateRange}`, { method: 'GET', }) diff --git a/src/components/permissions/ChannelAccessPanel.vue b/src/components/permissions/ChannelAccessPanel.vue index 4dcef10bc2..8010eb43ce 100644 --- a/src/components/permissions/ChannelAccessPanel.vue +++ b/src/components/permissions/ChannelAccessPanel.vue @@ -5,6 +5,7 @@ import { toast } from 'vue-sonner' import IconPlus from '~icons/heroicons/plus' import IconTrash from '~icons/heroicons/trash' import ChannelPermissionOverridesPanel from '~/components/permissions/ChannelPermissionOverridesPanel.vue' +import { invokeCapgoApi } from '~/services/capgoApi' import { useSupabase } from '~/services/supabase' import { getRbacRoleI18nKey } from '~/stores/organization' import { getErrorCode, getErrorMessage } from '~/utils/errors' @@ -144,7 +145,7 @@ async function loadChannelAccess() { channels.value = (channelsResult.data || []) as ChannelSummary[] channelRoles.value = (rolesResult.data || []) as Role[] - const { data, error } = await supabase.functions.invoke(`private/role_bindings/app/${props.appUuid}/channel`, { + const { data, error } = await invokeCapgoApi(`private/role_bindings/app/${props.appUuid}/channel`, { method: 'GET', }) @@ -180,7 +181,7 @@ async function addChannelRole() { isSaving.value = true try { - const { error } = await supabase.functions.invoke('private/role_bindings', { + const { error } = await invokeCapgoApi('private/role_bindings', { method: 'POST', body: { principal_type: props.principalType, @@ -224,7 +225,7 @@ async function updateChannelRole(binding: ChannelRoleBinding, event: Event) { isSaving.value = true try { - const { error } = await supabase.functions.invoke(`private/role_bindings/${binding.id}`, { + const { error } = await invokeCapgoApi(`private/role_bindings/${binding.id}`, { method: 'PATCH', body: { role_name: role.name }, }) @@ -251,7 +252,7 @@ async function removeChannelRole(binding: ChannelRoleBinding) { isSaving.value = true try { - const { error } = await supabase.functions.invoke(`private/role_bindings/${binding.id}`, { + const { error } = await invokeCapgoApi(`private/role_bindings/${binding.id}`, { method: 'DELETE', }) diff --git a/src/components/tables/AccessTable.vue b/src/components/tables/AccessTable.vue index 457bb7f958..96ccf04662 100644 --- a/src/components/tables/AccessTable.vue +++ b/src/components/tables/AccessTable.vue @@ -9,6 +9,7 @@ import IconShield from '~icons/heroicons/shield-check' import IconTrash from '~icons/heroicons/trash' import IconWrench from '~icons/heroicons/wrench' import ChannelAccessPanel from '~/components/permissions/ChannelAccessPanel.vue' +import { invokeCapgoApi } from '~/services/capgoApi' import { formatDate } from '~/services/date' import { checkPermissions } from '~/services/permissions' import { useSupabase } from '~/services/supabase' @@ -185,7 +186,7 @@ async function loadAccessReferenceData() { const nextPrincipalOptions: PrincipalOption[] = [] if (canUpdateUserRoles.value) { - const { data: principals, error: principalsError } = await supabase.functions.invoke(`private/role_bindings/app/${app.value.id}/principals`, { method: 'GET' }) + const { data: principals, error: principalsError } = await invokeCapgoApi(`private/role_bindings/app/${app.value.id}/principals`, { method: 'GET' }) if (principalsError) { console.error('Error loading assignable principals for app access:', principalsError) @@ -367,7 +368,7 @@ async function assignAccessRole() { return false } - const { error } = await supabase.functions.invoke('private/role_bindings', { + const { error } = await invokeCapgoApi('private/role_bindings', { method: 'POST', body: { principal_type: assignAccessForm.value.principal_type, @@ -457,7 +458,7 @@ async function changeUserRole(element: Element) { isLoading.value = true try { - const { error: updateError } = await supabase.functions.invoke(`private/role_bindings/${element.id}`, { + const { error: updateError } = await invokeCapgoApi(`private/role_bindings/${element.id}`, { method: 'PATCH', body: { role_name: newRoleName }, }) @@ -499,7 +500,7 @@ async function deleteElement(element: Element) { isLoading.value = true try { - const { error } = await supabase.functions.invoke(`private/role_bindings/${element.id}`, { + const { error } = await invokeCapgoApi(`private/role_bindings/${element.id}`, { method: 'DELETE', }) diff --git a/src/pages/ApiKeys.vue b/src/pages/ApiKeys.vue index 3dfd75ff39..b3a3ac35dc 100644 --- a/src/pages/ApiKeys.vue +++ b/src/pages/ApiKeys.vue @@ -23,6 +23,7 @@ import { showApiKeySecretModal, sortApiKeyRows, } from '~/services/apikeys' +import { invokeCapgoApi } from '~/services/capgoApi' import { formatLocalDate } from '~/services/date' import { isNativeAppStoreContext } from '~/services/nativeCompliance' import { checkPermissions } from '~/services/permissions' @@ -881,7 +882,7 @@ async function refreshData() { async function getKeys(retry = true): Promise { isLoading.value = true - const { data, error } = await supabase.functions.invoke('apikey', { + const { data, error } = await invokeCapgoApi('apikey', { method: 'GET', }) if (error) { @@ -1042,7 +1043,7 @@ async function createApiKey() { let plainKeyForDisplay: string | null = null - const { data, error } = await supabase.functions.invoke('apikey', { + const { data, error } = await invokeCapgoApi('apikey', { method: 'POST', body: { name: newApiKeyName.value.trim(), @@ -1252,7 +1253,7 @@ async function updateApiKey() { if (setExpirationCheckbox.value && expirationDate.value) expiresAt = dayjs(expirationDate.value).toISOString() - const { data, error } = await supabase.functions.invoke('apikey', { + const { data, error } = await invokeCapgoApi('apikey', { method: 'PUT', body: { id: key.id, @@ -1298,7 +1299,7 @@ async function regenrateKey(apikey: Database['public']['Tables']['apikeys']['Row const wasHashed = isHashedKey(apikey) - const { data, error } = await supabase.functions.invoke('apikey', { + const { data, error } = await invokeCapgoApi('apikey', { method: 'PUT', body: { id: apikey.id, diff --git a/src/pages/app/[app].channel.[channel].devices.vue b/src/pages/app/[app].channel.[channel].devices.vue index f8bb627983..ca0ebebb0f 100644 --- a/src/pages/app/[app].channel.[channel].devices.vue +++ b/src/pages/app/[app].channel.[channel].devices.vue @@ -8,6 +8,7 @@ import { useRoute, useRouter } from 'vue-router' import { toast } from 'vue-sonner' import plusOutline from '~icons/ion/add-outline' import IconAlertCircle from '~icons/lucide/alert-circle' +import { invokeCapgoApi } from '~/services/capgoApi' import { defaultApiHost, useSupabase } from '~/services/supabase' import { withBuiltinChannelVersion } from '~/services/versions' import { useAppDetailStore } from '~/stores/appDetail' @@ -147,7 +148,7 @@ async function customDeviceOverwritePart5( return } - const { error: addDeviceError } = await supabase.functions.invoke('private/create_device', { + const { error: addDeviceError } = await invokeCapgoApi('private/create_device', { body: { device_id: deviceId, app_id: route.params.app as string, diff --git a/src/pages/invitation.vue b/src/pages/invitation.vue index ef460a01ae..36010fe9a2 100644 --- a/src/pages/invitation.vue +++ b/src/pages/invitation.vue @@ -9,6 +9,7 @@ import IconShield from '~icons/lucide/shield-check' import IconX from '~icons/lucide/x' import { authGhostButtonClass, authInsetCardClass, authPrimaryButtonClass, authSecondaryButtonClass } from '~/components/auth/pageStyles' import Toggle from '~/components/Toggle.vue' +import { invokeCapgoApi } from '~/services/capgoApi' import { useSupabase } from '~/services/supabase' import { openSupport } from '~/services/support' @@ -26,8 +27,6 @@ const inviteRow = ref -const supabase = useSupabase() - // Terms and marketing acceptance const acceptTerms = ref(false) const acceptMarketing = ref(true) @@ -123,7 +122,7 @@ async function submitForm() { isLoading.value = true // Call the backend API to accept the invitation using Supabase Functions - const { data, error } = await supabase.functions.invoke('private/accept_invitation', { + const { data, error } = await invokeCapgoApi('private/accept_invitation', { body: { password: password.value, magic_invite_string: inviteMagicString.value, diff --git a/src/pages/onboarding/organization.vue b/src/pages/onboarding/organization.vue index 101286abd4..05952cbf68 100644 --- a/src/pages/onboarding/organization.vue +++ b/src/pages/onboarding/organization.vue @@ -19,6 +19,7 @@ import IconUserPlus from '~icons/lucide/user-plus' import IconUsers from '~icons/lucide/users-round' import IconBack from '~icons/material-symbols/arrow-back-ios-rounded' import InviteTeammateModal from '~/components/dashboard/InviteTeammateModal.vue' +import { invokeCapgoApi } from '~/services/capgoApi' import { formatNumberValue } from '~/services/formatLocale' import { createOnboardingAppFromDraft } from '~/services/onboardingAppCreate' import { uploadOrgLogoFile } from '~/services/photos' @@ -358,7 +359,7 @@ async function fetchWebsitePreview() { isLoadingWebsitePreview.value = true try { - const { data, error } = await supabase.functions.invoke('private/website_preview', { + const { data, error } = await invokeCapgoApi('private/website_preview', { body: { website: websiteInput.value.trim(), }, @@ -415,7 +416,7 @@ async function createOrganization() { ? websitePreview.value?.website : undefined - const { data, error } = await supabase.functions.invoke('organization', { + const { data, error } = await invokeCapgoApi('organization', { method: 'POST', body: { name: orgName, diff --git a/src/pages/settings/account/ChangePassword.vue b/src/pages/settings/account/ChangePassword.vue index 6b55561d02..303c0e0368 100644 --- a/src/pages/settings/account/ChangePassword.vue +++ b/src/pages/settings/account/ChangePassword.vue @@ -1,11 +1,13 @@