Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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 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)

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
22 changes: 19 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 `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)

Expand Down Expand Up @@ -448,7 +450,21 @@ 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
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
62 changes: 62 additions & 0 deletions scripts/supabase-cloud-functions.ts
Original file line number Diff line number Diff line change
@@ -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<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)
}
39 changes: 39 additions & 0 deletions tests/supabase-cloud-functions.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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('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')
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)
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})
Loading