Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/build_and_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,11 @@ jobs:
- name: Update functions
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_TOKEN }}
run: supabase functions deploy
# 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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: If the script fails, FUNCTIONS will be empty and supabase functions deploy will silently deploy ALL functions, defeating the allowlist. Add a guard: if [ ${#FUNCTIONS[@]} -eq 0 ]; then ... exit 1; fi

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/build_and_deploy.yml, line 98:

<comment>If the script fails, FUNCTIONS will be empty and `supabase functions deploy` will silently deploy ALL functions, defeating the allowlist. Add a guard: `if [ ${#FUNCTIONS[@]} -eq 0 ]; then ... exit 1; fi`</comment>

<file context>
@@ -92,7 +92,11 @@ jobs:
+        # 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[@]}"
 
</file context>

supabase functions deploy "${FUNCTIONS[@]}"

read_replica_schema:
needs: changes
Expand Down
23 changes: 20 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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)

Expand Down Expand Up @@ -448,7 +450,22 @@ 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`: `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
# or: bunx supabase functions deploy $(bun scripts/supabase-cloud-functions.ts deploy-args) --project-ref <ref>
```

### Deploy self-hosted Supabase functions

Self-hosted installs should keep deploying every function:

```bash
supabase functions deploy
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
77 changes: 77 additions & 0 deletions scripts/supabase-cloud-functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { readdirSync } from 'node:fs'
import { join } from 'node:path'
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)
*
* 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]

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<string>(CAPGO_CLOUD_SUPABASE_FUNCTIONS)
return localFunctions.filter(name => !keep.has(name))
}

export function buildCapgoCloudSupabaseDeployArgs(
functions: readonly string[] = CAPGO_CLOUD_SUPABASE_FUNCTIONS,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The deploy-argument helper can emit arbitrary function names when a caller supplies its optional argument, so a future caller can bypass the cloud allowlist while still using the allowlist-named API. Typing this parameter as readonly CapgoCloudSupabaseFunction[] keeps the helper aligned with its security boundary.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/supabase-cloud-functions.ts, line 37:

<comment>The deploy-argument helper can emit arbitrary function names when a caller supplies its optional argument, so a future caller can bypass the cloud allowlist while still using the allowlist-named API. Typing this parameter as `readonly CapgoCloudSupabaseFunction[]` keeps the helper aligned with its security boundary.</comment>

<file context>
@@ -0,0 +1,62 @@
+}
+
+export function buildCapgoCloudSupabaseDeployArgs(
+  functions: readonly string[] = CAPGO_CLOUD_SUPABASE_FUNCTIONS,
+): string[] {
+  if (functions.length === 0)
</file context>

): 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)
}
55 changes: 55 additions & 0 deletions tests/supabase-cloud-functions.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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 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', () => {
expect(() => buildCapgoCloudSupabaseDeployArgs([])).toThrow(/must not be empty/)
})

it('passes through multiple explicit deploy targets', () => {
expect(buildCapgoCloudSupabaseDeployArgs(['triggers', 'ok'])).toEqual(['triggers', 'ok'])
})

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')

const skipped = listCapgoCloudSkippedSupabaseFunctions(local)
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('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)
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})
Loading