DEV Community

kanta13jp1
kanta13jp1

Posted on

From 94 to 15 Supabase Edge Functions: Hub Architecture + CORS Fix Pattern

From 94 to 15 Supabase Edge Functions: Hub Architecture + CORS Fix Pattern

The Problem: Hitting the 99-Function Wall

Supabase caps Edge Functions at 99 per project. After months of adding one function per feature, 自分株式会社 reached 94. The solution: action-based hub integration.


The Action-Based Hub Pattern

Instead of one function per feature, one hub function handles many related features via an action field:

// growth-hub/index.ts
serve(async (req: Request) => {
  const body = await req.json();
  const action = body.action as string;

  switch (action) {
    case "referral.list":     return handleReferralList(admin, userId);
    case "referral.create":   return handleReferralCreate(admin, userId, body);
    case "acquisition.track": return handleAcquisitionTrack(admin, userId, body);
    case "roadmap.progress":  return handleRoadmapProgress(admin, userId);
    // ... 16 actions total
  }
});
Enter fullscreen mode Exit fullscreen mode

Dart side:

// OLD (deprecated function)
await client.functions.invoke('growth-referral',
  body: {'action': 'get_code'});

// NEW (hub + action name)
await client.functions.invoke('growth-hub',
  body: {'action': 'referral.list'});
Enter fullscreen mode Exit fullscreen mode

Final Hub Architecture (15 Functions)

Standalone (4):
  get-home-dashboard
  ai-assistant
  guitar-recording-studio
  local-election-intelligence

Macro-hubs (6):
  core-hub       — notifications, notes, feedback, achievements
  growth-hub     — referrals, acquisition, roadmap progress
  ai-hub         — AI features
  admin-hub      — support, admin, competitor monitoring
  app-hub        — app features
  schedule-hub   — scheduling

Mega-hubs (5):
  tools-hub, media-hub, enterprise-hub,
  social-commerce-hub, lifestyle-hub
Enter fullscreen mode Exit fullscreen mode

94 → 15 functions (84% reduction).


The CORS Error That Wasn't a CORS Error

When a deleted Edge Function still gets called from Flutter, the browser shows:

Access to XMLHttpRequest at
'https://xxx.supabase.co/functions/v1/growth-referral'
has been blocked by CORS policy: Response to preflight request
doesn't pass access control check: No 'Access-Control-Allow-Origin' header
Enter fullscreen mode Exit fullscreen mode

Root cause: The deleted function returns 404. A 404 response has no CORS headers. The browser interprets "no CORS headers" as a CORS error — not a 404. This is misleading.

Fix: Find every call to the old function name and update it to the hub equivalent.

# Find all references to the deleted function
grep -rn "growth-referral" lib/ --include="*.dart"

# 17 hits → replace with growth-hub + action name
Enter fullscreen mode Exit fullscreen mode

17 Deprecated Functions Fixed

Deprecated EF New Hub Action
growth-referral growth-hub referral.list/create
notification-center core-hub notification.list/mark_read
development-achievements core-hub achievements.list/add
submit-feedback core-hub feedback.submit
weather-widget tools-hub get_weather
wiki-database enterprise-hub wiki.*
voice-memo-transcriber media-hub transcribe.*
gantt-timeline-manager enterprise-hub gantt.*
code-playground enterprise-hub playground.*
real-estate-tracker lifestyle-hub realestate.*
(and 7 more)

CI Hard Cap: Fail if > 50 Functions

After the reduction, added a CI check to prevent creep back:

- name: Check Edge Function deploy count (hard cap 50)
  run: |
    DEPLOYED=$(grep "supabase functions deploy" .github/workflows/deploy-prod.yml \
      | grep -v "^#\|#" | awk '{print $4}' | sort)
    DEPLOY_COUNT=$(echo "$DEPLOYED" | grep -c . || true)
    if [ "$DEPLOY_COUNT" -gt 50 ]; then
      echo "❌ EF hard cap exceeded: ${DEPLOY_COUNT} > 50"
      exit 1
    fi
    echo "✅ EF count OK: ${DEPLOY_COUNT} <= 50"
  continue-on-error: false
Enter fullscreen mode Exit fullscreen mode

Any PR that adds a new standalone function fails CI until an existing function is consolidated.


Bonus: publicActions for Service-to-Service Auth

GitHub Actions calls the schedule-hub EF using SERVICE_ROLE_KEY, not a user JWT. The EF's normal auth check (getUser()) returns null and rejects with 401.

Fix: a publicActions array that skips user auth:

// schedule-hub/index.ts
const publicActions = ["digest.run", "health.check", "blog.auto_publish", "blog.create"];

let userId: string | null = null;
if (!publicActions.includes(action)) {
  userId = await getUserId(req);
  if (!userId) return json({ error: "Unauthorized" }, 401);
}
Enter fullscreen mode Exit fullscreen mode

Actions in publicActions bypass user auth entirely — service callers pass straight through.


Results

Metric Before After
Deployed functions 94 15
Production CORS errors 17 functions worth 0
CI EF guard None 50-function hard cap
GitHub Actions → EF auth 401 errors publicActions bypass

Rule of thumb: Every new feature should add an action to an existing hub, not create a new function. The hard cap CI check enforces this automatically.


Try it: 自分株式会社

buildinpublic #Flutter #Supabase #architecture #devops

Top comments (0)