Skip to content

Conversation

@Dalanir
Copy link
Contributor

@Dalanir Dalanir commented Jan 12, 2026

Summary (AI generated)

  • Added comprehensive documentation for Capgo's RBAC (Role-Based Access Control) system
  • Documented hybrid architecture supporting both legacy and new RBAC systems
  • Complete definition of database tables for role and permission management
  • Documentation of SQL functions and backend/frontend integration

Motivation (AI generated)

The RBAC system enables granular access control to Capgo platform resources. This documentation is necessary to:

  • Allow developers to understand and maintain the permissions system
  • Facilitate onboarding of new developers on the security system
  • Document the progressive migration from legacy system to new RBAC
  • Provide a reference guide for implementing permissions at org, app, channel, and bundle levels

Business Impact (AI generated)

This documentation positively impacts Capgo's business by:

  • Improved security: Clear documentation of access controls and best practices
  • Reduced development time: Developers can implement permissions correctly more quickly
  • Scalability: RBAC system allows more flexible permission management for enterprise clients
  • Compliance: Documentation required for security audits and certifications
  • Bug reduction: Better system understanding reduces implementation errors

Test Plan (AI generated)

  • Verify that RBAC_SYSTEM.md file is accessible and readable
  • Validate that all SQL code examples in the documentation are syntactically correct
  • Confirm that table schemas match existing migrations in supabase/migrations/
  • Verify that SQL functions mentioned exist in the database
  • Test usage examples provided in the documentation
  • Validate that documentation covers all scopes (platform, org, app, channel, bundle)

Screenshots

Checklist

  • My code follows the code style of this project and passes bun run lint:backend && bun run lint.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • My change has adequate E2E test coverage.
  • I have tested my code manually, and I have provided steps how to reproduce my tests

Summary by CodeRabbit

  • New Features

    • Access tab and App Access page: UI to view/manage roles, role assignments and groups.
  • Permissions

    • App/org/channel/bundle/device actions now use granular RBAC checks; UI actions and visibility respect permissions.
  • Documentation

    • New comprehensive RBAC system guide covering usage, migration, and debugging.
  • Localization

    • RBAC-related translations added/updated across many locales.
  • Backend

    • New RBAC APIs and centralized permission services.
  • Tests

    • New SQL and integration tests validating RBAC behavior and mappings.

✏️ Tip: You can customize this high-level summary in your review settings.

@Dalanir Dalanir requested a review from riderx January 12, 2026 15:24
@Dalanir Dalanir self-assigned this Jan 12, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 12, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds a new RBAC system: DB schema, SQL RPCs, backend RBAC utilities/middleware, private REST endpoints for groups/roles/bindings, frontend permission service and async guards, UI components for access management, translations, seeds, and SQL tests. Hybrid legacy↔RBAC gating via org/global flags.

Changes

Cohort / File(s) Summary
Documentation
RBAC_SYSTEM.md
New comprehensive RBAC technical documentation (model, SQL functions, algorithms, migrations, debugging, frontend/backend integration).
DB schema, types & seeds
supabase/functions/_backend/utils/postgres_schema.ts, supabase/functions/_backend/utils/supabase.types.ts, src/types/supabase.types.ts, supabase/seed.sql
Added RBAC tables (roles, role_bindings, role_permissions, role_hierarchy, permissions, groups, group_members, rbac_settings, new users), added rbac_id/use_new_rbac fields, seed changes and migrations.
SQL tests
supabase/tests/*.sql, tests/rbac-permissions.test.ts
New end-to-end and RLS-focused SQL tests validating legacy vs RBAC paths and permission mappings.
RBAC backend core
supabase/functions/_backend/utils/rbac.ts, multiple supabase/functions/_backend/* callers
New RBAC utility (checkPermission, batch helpers, pg helpers), legacy mapping helpers; many backend handlers switched from legacy has* helpers to checkPermission/rbac-aware flows.
Middleware & context
supabase/functions/_backend/utils/hono.ts, .../hono_middleware.ts
New RBAC-aware middleware middlewareRbacContext, expanded MiddlewareKeyVariables with rbacEnabled and resolvedOrgId, centralized header/key resolution.
Private REST APIs
supabase/functions/_backend/private/{groups.ts,role_bindings.ts,roles.ts}
New Hono apps exposing group, role, and role_binding CRUD endpoints with validation, RBAC enforcement, and transactional handling.
Public function signatures
supabase/functions/_backend/public/** (many)
Many public handlers now use Context<MiddlewareKeyVariables> and RBAC checkPermission gates replacing per-key/org helpers; signatures/types adjusted accordingly.
Supabase utilities & triggers
supabase/functions/_backend/triggers/on_user_delete.ts, supabase/functions/_backend/utils/org_email_notifications.ts
Triggers and notification helpers updated to aggregate RBAC bindings, groups, and legacy sources; expanded email preference schema.
Cloud/private routing
supabase/functions/private/index.ts, cloudflare_workers/api/index.ts
Registered new private routes /groups and /role_bindings; Cloudflare worker exposes groups private endpoint.
Frontend permission service
src/services/permissions.ts
New typed frontend service: hasPermission, checkPermissions (single/batch), scope types and options wrapping RPC rbac_check_permission.
Frontend UI & components
src/components/dashboard/AppAccess.vue, src/components/tables/AccessTable.vue, src/pages/app/[package].access.vue
New App access management UI: list/assign/remove role bindings, modals, RBAC-aware data flows and permission gating.
Permission guards & pages
many src/components/*, src/pages/*, src/layouts/*
Replaced role-based checks with async computed guards (computedAsync) using checkPermissions across bundles, channels, history, settings, members, notifications, security, webhooks, devices, etc.
Members page (hybrid)
src/pages/settings/organization/Members.vue
Hybrid RBAC/legacy member flows: RPCs for RBAC members, RBAC-aware invite/role update, function signature widened to accept RBAC role strings.
Navigation & typings
src/constants/appTabs.ts, src/constants/organizationTabs.ts, src/layouts/app.vue, src/typed-router.d.ts, src/auto-imports.d.ts, src/components.d.ts, src/env.d.ts
Added access, groups, role-assignments tabs; RBAC i18n helpers exported; new route /app/[package].access; global typings and env flag for RBAC.
Translations
messages/{de,en,es,fr,hi,id,it,ja,ko,pl,pt-br,ru,tr,vi,zh-cn}.json
Added RBAC UI keys, role labels, confirmations, and error messages across locales.
Frontend small UI changes
src/components/dashboard/AppSetting.vue, src/components/tables/*, various pages
Replaced many inline role checks with permission guards; UI shown/hidden and actions gated by checkPermissions results.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Client as Frontend
participant PermSvc as PermissionService (src/services/permissions.ts)
participant Backend as Backend RPC/HTTP
participant RPC as Supabase RPC (rbac_check_permission)
participant DB as Database

Client->>PermSvc: checkPermissions(perm(s), scope)
PermSvc->>Backend: RPC call -> rbac_check_permission
Backend->>RPC: rpc('rbac_check_permission', p_permission_key..., p_org_id..., p_app_id..., p_channel_id...)
RPC->>DB: read rbac_settings / legacy org flags
alt RBAC enabled for org
    DB-->>RPC: use_new_rbac = true
    RPC->>DB: evaluate roles, role_bindings, groups, hierarchy (propagation)
    DB-->>RPC: result boolean
else RBAC disabled or legacy path
    DB-->>RPC: use_new_rbac = false
    RPC->>DB: evaluate legacy org_users / user_min_right
    DB-->>RPC: result boolean
end
RPC-->>Backend: result
Backend-->>PermSvc: result
PermSvc-->>Client: Promise<boolean>

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • riderx

Poem

🐰 I hopped through roles and bindings bright,
Scopes and groups aligned just right,
Old checks whispered as new rules grew,
From DB to UI permissions flew,
A rabbit claps — RBAC now in sight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: RBAC system v1' is clear and specific, accurately reflecting the main change—addition of comprehensive RBAC system documentation and implementation.
Description check ✅ Passed The PR description provides a comprehensive summary, motivation, business impact, and test plan. However, the test plan items are largely unchecked, and E2E test coverage and manual reproduction steps are incomplete per the checklist.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (22)
supabase/functions/_backend/public/channel/delete.ts (1)

50-55: Delete operation result is not checked—potential silent failure.

The delete query result is not captured or validated. If the deletion fails due to RLS policies, foreign key constraints, or database errors, the function will still return a success response, misleading the caller.

🔧 Proposed fix to check delete result
-  await supabaseApikey(c, apikey.key)
+  const { error: deleteError } = await supabaseApikey(c, apikey.key)
     .from('channels')
     .delete()
     .eq('app_id', body.app_id)
     .eq('name', body.channel)
+  if (deleteError) {
+    throw simpleError('cannot_delete_channel', 'Failed to delete channel', { supabaseError: deleteError })
+  }
   return c.json(BRES)
planetscale/replicate_to_planetscale.sh (1)

80-88: Stop logging DB credentials (password is printed directly, and also via echoed SQL).
Line 81 prints SOURCE_PASSWORD, and the subscription SQL echoed at Line 185 includes the password; this is a secrets leak risk.

Proposed fix (mask/remove sensitive logging)
 echo "SOURCE_USER: $SOURCE_USER"
-echo "SOURCE_PASSWORD: $SOURCE_PASSWORD"
+echo "SOURCE_PASSWORD: [redacted]"
 echo "HOST: $SOURCE_HOST"
 echo "PORT: $SOURCE_PORT"
 echo "DB: $SOURCE_DB"
 # exit()
 # Restore file
 DUMP_FILE='data_replicate.dump'
 echo "Subscription creation SQL:"
-echo "$SQL_QUERY_SUB"
+echo "[redacted: subscription SQL contains credentials]"
 psql-17 "$TARGET_DB_URL" -v ON_ERROR_STOP=1 <<SQL
 $SQL_QUERY_SUB
 SQL
supabase/functions/_backend/utils/pg.ts (1)

187-204: Don’t log raw dbUrl (it can contain DB credentials).

This is a high-risk secret leak in logs; redact user/password at minimum.

Proposed fix (redact credentials)
 export function getPgClient(c: Context, readOnly = false) {
   const dbUrl = getDatabaseURL(c, readOnly)
   const requestId = c.get('requestId')
   const appName = c.res.headers.get('X-Worker-Source') ?? 'unknown source'
   const dbName = c.res.headers.get('X-Database-Source') ?? 'unknown source'
-  cloudlog({ requestId, message: 'SUPABASE_DB_URL', dbUrl, dbName, appName })
+  let safeDbUrl = dbUrl
+  try {
+    const u = new URL(dbUrl)
+    if (u.username)
+      u.username = 'REDACTED'
+    if (u.password)
+      u.password = 'REDACTED'
+    safeDbUrl = u.toString()
+  }
+  catch {
+    // ignore parse errors; keep minimal info
+    safeDbUrl = 'UNPARSEABLE_DB_URL'
+  }
+  cloudlog({ requestId, message: 'SUPABASE_DB_URL', dbUrl: safeDbUrl, dbName, appName })
 
   const isPooler = dbName.startsWith('sb_pooler')
   const options = {
     connectionString: dbUrl,
messages/en.json (1)

153-155: Fix duplicate translation keys in messages/en.json (JSON last-write-wins behavior).

This file contains duplicate keys (no-organization-selected, date, create) that will silently override earlier definitions and can break i18n tooling or export flows. Remove the duplicate entries from the later block:

Minimal fix
-  "no-organization-selected": "No organization selected",
   "assigned": "Assigned",
   "removed": "Removed",
   "event": "Event",
   "by": "By",
-  "date": "Date",
   "bundle-deployed": "Bundle Deployed",

Also note: create appears twice with different values ("create" vs "Create" at lines 498 and 1320), and there is a case-sensitive collision risk with access vs Access (lines 1482 and 1516).

supabase/functions/_backend/public/build/start.ts (1)

43-75: Handle hashed API keys: apikey.key! can be null → runtime crash.

checkPermission explicitly supports hashed keys by falling back to c.get('capgkey'), but this function still does const apikeyKey = apikey.key! and uses it for DB updates. With hashed keys, that can be null and will blow up / skip marking builds failed.

Proposed diff
 export async function startBuild(
   c: Context,
   jobId: string,
   appId: string,
   apikey: Database['public']['Tables']['apikeys']['Row'],
 ): Promise<Response> {
   let alreadyMarkedAsFailed = false
-  const apikeyKey = apikey.key!
+  // `apikey.key` can be null for hashed keys; use the raw key from request context.
+  const apikeyKey = apikey.key ?? c.get('capgkey')
+  if (!apikeyKey) {
+    cloudlogErr({
+      requestId: c.get('requestId'),
+      message: 'Missing API key in context (cannot update build_requests)',
+      job_id: jobId,
+      app_id: appId,
+      user_id: apikey.user_id,
+    })
+    throw simpleError('not_authorized', 'Missing API key')
+  }

   try {
@@
     if (!(await checkPermission(c, 'app.build_native', { appId }))) {
       const errorMsg = 'You do not have permission to start builds for this app'
@@
       await markBuildAsFailed(c, jobId, errorMsg, apikeyKey)
supabase/functions/_backend/public/build/cancel.ts (1)

23-67: Handle hashed API keys when updating build_requests after cancel.

The RBAC check supports hashed keys, but the DB update path still uses supabaseApikey(c, apikey.key) which may be null for hashed keys (same issue as startBuild).

Proposed diff
   // Update build_requests status to cancelled
   // Use authenticated client for data queries - RLS will enforce access
-  const supabase = supabaseApikey(c, apikey.key)
+  const apikeyKey = apikey.key ?? c.get('capgkey')
+  if (!apikeyKey) {
+    cloudlogErr({
+      requestId: c.get('requestId'),
+      message: 'Missing API key in context (cannot update build_requests)',
+      job_id: jobId,
+      app_id: appId,
+      user_id: apikey.user_id,
+    })
+    throw simpleError('not_authorized', 'Missing API key')
+  }
+  const supabase = supabaseApikey(c, apikeyKey)
supabase/functions/_backend/private/download_link.ts (1)

1-21: Use createHono to initialize the Hono app, but the proposed fix is incomplete.

The guideline requires using createHono from utils/hono.ts for Hono initialization. However, createHono(functionName: string, version: string, sentryDsn?: string) requires at least two parameters (functionName and version). The proposed diff calls it with no arguments, which will fail at runtime.

Additionally, the actual codebase pattern uses honoFactory.createApp() (as seen in other backend files), which differs from this guideline requirement.

Corrected approach needed

Option 1: Use createHono with proper parameters:

-import { Hono } from 'hono/tiny'
+import { createHono } from '../utils/hono.ts'
 import { middlewareAuth, parseBody, simpleError, useCors } from '../utils/hono.ts'

-export const app = new Hono<MiddlewareKeyVariables>()
+export const app = createHono('download_link', '1.0.0')

Option 2: Align with actual backend pattern:

-import { Hono } from 'hono/tiny'
+import { honoFactory } from '../utils/hono.ts'
-import { middlewareAuth, parseBody, simpleError, useCors } from '../utils/hono.ts'
+import { honoFactory, middlewareAuth, parseBody, simpleError, useCors } from '../utils/hono.ts'

-export const app = new Hono<MiddlewareKeyVariables>()
+export const app = honoFactory.createApp()

Verify which approach aligns with the intended guideline requirement.

supabase/functions/_backend/private/delete_failed_version.ts (2)

20-25: Do not log apikey / capgkey (secret leakage)
These logs can leak credentials into log storage and third-party sinks.

Proposed change
   const apikey = c.get('apikey')
   const capgkey = c.get('capgkey') as string
-  cloudlog({ requestId: c.get('requestId'), message: 'apikey', apikey })
-  cloudlog({ requestId: c.get('requestId'), message: 'capgkey', capgkey })
+  cloudlog({
+    requestId: c.get('requestId'),
+    message: 'auth context',
+    hasApikey: !!apikey,
+    capgkeyPrefix: capgkey ? capgkey.slice(0, 8) : null,
+  })

18-35: Move body validation before RBAC check (avoid “401” for malformed requests)
Right now a missing app_id/name won’t hit the 400s because checkPermission executes first.

Proposed change
 app.delete('/', middlewareKey(['all', 'write', 'upload']), async (c) => {
   const body = await parseBody<DataUpload>(c)
   cloudlog({ requestId: c.get('requestId'), message: 'delete failed version body', body })
+
+  if (!body.app_id) {
+    return quickError(400, 'error_app_id_missing', 'Error app_id missing', { body })
+  }
+  if (!body.name) {
+    return quickError(400, 'error_bundle_name_missing', 'Error bundle name missing', { body })
+  }
@@
-  // Auth context is already set by middlewareKey
-  if (!(await checkPermission(c, 'bundle.delete', { appId: body.app_id }))) {
+  // Auth context is already set by middlewareKey
+  if (!(await checkPermission(c, 'bundle.delete', { appId: body.app_id }))) {
     return quickError(401, 'not_authorized', 'You can\'t access this app', { app_id: body.app_id })
   }
@@
-  if (!body.app_id) {
-    return quickError(400, 'error_app_id_missing', 'Error bundle name missing', { body })
-  }
-  if (!body.name) {
-    return quickError(400, 'error_bundle_name_missing', 'Error bundle name missing', { body })
-  }

Also applies to: 45-50

supabase/functions/_backend/private/upload_link.ts (1)

23-36: Do not log apikey / capgkey (secret leakage) + validate body.app_id before RBAC check

Proposed change
   const apikey = c.get('apikey') as Database['public']['Tables']['apikeys']['Row']
   const capgkey = c.get('capgkey') as string
-  cloudlog({ requestId: c.get('requestId'), message: 'apikey', apikey })
-  cloudlog({ requestId: c.get('requestId'), message: 'capgkey', capgkey })
+  cloudlog({
+    requestId: c.get('requestId'),
+    message: 'auth context',
+    apikeyId: apikey?.id ?? null,
+    capgkeyPrefix: capgkey ? capgkey.slice(0, 8) : null,
+  })
+
+  if (!body.app_id) {
+    return quickError(400, 'error_app_id_missing', 'Error app_id missing', { body })
+  }
@@
   // Auth context is already set by middlewareKey
   if (!(await checkPermission(c, 'app.upload_bundle', { appId: body.app_id }))) {
     throw simpleError('app_access_denied', 'You can\'t access this app', { app_id: body.app_id })
   }
src/components/tables/HistoryTable.vue (1)

416-472: Server-side permission enforcement for rollback is insufficient

The RLS UPDATE policy on the channels table uses generic check_min_rights('write') permission, while the frontend enforces a specific channel.rollback_bundle permission. A user with 'write' access but without the channel.rollback_bundle permission can bypass the UI check by calling Supabase directly:

supabase.from('channels').update({version: item.version_id}).eq('id', channelId)

The RLS policy will allow this since it only validates 'write' permission, not the specific rollback permission. Update the RLS policy to enforce the RBAC permission explicitly using rbac_check_permission_direct('channel.rollback_bundle', ...) for version updates, or create a more restrictive trigger that blocks non-authorized version changes.

src/pages/app/[package].channel.[channel].vue (1)

448-475: Fix local channel state update guard in onSelectAutoUpdate.

The assignment currently won’t run if disable_auto_update was falsy; it should update whenever channel.value exists.

Proposed fix
-  if (channel.value?.disable_auto_update)
-    channel.value.disable_auto_update = value
+  if (channel.value)
+    channel.value.disable_auto_update = value
supabase/functions/_backend/public/organization/delete.ts (2)

69-73: Use capgkey (original key string) for supabaseApikey to support hashed keys safely.

If apikey.key is null for hashed keys, this call can break auth (or behave unexpectedly). Other endpoints in this PR use c.get('capgkey') for this reason.

Proposed fix
-  const { error } = await supabaseApikey(c, apikey.key)
+  const { error } = await supabaseApikey(c, c.get('capgkey') as string)
     .from('orgs')
     .delete()
     .eq('id', orgId)

26-67: Re-check supabaseAdmin usage in a public endpoint (least privilege).

This is user-triggered deletion code; using the admin client for storage deletion is high impact if anything upstream is bypassed. If this must remain, consider adding extra guardrails (defense-in-depth) and ensure logs don’t leak sensitive paths.

supabase/functions/_backend/public/organization/members/get.ts (2)

14-31: Remove PII logs (email): don’t cloudlog raw members data.

cloudlog(..., data) and cloudlog(..., data: parsed.data) will emit user emails (and possibly more) into logs.

Proposed fix
-  cloudlog({ requestId: c.get('requestId'), message: 'data', data, error })
+  cloudlog({ requestId: c.get('requestId'), message: 'get_org_members result', hasData: !!data, hasError: !!error })
@@
-  cloudlog({ requestId: c.get('requestId'), message: 'Members', data: parsed.data })
+  cloudlog({ requestId: c.get('requestId'), message: 'Members fetched', count: parsed.data.length })

Also applies to: 53-63


14-31: Update schema to handle image_url as optional.

The RPC return for pending invitations sets image_url to an empty string, but for regular members it uses users.image_url which may be nullable. z.string() will fail validation if the value is null, causing a 400 error. Update to z.string().optional().

Note: supabaseApikey() already supports hashed keys via fallback to c.get('capgkey') when apikey.key is null, so no change needed there.

supabase/functions/_backend/public/organization/members/post.ts (1)

22-34: Potential null safety issue with capgkey type assertion.

The parameter is correctly renamed to _apikey to indicate it's unused. However, line 34 uses c.get('capgkey') as string which could be undefined based on the MiddlewareKeyVariables interface where capgkey?: string is optional.

Consider adding a null check or using a fallback:

Suggested improvement
-  const supabase = supabaseApikey(c, c.get('capgkey') as string)
+  const capgkey = c.get('capgkey')
+  if (!capgkey) {
+    throw simpleError('missing_api_key', 'API key is required')
+  }
+  const supabase = supabaseApikey(c, capgkey)
supabase/functions/_backend/public/app/get.ts (2)

37-96: Blocker: simpleError is used but not imported in this file.

Line 58/78/93 call simpleError(...), but the imports (Line 4) only include quickError. This should fail typecheck/build.

Proposed fix
-import { quickError } from '../../utils/hono.ts'
+import { quickError, simpleError } from '../../utils/hono.ts'

16-34: Hashed API keys likely break app fetch path.

checkPermission explicitly supports hashed keys by using c.get('capgkey') when apikey.key is null, but Line 24 uses supabaseApikey(c, apikey.key). If apikey.key is null, this will likely fail to query.

Proposed fix
-  const { data, error: dbError } = await supabaseApikey(c, apikey.key)
+  const apikeyString = apikey.key ?? c.get('capgkey')
+  if (!apikeyString) {
+    throw quickError(401, 'invalid_apikey', 'Missing API key')
+  }
+
+  const { data, error: dbError } = await supabaseApikey(c, apikeyString)
     .from('apps')
     .select('*')
     .eq('app_id', appId)
     .single()
supabase/functions/_backend/public/organization/members/delete.ts (2)

38-47: Don’t assume capgkey exists; derive apikey string safely.

Line 39 (c.get('capgkey') as string) can be undefined at runtime; the assertion only silences types. Use the same apikey-string derivation pattern as checkPermission (auth apikey key or capgkey) and fail fast if missing.

Proposed fix
-  const supabase = supabaseApikey(c, c.get('capgkey') as string)
+  const apikeyString = c.get('auth')?.apikey?.key ?? c.get('capgkey')
+  if (!apikeyString) {
+    throw simpleError('invalid_apikey', 'Missing API key')
+  }
+  const supabase = supabaseApikey(c, apikeyString)

27-36: Avoid supabaseAdmin in user-facing APIs (service key); prefer a constrained RPC.

Per repo guidelines, using the admin SDK/service key in user-facing endpoints should be avoided. Here it’s used to look up a user by email (Line 28-32). Consider a SECURITY DEFINER SQL function like get_user_id_by_email_in_org(org_id, email) (or similar) that:

  • checks caller’s org permission / membership server-side
  • returns only a UUID (or null)
    Then call it via the authenticated client (supabaseApikey).
src/pages/settings/organization/Members.vue (1)

299-416: RBAC invites will fail—RBAC role names are not valid in the user_min_right enum.

When RBAC is enabled, the frontend passes org_member, org_admin, org_billing_admin, or org_super_admin directly to sendInvitation(), which calls the invite_user_to_org RPC. However, that RPC expects the user_min_right enum type, which only contains legacy values: read, upload, write, admin, super_admin, invite_read, invite_upload, invite_write, invite_admin, invite_super_admin. RBAC role names are not part of this enum and will cause a type error at runtime.

The as any cast in handlePermissionSelection masks the mismatch at compile time but does not resolve the underlying incompatibility.

🤖 Fix all issues with AI agents
In @RBAC_SYSTEM.md:
- Around line 1-35: CI flags French words in RBAC_SYSTEM.md ("ressources",
"Fonctions", "supporte") causing the pipeline to fail; fix by either translating
the document to English or updating the typo allowlist: add these exact French
tokens (and any other valid French terms in the doc) to the .typos.toml
allowlist/extend-words entry so the linter ignores them, ensuring you update the
repository's .typos.toml rather than changing the RBAC_SYSTEM.md content unless
you choose translation.
- Around line 801-915: The function rbac_check_permission_direct currently
accepts p_user_id and is executable by the authenticated role, allowing clients
to probe other users' permissions; fix by validating the caller: at the start of
rbac_check_permission_direct add a check that if p_user_id IS NOT NULL AND
p_user_id != auth.uid() then RETURN false; alternatively implement a public
wrapper without the p_user_id parameter that calls the internal
rbac_check_permission_direct (which you then restrict to service_role only) so
client-facing RPCs cannot supply arbitrary user IDs; update permission grants
accordingly.

In @src/components/dashboard/AppAccess.vue:
- Line 73: The file uses the Ref type (e.g., Ref<TableColumn[]>) for the columns
constant but did not import Ref from Vue; add an import for Ref alongside
existing Vue imports (so the Ref type is available) and ensure the columns
declaration remains: const columns: Ref<TableColumn[]> = ref<TableColumn[]>([
... ]). This fixes the missing type reference for Ref used with TableColumn and
the columns symbol.

In @src/pages/settings/organization/Members.vue:
- Around line 82-101: The function checkRbacEnabled is missing its closing
brace, causing subsequent top-level declarations like isInviteNewUserDialogOpen
to be nested inside the function; fix by inserting the missing closing "}"
immediately after the catch block (or before the line declaring const
isInviteNewUserDialogOpen = ref(false)) so checkRbacEnabled() properly ends and
isInviteNewUserDialogOpen remains a top-level reactive ref; verify no other
braces are unbalanced in the surrounding script.

In @src/stores/organization.ts:
- Around line 30-75: The file is missing three exports referenced by
auto-imports: implement and export LEGACY_TO_RBAC_ROLE_MAPPING (reverse of
RBAC_TO_LEGACY_ROLE_MAPPING, mapping each legacy OrganizationRole to the array
of RbacRoleName(s) that include it—ensure 'owner' remains org-scoped only), add
RBAC_ORG_ROLE_DISPLAY_NAMES as a Record<RbacRoleName, string> with
human-friendly labels for each RBAC role, and implement
getRbacRoleDisplayName(role: string): string | undefined to return
RBAC_ORG_ROLE_DISPLAY_NAMES[role]; ensure all three are exported and types match
the existing RbacRoleName and OrganizationRole usages and that names exactly
match LEGACY_TO_RBAC_ROLE_MAPPING, RBAC_ORG_ROLE_DISPLAY_NAMES, and
getRbacRoleDisplayName so auto-imports compile.

In @supabase/functions/_backend/utils/hono_middleware.ts:
- Around line 26-105: middlewareRbacContext has three issues: replace direct
c.req.json() with cloning (use c.req.raw.clone().json()) before parsing so
downstream handlers can still read the body; change the resolver handling from
resolved instanceof Promise ? await resolved : resolved to await
Promise.resolve(options.orgIdResolver(c)) (or await Promise.resolve(...)) to
safely handle thenables; and await the closeClient(...) calls after both DB
usages (the block resolving app org and the RBAC-enabled check) because
closeClient returns a Promise and must be awaited to avoid leaking connections
when using getPgClient/getDrizzleClient.

In @supabase/functions/_backend/utils/rbac.ts:
- Around line 35-81: The Permission union type and the
PERMISSION_TO_LEGACY_RIGHT mapping in rbac.ts are missing the
'app.update_user_roles' entry (the DB/migrations and helper
user_has_app_update_user_roles() expect it), causing a TypeScript error; add
'app.update_user_roles' to the exported Permission type and add an entry in
PERMISSION_TO_LEGACY_RIGHT mapping that maps 'app.update_user_roles' -> 'admin'
to match other app-level admin permissions like 'app.delete' and
'app.read_audit'.

In @supabase/seed.sql:
- Around line 544-547: Replace the stray semicolon that terminates the VALUES
list after the tuple starting with (13, NOW(), 'com.stats.app', '1.0.0', ...)
with a comma so the subsequent tuples (14, …), (15, …), (16, …) are part of the
same INSERT VALUES list, and ensure the final tuple (16, …) ends the statement
with a single semicolon.
- Around line 482-485: The VALUES block is malformed: a stray semicolon ends the
first tuple and the following tuples are left dangling. Replace the semicolon
after the first tuple with a comma (or prepend a single INSERT INTO ... VALUES
(...) before the first tuple if the INSERT is missing), ensure all tuples are
comma-separated, and terminate the final tuple with a single semicolon; in
short, make the rows with the 'user_min_right'::"public"."user_min_right" casts
(e.g., tuples containing UUIDs like 'f6a7b8c9-d0e1-4f2a-9b3c-4d5e6f7a8b92') part
of one well-formed INSERT INTO ... VALUES (...) , (...) , (...) ; statement.
🟠 Major comments (21)
tests/events.test.ts-3-3 (1)

3-3: Use getEndpointUrl() instead of BASE_URL in tests (per repo testing guidelines).
Line 3 imports BASE_URL, and the file builds URLs via ${BASE_URL}/...; this can break Cloudflare Workers routing expectations. As per coding guidelines, prefer getEndpointUrl(path).

Proposed fix (switch to getEndpointUrl)
-import { APP_NAME, BASE_URL, getSupabaseClient, headers, resetAndSeedAppData, resetAndSeedAppDataStats, resetAppData, resetAppDataStats, USER_EMAIL } from './test-utils.ts'
+import { APP_NAME, getEndpointUrl, getSupabaseClient, headers, resetAndSeedAppData, resetAndSeedAppDataStats, resetAppData, resetAppDataStats, USER_EMAIL } from './test-utils.ts'
-    const response = await fetch(`${BASE_URL}/private/events`, {
+    const response = await fetch(getEndpointUrl('/private/events'), {
       method: 'POST',
       headers: {
         capgkey: headers.Authorization,
       },

(Apply the same replacement for the other ${BASE_URL}/private/events occurrences in this file.)

messages/de.json-1454-1463 (1)

1454-1463: Add missing RBAC translation keys to de.json (and other locales).

This hunk adds only the role labels at lines 1454–1463. The verification reveals de.json is missing 58 keys found in en.json, including critical RBAC strings: app-access-control, assign-app-role, assign-role, error-assigning-role, error-removing-role, error-role-already-assigned, no-role-bindings, principal-type, role-assigned, role-assignments, role-assignments-description, role-removed, scope, scope-type, and others. If the UI references these keys, they will render as raw key names in German.

This gap exists across most locales (es.json, hi.json, id.json, it.json, ja.json, ko.json, pl.json, pt-br.json, ru.json, tr.json, vi.json, zh-cn.json also missing 58 keys; fr.json missing 21).

supabase/functions/_backend/utils/postgres_schema.ts-164-169 (1)

164-169: Add explicit composite primary key to group_members table in Drizzle schema.

The group_members table definition lacks a primaryKey() declaration. The actual database migration defines PRIMARY KEY (group_id, user_id), but the Drizzle schema must explicitly declare this:

Required fix
export const group_members = pgTable('group_members', {
  group_id: uuid('group_id').notNull(),
  user_id: uuid('user_id').notNull(),
  added_by: uuid('added_by'),
  added_at: timestamp('added_at').notNull().defaultNow(),
}, (table) => ({
  pk: primaryKey({ columns: [table.group_id, table.user_id] }),
}))
supabase/functions/_backend/private/roles.ts-1-13 (1)

1-13: Use createHono instead of instantiating Hono directly.

All backend files must use createHono for Hono app initialization. This sets up essential middleware for Sentry error tracking, request logging, request ID generation (Cloudflare Ray ID, Supabase Execution ID, or UUID), and error handling. Direct instantiation with new Hono() bypasses these.

Replace export const app = new Hono<MiddlewareKeyVariables>() with an appropriate call to createHono('roles', <version>) (ensure functionName and version are provided).

RBAC_SYSTEM.md-665-707 (1)

665-707: Legacy mapping likely references non-existent enum values (invite_*).
rbac_permission_for_legacy() maps public.user_min_right values like invite_super_admin, invite_admin, invite_write, invite_upload (Line 679-682, 687-690, 695-698). Based on learnings, user_min_right is read, upload, write, admin, super_admin only, so this section may be wrong/outdated and should be aligned to the actual enum values. Based on learnings, avoid introducing “owner” semantics into app-level checks as well.

RBAC_SYSTEM.md-189-259 (1)

189-259: Doc correctness: conflicting schemas/types + scope examples don’t line up.

  • role_bindings is defined once with scope_type, channel_id uuid REFERENCES channels(rbac_id), audit fields, etc. (Line 193-216), but later you show a simplified CREATE TABLE role_bindings (...) that conflicts (e.g., channel_id bigint REFERENCES channels(id) and missing scope/audit columns) (Line 1239-1247). Pick one canonical definition (ideally the real migration schema) and delete/annotate the simplified one.
  • Bundle permissions are described as “scope: app” (Line 587-596) while later “Bundle permissions (scope: 'bundle')” (Line 1182-1186) — make this consistent with the permissions.scope_type CHECK (Line 158-161) and actual implementation.
  • Best-practices snippet uses { appId, bundleId } (Line 1906-1907) but the documented PermissionScope only has orgId/appId/channelId (Line 970-974). Update the example or the scope type.

Also applies to: 587-596, 1208-1247, 1901-1907

supabase/functions/_backend/private/devices.ts-1-7 (1)

1-7: Use createHono per backend guidelines (instead of new Hono)
This repo’s backend guidelines require createHono from utils/hono.ts for app initialization.

Proposed change
 import type { MiddlewareKeyVariables } from '../utils/hono.ts'
 import type { Order } from '../utils/types.ts'
-import { Hono } from 'hono/tiny'
-import { parseBody, simpleError, useCors } from '../utils/hono.ts'
+import { createHono, parseBody, simpleError, useCors } from '../utils/hono.ts'
 import { middlewareV2 } from '../utils/hono_middleware.ts'
 import { cloudlog } from '../utils/logging.ts'
 import { checkPermission } from '../utils/rbac.ts'
 import { countDevices, readDevices } from '../utils/stats.ts'

@@
-export const app = new Hono<MiddlewareKeyVariables>()
+export const app = createHono<MiddlewareKeyVariables>()

Also applies to: 25-25

src/components/tables/HistoryTable.vue-47-51 (1)

47-51: Likely incorrect scope: pass channelId for channel.rollback_bundle
Using only { appId } for a channel-scoped permission risks over-authorizing if your RBAC differentiates per-channel access.

Proposed change
 const canRollbackBundle = computedAsync(async () => {
   if (!props.appId)
     return false
-  return await hasPermission('channel.rollback_bundle', { appId: props.appId })
+  return await hasPermission('channel.rollback_bundle', {
+    appId: props.appId,
+    channelId: props.channelId,
+  })
 }, false)
supabase/functions/_backend/files/files.ts-380-381 (1)

380-381: Use 'app.upload_bundle' instead of 'app.read_bundles' for upload access.

The checkWriteAppAccess function guards upload endpoints (POST, PATCH at lines 461, 466) and is named for write access validation, but it checks the 'app.read_bundles' permission (line 381). The RBAC system defines 'app.upload_bundle' (upload level) as the correct permission for uploads. Other upload endpoints like upload_link.ts correctly use 'app.upload_bundle'. Using the read permission allows users with only read access to perform uploads, bypassing intended access controls.

Change line 381 from:

const hasPermission = await checkPermissionPg(c, 'app.read_bundles', { appId: app_id }, drizzleClient, userId, capgkey)

to:

const hasPermission = await checkPermissionPg(c, 'app.upload_bundle', { appId: app_id }, drizzleClient, userId, capgkey)
src/pages/app/[package].access.vue-21-33 (1)

21-33: Avoid stale “app found” state: clear app on refresh and handle Supabase errors explicitly.

app.value = dataApp || app.value keeps old data when the next app doesn’t exist (or query fails), so the UI can incorrectly render AccessTable for the previous app. Also .single() errors are ignored.

Proposed fix
 async function loadAppInfo() {
   try {
-    const { data: dataApp } = await supabase
+    const { data: dataApp, error } = await supabase
       .from('apps')
       .select()
       .eq('app_id', id.value)
       .single()
-    app.value = dataApp || app.value
+    if (error) {
+      console.error('Error loading app info:', error)
+      app.value = undefined
+      return
+    }
+    app.value = dataApp ?? undefined
   }
   catch (error) {
     console.error('Error loading app info:', error)
+    app.value = undefined
   }
 }
 
 async function refreshData() {
   isLoading.value = true
   try {
+    app.value = undefined
     await loadAppInfo()
   }
   catch (error) {
     console.error('Error refreshing data:', error)
   }
   finally {
     isLoading.value = false
   }
 }

Also applies to: 35-46

supabase/tests/35_test_is_admin_rbac.sql-22-65 (1)

22-65: Make the test idempotent: protect role_bindings insert against duplicates and assert prerequisites.

If public.role_bindings has a uniqueness constraint for the binding, this can hard-fail the test run (not just an ok(false)).

Possible adjustment
 -- 5) Grant platform_super_admin role to test_admin
+SELECT ok(
+    EXISTS (SELECT 1 FROM public.roles WHERE name = 'platform_super_admin'),
+    'platform_super_admin role exists'
+);
+
 INSERT INTO public.role_bindings (
@@
 FROM public.roles r
 WHERE r.name = 'platform_super_admin';
+-- If a unique constraint exists for this binding, keep the test idempotent:
+-- ON CONFLICT DO NOTHING;
src/pages/app/[package].channel.[channel].vue-47-57 (1)

47-57: Use channelId instead of appId for channel permission checks—the backend auto-derives appId and orgId.

Lines 50 and 56 should pass { channelId: id.value } not { appId: packageId.value }. The permission service documentation explicitly shows channel-scoped permissions are checked via channelId, and the backend automatically derives parent scopes.

Affected locations
  • Lines 47–57: canUpdateChannelSettings and canPromoteBundle computed definitions
  • Lines 138–143, 263–269, 292–296: Functions using these computed values
supabase/tests/34_test_rbac_rls.sql-23-36 (1)

23-36: Replace tautological assertions for roles/permissions reads.

Line 26 and Line 33 (COUNT(*) >= 0) will always pass even if RLS blocks everything (returns 0). Prefer asserting existence of known rows (seeded), or create a row as admin and assert the regular user can see that exact row.

Proposed fix (example)
 -- 3) Regular user can read roles
 SELECT tests.authenticate_as('test_user');
 SELECT ok(
-    (SELECT COUNT(*) FROM public.roles) >= 0,
+    EXISTS (SELECT 1 FROM public.roles),
     'Regular user can read roles'
 );

 -- 4) Regular user can read permissions
 SELECT tests.authenticate_as('test_user');
 SELECT ok(
-    (SELECT COUNT(*) FROM public.permissions) >= 0,
+    EXISTS (SELECT 1 FROM public.permissions),
     'Regular user can read permissions'
 );
supabase/functions/_backend/private/role_bindings.ts-11-12 (1)

11-12: Middleware registration may not cover all routes.

Same issue as in groups.ts - middleware registered on '/' may not apply to sub-routes like /:org_id or /:binding_id.

Proposed fix
-app.use('/', useCors)
-app.use('/', middlewareAuth)
+app.use('*', useCors)
+app.use('*', middlewareAuth)
supabase/functions/_backend/private/role_bindings.ts-99-101 (1)

99-101: Missing validation for apikey principal type.

The code accepts 'apikey' as a valid principal_type but there's no validation logic to verify the API key exists or belongs to the organization, unlike the validation for 'user' (lines 150-165) and 'group' (lines 168-183).

Proposed fix

Add validation for apikey principal type:

// Si principal_type est apikey, vérifier qu'il appartient à l'org
if (principal_type === 'apikey') {
  const [apikey] = await drizzle
    .select()
    .from(schema.apikeys)
    .where(
      and(
        eq(schema.apikeys.id, principal_id),
        eq(schema.apikeys.owner_org, org_id),
      ),
    )
    .limit(1)

  if (!apikey) {
    return c.json({ error: 'API key not found in this org' }, 400)
  }
}
supabase/functions/_backend/private/role_bindings.ts-122-124 (1)

122-124: String comparison for permission levels is unreliable.

Same issue as in groups.ts - using < 'admin' for string comparison doesn't reliably determine privilege levels.

Proposed fix
-    if (!orgAccess.length || orgAccess[0].user_right < 'admin') {
+    const adminRoles = ['admin', 'super_admin']
+    if (!orgAccess.length || !adminRoles.includes(orgAccess[0].user_right)) {
       return c.json({ error: 'Forbidden - Admin rights required' }, 403)
     }

Consider creating a shared helper function for permission checks to ensure consistency across all endpoints.

supabase/functions/_backend/private/groups.ts-94-96 (1)

94-96: String comparison for permission levels may not work correctly.

The comparison orgAccess[0].user_right < 'admin' uses lexicographic string comparison. If user_right is an enum like ['read', 'upload', 'write', 'admin', 'super_admin'], this comparison won't correctly determine privilege levels. Consider comparing against an explicit list or using a helper function.

Proposed fix
-    if (!orgAccess.length || orgAccess[0].user_right < 'admin') {
+    const adminRoles = ['admin', 'super_admin']
+    if (!orgAccess.length || !adminRoles.includes(orgAccess[0].user_right)) {
       return c.json({ error: 'Forbidden - Admin rights required' }, 403)
     }
src/services/permissions.ts-209-221 (1)

209-221: checkPermissionsBatch keying by permission will corrupt results across scopes.
Example: two checks for 'app.read' on different apps will collide and one overwrites the other. Use a composite key (permission + scope) or return an array preserving order.

One possible fix (stable composite key)
 export async function checkPermissionsBatch(
   checks: Array<{ permission: Permission, scope: PermissionScope }>,
 ): Promise<Record<string, boolean>> {
   const results = await Promise.all(
     checks.map(async ({ permission, scope }) => ({
-      key: permission,
+      key: `${permission}:${JSON.stringify(scope)}`,
       allowed: await hasPermission(permission, scope),
     })),
   )

   return Object.fromEntries(
     results.map(({ key, allowed }) => [key, allowed]),
   )
 }
supabase/functions/_backend/utils/rbac.ts-170-207 (1)

170-207: isRbacEnabledForOrg() cache is incorrect across multiple orgIds in the same request.
It caches a single boolean in c.set('rbacEnabled', enabled) without tying it to orgId, so a second call with a different orgId may reuse the wrong value.

One possible fix (cache orgId + value)
 export async function isRbacEnabledForOrg(
   c: Context<MiddlewareKeyVariables>,
   orgId: string | null,
 ): Promise<boolean> {
-  const cached = c.get('rbacEnabled')
-  if (cached !== undefined) {
-    return cached
-  }
+  const cached = c.get('rbacEnabled')
+  const cachedOrgId = c.get('resolvedOrgId')
+  if (cached !== undefined && cachedOrgId === orgId) {
+    return cached
+  }

   if (!orgId) {
     return false
   }

   let pgClient
   try {
     pgClient = getPgClient(c)
     const drizzleClient = getDrizzleClient(pgClient)

     const result = await drizzleClient.execute(
       sql`SELECT public.rbac_is_enabled_for_org(${orgId}::uuid) as enabled`,
     )

     const enabled = (result.rows[0] as any)?.enabled === true
-    c.set('rbacEnabled', enabled)
+    c.set('rbacEnabled', enabled)
+    c.set('resolvedOrgId', orgId)
     return enabled
   }
src/pages/settings/organization/Members.vue-238-271 (1)

238-271: RBAC member deletion is broken: aid: 0 mapping prevents all RBAC members from being deleted.

RBAC members are mapped with aid: 0 (line 252), but deleteMember() explicitly blocks deletion when aid === 0 with the "cannot-delete-owner" error message. This makes every RBAC member undeletable. Additionally, _deleteMember() attempts to delete from org_users table by member.aid, which would also fail for RBAC members since aid is not populated from the RBAC data model.

src/auto-imports.d.ts-11-15 (1)

11-15: Remove non-existent exports from auto-imports declarations.

The following exports are declared in auto-imports.d.ts but do not exist in src/stores/organization.ts:

  • LEGACY_TO_RBAC_ROLE_MAPPING (lines 11, 353)
  • RBAC_ORG_ROLE_DISPLAY_NAMES (lines 12, 353)
  • getRbacRoleDisplayName (line 46)

Only RBAC_TO_LEGACY_ROLE_MAPPING, RBAC_ORG_ROLE_I18N_KEYS, RBAC_ROLE_HIERARCHY, and getRbacRoleI18nKey are actually exported from organization.ts. Remove the non-existent entries from auto-imports.d.ts or add them to organization.ts if they are intended.

🟡 Minor comments (15)
messages/es.json-1454-1463 (1)

1454-1463: Spanish consistency: consider matching existing “Súper administrador” capitalization/diacritics.
role-org-super-admin is “Super Administrador”; elsewhere the UI uses “Súper administrador”.

Proposed tweak
-  "role-org-super-admin": "Super Administrador",
+  "role-org-super-admin": "Súper administrador",
messages/en.json-1482-1487 (1)

1482-1487: Remove the unused capitalized "Access" key at line 1516; use the existing lowercase "access" key instead.

The lowercase "access" key (line 1482) is actively used in the codebase—it's referenced in src/constants/appTabs.ts as a tab label and translated via t(tab.label) in src/components/Tabs.vue. The capitalized "Access" key at line 1516 has no references in the codebase and should be removed to avoid confusion and accidental misuse.

messages/ko.json-1454-1463 (1)

1454-1463: Korean “role-app-reader” translation is likely misleading (“리더” reads as “leader”).

Consider changing "role-app-reader": "앱 리더" to something unambiguous like “앱 뷰어”, “읽기 전용”, or “읽기 권한”.
Also consider "zip-bundle": "지프 앱 번들" → “ZIP 앱 번들” for consistency with “ZIP” as a term.

messages/tr.json-1454-1463 (1)

1454-1463: Consider a more natural Turkish label for “role-app-reader”.

“Uygulama Okuyucu” is understandable, but “Uygulama Görüntüleyici” (or similar) may read more naturally for “read-only/viewer”.

supabase/functions/_backend/private/devices.ts-29-35 (1)

29-35: Add body.appId validation before calling checkPermission

Missing appId currently results in a 403 authorization error instead of a 400-style input validation error. Following the established pattern in the codebase (e.g., stripe_checkout.ts), validate the required field before permission checks to provide clearer error semantics.

Proposed change
 app.post('/', middlewareV2(['read', 'write', 'all', 'upload']), async (c) => {
   const body = await parseBody<DataDevice>(c)
   cloudlog({ requestId: c.get('requestId'), message: 'post devices body', body })
+  if (!body.appId) {
+    throw simpleError('missing_app_id', 'Missing appId', { body })
+  }
   if (!(await checkPermission(c, 'app.read_devices', { appId: body.appId }))) {
     throw simpleError('app_access_denied', 'You can\'t access this app', { app_id: body.appId })
   }
supabase/functions/_backend/private/stripe_portal.ts-44-45 (1)

44-45: Typo in error code and message: not_authorize should be not_authorized.

Same issue as in stripe_checkout.ts - the error code and message are inconsistent with the rest of the file (lines 23, 31, 40 use not_authorized/Not authorized).

Proposed fix
   if (!await checkPermission(c, 'org.update_billing', { orgId: body.orgId }))
-    throw simpleError('not_authorize', 'Not authorize')
+    throw simpleError('not_authorized', 'Not authorized')
supabase/functions/_backend/private/stripe_checkout.ts-54-55 (1)

54-55: Typo in error code and message: not_authorize should be not_authorized.

The error code and message are inconsistent with the rest of the file (lines 33, 41, 50 all use not_authorized/Not authorized). This could cause issues for clients parsing error codes.

Proposed fix
   if (!await checkPermission(c, 'org.update_billing', { orgId: body.orgId }))
-    throw simpleError('not_authorize', 'Not authorize')
+    throw simpleError('not_authorized', 'Not authorized')
src/pages/settings/organization/Plans.vue-176-177 (1)

176-177: Potential race condition between organization change and permission check.

When currentOrganization changes, canUpdateBilling will re-compute asynchronously. At line 177, canUpdateBilling.value may still reflect the previous organization's permission result or the default false value, potentially triggering the "cannot-view-plans" dialog incorrectly.

Consider awaiting the permission check or using watchEffect with the permission result as a dependency:

Potential fix using explicit await
 watch(currentOrganization, async (newOrg, prevOrg) => {
-  if (newOrg && !canUpdateBilling.value) {
+  if (newOrg) {
+    const hasBillingPermission = await hasPermission('org.update_billing', { orgId: newOrg.gid })
+    if (!hasBillingPermission) {
     if (!initialLoad.value) {
       // ... rest of the logic
     }
+    }
+  }
src/pages/app/[package].access.vue-1-17 (1)

1-17: Use useRouter() instead of $router in the template (per repo guidance).

Proposed fix
 <script setup lang="ts">
@@
-import { useRoute } from 'vue-router'
+import { useRoute, useRouter } from 'vue-router'
@@
 const route = useRoute('/app/[package].access')
+const router = useRouter()
@@
 </script>
@@
-      <button class="mt-4 text-white d-btn d-btn-primary" @click="$router.push(`/app`)">
+      <button class="mt-4 text-white d-btn d-btn-primary" @click="router.push('/app')">
         {{ t('back-to-apps') }}
       </button>

Also applies to: 76-78

supabase/functions/_backend/public/app/post.ts-27-30 (1)

27-30: Remove the conditional body.owner_org && from the permission check.

The permission 'org.update_settings' is the correct choice—no dedicated 'org.create_app' permission exists in the RBAC system. However, since owner_org is a required field in the CreateApp interface (line 12), the conditional check bypasses permission validation if owner_org is falsy, creating a security gap.

Align with the pattern used in organization/put.ts and organization/delete.ts, which perform the permission check unconditionally:

if (!(await checkPermission(c, 'org.update_settings', { orgId: body.owner_org }))) {
  throw quickError(403, 'cannot_access_organization', 'You can\'t access this organization', { org_id: body.owner_org })
}

Alternatively, add explicit validation for owner_org before the permission check if optional handling is intentional.

tests/rbac-permissions.test.ts-11-14 (1)

11-14: Drop unused constant or use it.

TEST_RBAC_ORG_ID (Line 13) isn’t referenced.

supabase/functions/_backend/public/organization/members/delete.ts-10-13 (1)

10-13: Add UUID validation for orgId

orgId should be validated as a UUID on line 11, not just a string. For a permission-critical endpoint that manages organization membership, UUID validation is essential:

 const deleteBodySchema = z.object({
-  orgId: z.string(),
+  orgId: z.string().uuid(),
   email: z.email(),
 })

The z.email() validation is correct and fully supported in zod/mini.

supabase/functions/_backend/private/groups.ts-14-14 (1)

14-14: Fix typo flagged by pipeline.

Proposed fix
-// GET /private/groups/:org_id - Liste des groupes d'un org
+// GET /private/groups/:org_id - List groups for an org
src/components/tables/AccessTable.vue-162-162 (1)

162-162: Fix typo flagged by pipeline.

The pipeline failure indicates a spelling error in the French comment.

Proposed fix
-    // Mettre à jour le role_id existant
+    // Mettre à jour le role_id existent

Or consider using English comments for consistency:

-    // Mettre à jour le role_id existant
+    // Update the existing role_id
messages/fr.json-1454-1500 (1)

1454-1500: Translation map: consider removing the capitalized duplicate key and fix remaining English strings.
Having both "access" and "Access" is easy to misuse; also there are still values like "Open in new tab" / "Your API Key" in the FR file.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/constants/organizationTabs.ts (1)

8-10: Duplicate icon import: IconShield and IconSecurity reference the same icon.

Both IconSecurity (line 8) and IconShield (line 9) import from ~icons/heroicons/shield-check. Either reuse IconSecurity for the role-assignments tab, or choose a different icon if visual distinction is intended.

♻️ Proposed fix: Reuse existing IconSecurity
 import IconSecurity from '~icons/heroicons/shield-check'
-import IconShield from '~icons/heroicons/shield-check'
 import IconUserGroup from '~icons/heroicons/user-group'

Then update line 20:

-  { label: 'role-assignments', key: '/settings/organization/role-assignments', icon: IconShield },
+  { label: 'role-assignments', key: '/settings/organization/role-assignments', icon: IconSecurity },
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0dc077c and 75934f6.

📒 Files selected for processing (1)
  • src/constants/organizationTabs.ts
🧰 Additional context used
📓 Path-based instructions (6)
src/**/*.{ts,tsx,vue,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use ~/ alias for imports from src/ directory in frontend TypeScript and Vue components

Files:

  • src/constants/organizationTabs.ts
src/**/*.{vue,ts,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Frontend ESLint must pass before commit; run bun lint:fix to auto-fix issues in frontend files

Files:

  • src/constants/organizationTabs.ts
**/*.{vue,ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Run bun lint to lint Vue, TypeScript, and JavaScript files; use bun lint:fix to auto-fix issues

Files:

  • src/constants/organizationTabs.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use single quotes and no semicolons per @antfu/eslint-config rules

Files:

  • src/constants/organizationTabs.ts
src/**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Use path alias ~/ to map to src/ for imports in TypeScript and JavaScript files

Files:

  • src/constants/organizationTabs.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Run bun lint or lint/format command before validating any backend or frontend task to ensure consistent formatting

Files:

  • src/constants/organizationTabs.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: riderx
Repo: Cap-go/capgo PR: 1297
File: src/components/dashboard/DeploymentBanner.vue:77-79
Timestamp: 2025-12-23T01:19:15.067Z
Learning: In the Cap-go codebase, the 'owner' role exists only at the organization level (used in src/stores/organization.ts), not as an app-level permission. App-level permissions use the user_min_right database enum which includes: read, upload, write, admin, super_admin (but NOT owner). When checking app permissions, never include 'owner' in the role checks.
📚 Learning: 2025-10-30T14:58:37.007Z
Learnt from: Dalanir
Repo: Cap-go/capgo PR: 1238
File: src/layouts/settings.vue:7-13
Timestamp: 2025-10-30T14:58:37.007Z
Learning: In the Cap-go/capgo repository, heroicons are imported using the pattern `~icons/heroicons/icon-name` (e.g., `~icons/heroicons/user`, `~icons/heroicons/bell`) without requiring `/outline/` or `/solid/` path segments. Solid variants use a `-solid` suffix in the icon name itself (e.g., `~icons/heroicons/arrow-path-solid`).

Applied to files:

  • src/constants/organizationTabs.ts
🔇 Additional comments (1)
src/constants/organizationTabs.ts (1)

19-20: New RBAC tabs structure looks correct.

The groups and role-assignments tabs are appropriately placed and follow the existing tab definition pattern. The route paths are consistent with the organization settings structure.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
supabase/seed.sql (1)

474-485: Fix broken org_users seed INSERT (SQL syntax error).

Line 482 ends the VALUES list with );, but Lines 483-485 continue with additional tuples—this will fail parsing.

Proposed fix
 INSERT INTO "public"."org_users" ("org_id", "user_id", "user_right", "app_id", "channel_id") VALUES
@@
-    ('c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f', '8b2c3d4e-5f6a-4b7c-8d9e-0f1a2b3c4d5e', 'super_admin'::"public"."user_min_right", null, null),
-    ('f6a7b8c9-d0e1-4f2a-9b3c-4d5e6f7a8b92', 'e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a81', 'super_admin'::"public"."user_min_right", null, null);
-    ('046a36ac-e03c-4590-9257-bd6c9dba9ee8', 'c591b04e-cf29-4945-b9a0-776d0672061a', 'admin'::"public"."user_min_right", null, null),
-    ('34a8c55d-2d0f-4652-a43f-684c7a9403ac', '6aa76066-55ef-4238-ade6-0b32334a4097', 'write'::"public"."user_min_right", null, null);
-    ('a7b8c9d0-e1f2-4a3b-9c4d-5e6f7a8b9ca4', 'f6a7b8c9-d0e1-4f2a-9b3c-4d5e6f708193', 'super_admin'::"public"."user_min_right", null, null);
+    ('c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f', '8b2c3d4e-5f6a-4b7c-8d9e-0f1a2b3c4d5e', 'super_admin'::"public"."user_min_right", null, null),
+    ('f6a7b8c9-d0e1-4f2a-9b3c-4d5e6f7a8b92', 'e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a81', 'super_admin'::"public"."user_min_right", null, null),
+    ('046a36ac-e03c-4590-9257-bd6c9dba9ee8', 'c591b04e-cf29-4945-b9a0-776d0672061a', 'admin'::"public"."user_min_right", null, null),
+    ('34a8c55d-2d0f-4652-a43f-684c7a9403ac', '6aa76066-55ef-4238-ade6-0b32334a4097', 'write'::"public"."user_min_right", null, null),
+    ('a7b8c9d0-e1f2-4a3b-9c4d-5e6f7a8b9ca4', 'f6a7b8c9-d0e1-4f2a-9b3c-4d5e6f708193', 'super_admin'::"public"."user_min_right", null, null);
supabase/functions/_backend/utils/hono.ts (1)

111-136: JWT validation should return 401 Unauthorized, not 400 Bad Request.

Line 125: simpleError() always returns status 400, but authentication failures should return 401. Change to quickError(401, 'invalid_jwt', 'Invalid JWT').

Note: The jwt field in the auth context stores the raw Authorization header value (including the "Bearer " prefix), which is correct for the current usage in supabaseWithAuth() but should be documented to prevent downstream confusion if the auth object is used elsewhere.

Proposed fix
  if (authError || !authData.user) {
    cloudlog({ requestId: c.get('requestId'), message: 'Invalid JWT', error: authError })
-   return simpleError('invalid_jwt', 'Invalid JWT')
+   throw quickError(401, 'invalid_jwt', 'Invalid JWT')
  }
supabase/functions/_backend/public/bundle/set_channel.ts (1)

15-27: Bug: check channel-scoped permission with channelId, not just appId.

With { appId: body.app_id } (Line 25), a principal that only has a channel-level role binding can be denied incorrectly. Use { channelId: body.channel_id } (and optionally include appId too).

Proposed fix
-  if (!(await checkPermission(c, 'channel.promote_bundle', { appId: body.app_id }))) {
+  if (!(await checkPermission(c, 'channel.promote_bundle', { channelId: body.channel_id }))) {
     throw simpleError('cannot_access_app', 'You can\'t access this app', { app_id: body.app_id })
   }
src/components/dashboard/AppSetting.vue (1)

104-147: Fix incorrect org role value at line 826.

The code checks org.role === 'org_super_admin', but org.role contains legacy role values ('super_admin' or 'owner'), not RBAC names. Change to org.role === 'super_admin' || org.role === 'owner' to match the actual role values stored in the organization object.

This also applies to lines 301-306, 430-435, 637-642, and 825-830 if they have the same issue.

supabase/functions/_backend/private/delete_failed_version.ts (1)

20-29: Do not log capgkey (API key); remove or heavily redact.

Line 24 logs the full capgkey, which is a credential. This is a production security incident waiting to happen (logs are widely accessible and long-lived).

Proposed diff
   const apikey = c.get('apikey')
   const capgkey = c.get('capgkey') as string
   cloudlog({ requestId: c.get('requestId'), message: 'apikey', apikey })
-  cloudlog({ requestId: c.get('requestId'), message: 'capgkey', capgkey })
+  cloudlog({
+    requestId: c.get('requestId'),
+    message: 'capgkey',
+    capgkey: capgkey ? `${capgkey.slice(0, 6)}...` : 'missing',
+  })
-  const { data: userId, error: _errorUserId } = await supabaseApikey(c, capgkey)
-    .rpc('get_user_id', { apikey: capgkey, app_id: body.app_id })
-  if (_errorUserId) {
-    return quickError(404, 'user_not_found', 'Error User not found', { _errorUserId })
-  }
supabase/functions/_backend/files/files.ts (1)

373-403: Change app.read_bundles to app.upload_bundle in checkWriteAppAccess.

Line 381: checkWriteAppAccess is a middleware protecting file upload/write operations (POST, PATCH routes), yet it checks app.read_bundles (read permission). This is an authorization regression. The RBAC system clearly distinguishes app.read_bundles (read) from app.upload_bundle (upload), and similar upload endpoints (e.g., bundle/create.ts) correctly use app.upload_bundle. File write operations should require upload permission, not read permission.

src/layouts/settings.vue (1)

54-123: Billing tab can “stick” on native because removal only happens when !canReadBilling.

Current logic only removes /billing when !canReadBilling.value, not when Capacitor.isNativePlatform() is true. If the tab ever gets added in a non-native context (or base tabs change), it won’t be forcibly removed on native.

Proposed tweak
-  if (!Capacitor.isNativePlatform()
-    && canReadBilling.value
-    && !organizationTabs.value.find(tab => tab.key === '/billing')) {
+  if (!Capacitor.isNativePlatform()
+    && canReadBilling.value
+    && !organizationTabs.value.find(tab => tab.key === '/billing')) {
     organizationTabs.value.push({
       label: 'billing',
       icon: IconBilling,
       key: '/billing',
       onClick: () => openPortal(organizationStore.currentOrganization?.gid ?? '', t),
     })
   }
-  else if (!canReadBilling.value) {
+  else if (Capacitor.isNativePlatform() || !canReadBilling.value) {
     organizationTabs.value = organizationTabs.value.filter(tab => tab.key !== '/billing')
   }
🤖 Fix all issues with AI agents
In @src/components/dashboard/AppAccess.vue:
- Line 3: The file imports computed, onMounted, ref, watch from 'vue' but uses
the Vue type Ref (e.g., Ref<TableColumn[]>) on line 73, causing a TS error; fix
it by adding Ref to the import list from 'vue' (include Ref alongside
computed/onMounted/ref/watch) so the type Ref<TableColumn[]> is recognized where
used in AppAccess.vue.

In @src/pages/settings/organization/Members.vue:
- Around line 82-101: The function checkRbacEnabled is missing its closing
brace, causing a syntax error; close the function body by adding a single '}'
right after the existing catch block (before the declaration of
isInviteNewUserDialogOpen) so the try/catch and the async function are properly
terminated and ensure indentation matches surrounding code.

In @src/stores/organization.ts:
- Around line 287-372: The comment block around invalidateRoleBindingsCache and
hasPermissionsInRbac contains French text and typos causing CI spellcheck
failures; replace or rewrite those block comments in English and fix
misspellings (e.g., "Invalide" → "Invalidate", "FONCTION" → "FUNCTION",
"EXEMPLE"/"AVANTAGES"/"autorisation" → "EXAMPLE"/"ADVANTAGES"/"authorization"),
keeping the same intent and references to RBAC migration and the TODO for
hasPermission; update the JSDoc above hasPermissionsInRbac and the surrounding
explanatory comments to clean, consistent English phrasing while preserving
mentions of RBAC_ROLE_HIERARCHY, role_bindings, role_permissions, and the
migration steps so spellcheck passes.

In @supabase/functions/_backend/private/groups.ts:
- Around line 94-96: The permission check using orgAccess[0].user_right <
'admin' is invalid because it relies on lexicographic string comparison; replace
it with an explicit privilege check (e.g., verify user_right is one of
['admin','super_admin'] or map enum values to numeric ranks and compare ranks)
inside the same conditional that returns the 403; update the check near
orgAccess[0].user_right to either test inclusion in the allowed set or call a
new utility (e.g., hasRequiredRight(user_right, 'admin')) that encapsulates the
enum-to-rank logic.

In @supabase/functions/_backend/private/invite_new_user_to_org.ts:
- Around line 51-56: validateInvite currently returns { status: 403 } on RBAC
denial which the invite_new_user_to_org handler ignores and later throws a 400;
change validateInvite to throw a quickError(403, ...) (or otherwise throw an
error) immediately when checkPermission fails so the route handler won’t
continue to the simpleError path. Specifically, inside validateInvite replace
the return { status: 403 } branch with throw quickError(403, 'not authorized
(insufficient permissions)') (or equivalent) and apply the same change for any
other permission-denial branches referenced by the invite_new_user_to_org flow
so callers stop execution and the correct 403 is returned.

In @supabase/functions/_backend/private/role_bindings.ts:
- Around line 251-253: The condition uses a lexicographic string comparison
(orgAccess[0].user_right < 'admin') which is incorrect; replace it with an
explicit role check (e.g., ensure orgAccess exists and orgAccess[0].user_right
=== 'admin' or use a role-to-rank lookup and compare numeric ranks) so the
handler correctly enforces admin-only access before returning c.json({ error:
'Forbidden - Admin rights required' }, 403).
- Around line 122-124: The current check uses lexicographic string comparison on
orgAccess[0].user_right which is invalid for role hierarchy; replace it with an
explicit hierarchy check (e.g., define an ordered roles array like
['read','upload','write','admin','super_admin'] and compare indices, or simply
check membership in allowedRoles = ['admin','super_admin']) and use that to
decide if the user is permitted before returning c.json({ error: 'Forbidden -
Admin rights required' }, 403). Ensure you reference orgAccess[0].user_right for
the lookup and keep the same response when unauthorized.

In @supabase/functions/_backend/public/build/start.ts:
- Around line 5-7: The code uses a non-null assertion apikey.key! before calling
checkPermission which will throw if the DB row has key=null; change the logic in
the start handler to defensively handle nullable keys: read apikey without using
!, prefer apikey.key ?? apikey.hashed_key (or pass the whole apikey object to
checkPermission if it understands hashed keys), and ensure any failure to
resolve a usable key triggers markBuildAsFailed and returns the controlled error
response rather than letting an exception bubble; update references to
apikey.key! to these safe checks and add a guarded error path that calls
markBuildAsFailed when no valid key is available.
🟠 Major comments (13)
supabase/functions/_backend/utils/postgres_schema.ts-164-169 (1)

164-169: group_members needs a PK/unique constraint + FK to prevent duplicates/orphans.

Without a composite PK/unique index, the same (group_id, user_id) can be inserted multiple times, which can break effective-permissions calculations.

Possible patch
-import { bigint, boolean, integer, pgEnum, pgTable, serial, text, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'
+import { bigint, boolean, integer, pgEnum, pgTable, serial, text, timestamp, uuid, varchar, primaryKey } from 'drizzle-orm/pg-core'

-export const group_members = pgTable('group_members', {
-  group_id: uuid('group_id').notNull(),
+export const group_members = pgTable('group_members', {
+  group_id: uuid('group_id').notNull().references(() => groups.id, { onDelete: 'cascade' }),
   user_id: uuid('user_id').notNull(),
   added_by: uuid('added_by'),
   added_at: timestamp('added_at').notNull().defaultNow(),
-})
+}, (t) => ({
+  pk: primaryKey({ columns: [t.group_id, t.user_id] }),
+}))
supabase/functions/_backend/utils/postgres_schema.ts-154-162 (1)

154-162: Add FK + per-org uniqueness for groups.

groups.org_id should likely reference orgs.id, and name is typically unique within an org (otherwise UI/API gets ambiguous).

Possible patch
-import { bigint, boolean, integer, pgEnum, pgTable, serial, text, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'
+import { bigint, boolean, integer, pgEnum, pgTable, serial, text, timestamp, uuid, varchar, uniqueIndex } from 'drizzle-orm/pg-core'

-export const groups = pgTable('groups', {
+export const groups = pgTable('groups', {
   id: uuid('id').primaryKey().defaultRandom(),
-  org_id: uuid('org_id').notNull(),
+  org_id: uuid('org_id').notNull().references(() => orgs.id, { onDelete: 'cascade' }),
   name: text('name').notNull(),
   description: text('description'),
   is_system: boolean('is_system').notNull().default(false),
   created_by: uuid('created_by'),
   created_at: timestamp('created_at').notNull().defaultNow(),
-})
+}, (t) => ({
+  orgNameUq: uniqueIndex('groups_org_id_name_uq').on(t.org_id, t.name),
+}))
supabase/functions/_backend/utils/postgres_schema.ts-171-186 (1)

171-186: Add missing foreign key constraints and indexes to match the database schema in role_bindings.

The Drizzle schema for role_bindings (lines 171–186) is missing critical constraints and indexes that are defined in the migration (20251222140030_rbac_system.sql). The types themselves are correct—channel_id is uuid and correctly references channels(rbac_id) (also uuid), and app_id is intentionally uuid to reference apps.id rather than apps.name. However:

  • Missing foreign keys: role_id, org_id, app_id, channel_id, and bundle_id all lack .references() constraints that exist in the database migration.
  • Missing CHECK constraint: scope_type should validate against ('platform', 'org', 'app', 'bundle', 'channel') as enforced in the migration.
  • Missing indexes: The migration defines five unique scope-based indexes and two composite indexes for efficient lookups and SSD enforcement.

Sync the Drizzle schema with the authoritative migration schema to ensure ORM queries reflect actual constraints:

Update to add FKs and basic index hint
 export const role_bindings = pgTable('role_bindings', {
   id: uuid('id').primaryKey().defaultRandom(),
   principal_type: text('principal_type').notNull(),
   principal_id: uuid('principal_id').notNull(),
-  role_id: uuid('role_id').notNull(),
+  role_id: uuid('role_id').notNull().references(() => roles.id, { onDelete: 'cascade' }),
   scope_type: text('scope_type').notNull(),
-  org_id: uuid('org_id'),
-  app_id: uuid('app_id'),
+  org_id: uuid('org_id').references(() => orgs.id, { onDelete: 'cascade' }),
+  app_id: uuid('app_id').references(() => apps.id, { onDelete: 'cascade' }),
   bundle_id: bigint('bundle_id', { mode: 'number' }),
-  channel_id: uuid('channel_id'),
+  channel_id: uuid('channel_id').references(() => channels.rbac_id, { onDelete: 'cascade' }),
   granted_by: uuid('granted_by').notNull(),
   granted_at: timestamp('granted_at').notNull().defaultNow(),
   expires_at: timestamp('expires_at'),
   reason: text('reason'),
   is_direct: boolean('is_direct').notNull().default(true),
 })
supabase/functions/_backend/private/stripe_portal.ts-5-5 (1)

5-5: Fix auth error code/message + consider checking permission before querying org.

  • Line 45: not_authorize / Not authorize looks accidental and inconsistent with the rest of the file (and likely client expectations).
  • Line 44: You can run checkPermission before fetching org.customer_id to fail fast.
Proposed fix
-  if (!await checkPermission(c, 'org.update_billing', { orgId: body.orgId }))
-    throw simpleError('not_authorize', 'Not authorize')
+  if (!await checkPermission(c, 'org.update_billing', { orgId: body.orgId }))
+    throw simpleError('not_authorized', 'Not authorized')

Also applies to: 9-12, 44-45

RBAC_SYSTEM.md-631-915 (1)

631-915: Security note: don’t expose SECURITY DEFINER functions that accept arbitrary p_user_id.

The doc demonstrates calling rbac_check_permission_direct(p_user_id => 'user-uuid'::uuid, ...) while the function is SECURITY DEFINER. If this RPC is granted to authenticated, clients could probe permissions for other users unless the SQL enforces p_user_id = auth.uid() (or you only expose a no-arg wrapper that uses auth.uid()). This mirrors prior learnings about not granting inner functions with user_id params.

Also applies to: 797-945

tests/rbac-permissions.test.ts-26-177 (1)

26-177: Use dedicated TEST_RBAC_ORG_ID and a dedicated test user/app for RBAC tests instead of reusing ORG_ID.

Tests that enable/disable RBAC mode modify shared global state (rbac_settings WHERE id = 1 and per-org use_new_rbac flags). Since tests run in parallel and share ORG_ID with other test files, these mutations can interfere with other tests' setup. TEST_RBAC_ORG_ID is already defined in seed but unused. Create dedicated RBAC-specific test fixtures for org, user, and app or reuse TEST_RBAC_ORG_ID; ensure all state mutations are properly reverted in afterEach hooks (currently line 156 updates only the org flag, not the global setting).

src/stores/organization.ts-102-103 (1)

102-103: RBAC cache won’t be reactive with Ref<Map> mutations; UI can get stuck showing “no access”.

_roleBindingsCache is a ref(new Map()) (Line 102) and you mutate it via .set/.delete/.clear (Lines 278-279, 291-296, 508-509). Vue won’t track internal Map mutations through a Ref, so anything relying on this for rendering can fail to update after role bindings load.

One workable pattern (replace Map on writes)
-  const _roleBindingsCache: Ref<Map<string, RoleBinding[]>> = ref(new Map()) // Cache des role_bindings par orgId
+  const _roleBindingsCache: Ref<Map<string, RoleBinding[]>> = ref(new Map()) // Cache des role_bindings par orgId
+
+  const setRoleBindingsCache = (orgId: string, bindings: RoleBinding[]) => {
+    // Replace the Map instance to trigger reactivity
+    const next = new Map(_roleBindingsCache.value)
+    next.set(orgId, bindings)
+    _roleBindingsCache.value = next
+  }

...
-      _roleBindingsCache.value.set(orgId, userBindings)
+      setRoleBindingsCache(orgId, userBindings)

...
-      _roleBindingsCache.value.delete(orgId)
+      const next = new Map(_roleBindingsCache.value)
+      next.delete(orgId)
+      _roleBindingsCache.value = next
...
-      _roleBindingsCache.value.clear()
+      _roleBindingsCache.value = new Map()

...
-          _roleBindingsCache.value.set(org.gid, bindingsData)
+          setRoleBindingsCache(org.gid, bindingsData)

Also applies to: 251-285, 290-297, 299-332, 496-515

supabase/functions/_backend/utils/rbac.ts-170-207 (1)

170-207: rbacEnabled context cache should be per-org (or include orgId).

isRbacEnabledForOrg() caches a single boolean in c.get('rbacEnabled') (Lines 175-178, 195-196). If any request ever checks multiple orgIds, the first result will be reused incorrectly.

Safer cache shape
 export async function isRbacEnabledForOrg(
   c: Context<MiddlewareKeyVariables>,
   orgId: string | null,
 ): Promise<boolean> {
-  // Check cache first
-  const cached = c.get('rbacEnabled')
-  if (cached !== undefined) {
-    return cached
-  }
+  const cachedOrgId = c.get('resolvedOrgId')
+  const cachedEnabled = c.get('rbacEnabled')
+  if (cachedOrgId === orgId && cachedEnabled !== undefined)
+    return cachedEnabled

   if (!orgId) {
     return false
   }
...
-    c.set('rbacEnabled', enabled)
+    c.set('resolvedOrgId', orgId)
+    c.set('rbacEnabled', enabled)
     return enabled
supabase/functions/_backend/utils/rbac.ts-239-312 (1)

239-312: Enforce “non-empty scope” and use read-only PG client for SELECT-only checks.

checkPermission() accepts scope: PermissionScope but doesn’t enforce the “at least one must be provided” contract (Lines 257-258). Also it uses getPgClient(c) for a SELECT (Line 270) where getPgClient(c, true) seems more appropriate.

Proposed diff
 export async function checkPermission(
   c: Context<MiddlewareKeyVariables>,
   permission: Permission,
   scope: PermissionScope,
 ): Promise<boolean> {
...
   const { orgId = null, appId = null, channelId = null } = scope
+  if (orgId == null && appId == null && channelId == null) {
+    cloudlog({
+      requestId: c.get('requestId'),
+      message: 'checkPermission: missing scope',
+      permission,
+    })
+    return false
+  }

...
-    pgClient = getPgClient(c)
+    pgClient = getPgClient(c, true)
supabase/functions/_backend/public/build/status.ts-5-7 (1)

5-7: Avoid assuming apikey.key is present; hashed API keys likely break this path

checkPermission explicitly supports hashed keys via c.get('capgkey'), but supabaseApikey(c, apikey.key) (Line 39) still assumes a non-null key. If apikey.key is null for hashed keys, this can break build status reads (and subsequent DB updates) even though the RBAC check passes.

Proposed diff
+import type { MiddlewareKeyVariables } from '../../utils/hono.ts'
 import type { Context } from 'hono'
 import type { Database } from '../../utils/supabase.types.ts'
 import { simpleError } from '../../utils/hono.ts'
 import { cloudlog, cloudlogErr } from '../../utils/logging.ts'
 import { recordBuildTime, supabaseApikey } from '../../utils/supabase.ts'
 import { checkPermission } from '../../utils/rbac.ts'
 import { getEnv } from '../../utils/utils.ts'

 export async function getBuildStatus(
-  c: Context,
+  c: Context<MiddlewareKeyVariables>,
   params: BuildStatusParams,
   apikey: Database['public']['Tables']['apikeys']['Row'],
 ): Promise<Response> {
@@
-  const supabase = supabaseApikey(c, apikey.key)
+  const apikeyString = apikey.key ?? c.get('capgkey') ?? null
+  if (!apikeyString)
+    throw simpleError('unauthorized', 'Missing API key')
+  const supabase = supabaseApikey(c, apikeyString)

Also applies to: 33-40

supabase/tests/33_test_rbac_phase1.sql-1-7 (1)

1-7: Make CREATE EXTENSION idempotent to avoid flaky test runs.

CREATE EXTENSION "basejump-supabase_test_helpers"; will error if the extension is already installed.

Proposed fix
-CREATE EXTENSION "basejump-supabase_test_helpers";
+CREATE EXTENSION IF NOT EXISTS "basejump-supabase_test_helpers";
supabase/functions/_backend/public/organization/delete.ts-1-24 (1)

1-24: Create a dedicated org.delete permission instead of reusing org.update_settings for deletion.

The org.update_settings permission is semantically incorrect for organization deletion. It should only handle settings updates. Other resources follow the established pattern with dedicated delete permissions (app.delete, bundle.delete, channel.delete), but org deletion is missing this. Using an overly broad permission creates a privilege escalation risk: if org.update_settings is granted more widely in the future, deletion rights would inadvertently expand. Add org.delete to the permissions table and use it here instead.

supabase/functions/_backend/private/groups.ts-9-9 (1)

9-9: Use createHono from utils/hono.ts per coding guidelines.

Per the coding guidelines, Hono app initialization should use createHono from utils/hono.ts rather than instantiating directly.

♻️ Use createHono helper
-import { Hono } from 'hono/tiny'
-import { middlewareAuth, useCors } from '../utils/hono.ts'
+import { createHono, middlewareAuth, useCors } from '../utils/hono.ts'

-export const app = new Hono<MiddlewareKeyVariables>()
+export const app = createHono()

Based on learnings: "Use createHono from utils/hono.ts for all Hono framework application initialization and routing"

🟡 Minor comments (9)
messages/zh-cn.json-1454-1463 (1)

1454-1463: Translations look OK; consider terminology consistency for role names.

Keys/format look fine. Minor: double-check whether “计费经理 / 应用阅读者” matches your existing zh-CN terminology elsewhere (some products prefer “计费管理员 / 应用读者”).

messages/ja.json-1454-1463 (1)

1454-1463: Consider adjusting JP wording for “role-app-reader”.

Line 1457 uses アプリ読者, which can read a bit unnatural in UI. Consider 閲覧者 (common in admin UIs) or リーダー for consistency with other locales.

src/components/dashboard/DeploymentBanner.vue-28-34 (1)

28-34: Don’t require userRole for an RBAC-based permission gate (and update the doc comment)

hasAdminPermission now hinges on hasPermission('channel.promote_bundle', ...), but it still returns false when userRole.value is unset (Line 98-99). This can hide the banner for authorized users if role loading fails/returns null. Also the “admin/super_admin roles” comment is no longer accurate.

Proposed diff
 const hasAdminPermission = computedAsync(async () => {
-  if (!props.appId || !userRole.value)
+  if (!props.appId)
     return false
   return await hasPermission('channel.promote_bundle', { appId: props.appId })
 }, false)

Also applies to: 97-101

src/components/tables/ChannelTable.vue-54-65 (1)

54-65: Potential UX glitch: permission refs default to false and can block the auto “add channel” flow

Because canUpdateChannel starts as false, the count === 0 path that calls showAddModal() can race and show no-permission for users who actually have permission (until computedAsync resolves). Consider deferring that auto-open until the permission check is finished, or separating “unknown yet” from “false”.

Also applies to: 286-287, 302-306

messages/en.json-1504-1506 (1)

1504-1506: Confirm the confirmation text matches the actual scope being removed.

"remove-role-confirm": "Do you want to remove this user's access to the app?" reads app-specific, but it’s under generic role assignment management. If this confirm is also used for org/channel scope removals, the text will be misleading.

supabase/functions/_backend/private/groups.ts-14-14 (1)

14-14: Fix French comments to English.

The pipeline is failing due to French comments. Replace French text with English throughout the file.

🔧 Fix French comments
-// GET /private/groups/:org_id - Liste des groupes d'un org
+// GET /private/groups/:org_id - List groups for an org

Line 42:

-    // Récupérer les groupes
+    // Fetch groups

Line 63:

-// POST /private/groups/:org_id - Créer un groupe
+// POST /private/groups/:org_id - Create a group

Line 98:

-    // Créer le groupe
+    // Create the group

Line 118:

-// PUT /private/groups/:group_id - Modifier un groupe
+// PUT /private/groups/:group_id - Update a group

And similar changes for all other French comments (lines 26, 82, 133, 148, 164, 182, 194, 209, 225, 238, 250, 261, 277, 297, 316, 327, 343, 359, 378, 391, 402, 418).

src/components/tables/AccessTable.vue-254-266 (1)

254-266: Comment typo flagged by pipeline: "fonction" should be "function".

The pipeline flagged this French comment. Consider using English comments for consistency.

🔧 Suggested fix
-// Filtrer les éléments en fonction de la recherche
+// Filter elements based on search
supabase/functions/_backend/private/role_bindings.ts-75-78 (1)

75-78: Handle malformed JSON input gracefully.

c.req.json() throws if the request body is not valid JSON. Wrap it in a try-catch to return a 400 response for malformed input.

Suggested fix
 app.post('/', async (c: Context<MiddlewareKeyVariables>) => {
   const userId = c.get('auth')?.userId
-  const body = await c.req.json()
+  let body
+  try {
+    body = await c.req.json()
+  }
+  catch {
+    return c.json({ error: 'Invalid JSON body' }, 400)
+  }
supabase/functions/_backend/public/organization/put.ts-27-30 (1)

27-30: Inconsistent error response for permission failure.

Permission failures typically return 401 (Unauthorized) or 403 (Forbidden) status codes. This uses simpleError which returns 400. Compare with app/put.ts which uses quickError(401, ...) for the same type of check.

🐛 Suggested fix for consistency
   // Auth context is already set by middlewareKey
   if (!(await checkPermission(c, 'org.update_settings', { orgId: body.orgId }))) {
-    throw simpleError('cannot_access_organization', 'You can\'t access this organization', { orgId: body.orgId })
+    throw quickError(401, 'cannot_access_organization', 'You can\'t access this organization', { orgId: body.orgId })
   }
🧹 Nitpick comments (24)
supabase/functions/_backend/public/device/get.ts (1)

45-55: RBAC migration looks correct.

The permission check 'app.read_devices' with { appId: body.app_id } scope is appropriate for this endpoint. The defensive ordering (validation before authorization) is good practice.

As per coding guidelines, consider typing the context parameter more specifically for better type safety:

💡 Optional: Use typed Context
-export async function get(c: Context, body: GetDevice, apikey: Database['public']['Tables']['apikeys']['Row']): Promise<Response> {
+export async function get(c: Context<MiddlewareKeyVariables>, body: GetDevice, apikey: Database['public']['Tables']['apikeys']['Row']): Promise<Response> {

This would require importing MiddlewareKeyVariables from ../../utils/hono.ts.

supabase/functions/_backend/utils/postgres_schema.ts (1)

142-152: Consider constraining roles more tightly (enum + uniqueness).

scope_type: text + name: text with no uniqueness/indexing makes it easy to create ambiguous roles (same name in same scope). Also, if defaultRandom() is used, ensure the DB has gen_random_uuid() available. As per learnings, keeping schema here is good—please ensure migrations match it.

Proposed direction (sketch)
  • Add pgEnum for scope_type (and reuse it in role_bindings.scope_type)
  • Add a unique constraint like (scope_type, name)
  • Add an index on priority_rank if it’s used for ordering
  • Verify gen_random_uuid() support in your Supabase/Postgres setup
supabase/functions/_backend/private/download_link.ts (1)

7-7: Avoid double-auth and keep a single source of truth for user identity.

Now that middlewareAuth sets c.get('auth'), this handler can drift if supabase.auth.getUser() ever disagrees (or if you later optimize middleware). Consider deriving userId from c.get('auth') here (and only using Supabase client calls for data), so the RBAC decision + URL generation use the same identity.

Also applies to: 38-40

supabase/functions/_backend/utils/hono.ts (1)

40-42: New RBAC context variables: ensure they’re set (or remove).

rbacEnabled / resolvedOrgId are added to MiddlewareKeyVariables, but nothing here populates them—please ensure the intended middleware sets them, otherwise they’ll silently stay undefined.

tests/rbac-permissions.test.ts (1)

179-215: Consider it.concurrent() for the pure read-only mapping loop.

The permission→legacy mapping checks (Line 179+) are read-only and can safely be it.concurrent() to align with the repo’s parallel testing guidance. Based on learnings.

src/components/dashboard/AppSetting.vue (1)

42-53: Harden computed permission checks against thrown errors/timeouts.

If hasPermission(...) can throw (network/RPC), computedAsync will reject; consider a local try/catch and default to false.

Proposed fix
 const canUpdateSettings = computedAsync(async () => {
   if (!appRef.value)
     return false
-  return await hasPermission('app.update_settings', { appId: props.appId })
+  try {
+    return await hasPermission('app.update_settings', { appId: props.appId })
+  }
+  catch {
+    return false
+  }
 }, false)
 
 const canDeleteApp = computedAsync(async () => {
   if (!appRef.value)
     return false
-  return await hasPermission('app.delete', { appId: props.appId })
+  try {
+    return await hasPermission('app.delete', { appId: props.appId })
+  }
+  catch {
+    return false
+  }
 }, false)
src/stores/organization.ts (2)

30-75: Tighten types for i18n helper + avoid stringly-typed map key access.

getRbacRoleI18nKey(role: string) (Line 73) defeats the point of RbacRoleName and can mask typos at callsites. Prefer role: RbacRoleName (or role: string but with a type-guard).

Proposed diff
-export function getRbacRoleI18nKey(role: string): string | undefined {
+export function getRbacRoleI18nKey(role: RbacRoleName): string | undefined {
   return RBAC_ORG_ROLE_I18N_KEYS[role]
 }

299-405: hasPermissionsInRole() legacy path is brittle (no legacy hierarchy); consider “minimum right” semantics.

Legacy fallback currently does strict equality (Line 404) against mapped legacy roles, so callers must remember to always include higher roles in requiredRoles (e.g. pass both org_admin and org_super_admin). That’s easy to get wrong and will silently deny legitimate access.

If the legacy model is “minimum right”, consider a simple rank table (read < upload < write < admin < super_admin, plus owner at org scope) and check rank(currentRole) >= rank(requiredMinRole).

supabase/functions/_backend/utils/rbac.ts (1)

339-366: Avoid N DB connections in checkPermissions / checkAnyPermission.

Both helpers call checkPermission in a loop (Lines 344-347, 360-364), which opens/closes a PG client per permission. For endpoints checking multiple permissions, this is a big overhead. Prefer opening one client and reusing via checkPermissionPg.

supabase/functions/_backend/private/delete_failed_version.ts (1)

31-34: Permission check migration looks good; consider returning 403 (not 401) for “authenticated but forbidden”.

The RBAC check itself (Line 32) is the right direction. For consistency, the failure path currently returns 401 (Line 33) which usually means “not authenticated”, while this is “forbidden”.

supabase/functions/_backend/public/build/logs.ts (1)

5-5: Align Hono context typing with RBAC helper expectations

checkPermission is typed as Context<MiddlewareKeyVariables> (per supabase/functions/_backend/utils/rbac.ts), but this function accepts Context without variables typing, which can drift from the repo convention and reduce type-safety around c.get('auth')/c.get('requestId'). As per coding guidelines, consider updating the signature (and imports) to use Context<MiddlewareKeyVariables> here.

Proposed diff
-import type { Context } from 'hono'
+import type { Context } from 'hono'
+import type { MiddlewareKeyVariables } from '../../utils/hono.ts'

 export async function streamBuildLogs(
-  c: Context,
+  c: Context<MiddlewareKeyVariables>,
   jobId: string,
   appId: string,
   apikey: Database['public']['Tables']['apikeys']['Row'],
 ): Promise<Response> {

Also applies to: 74-84

messages/en.json (1)

1478-1524: Avoid generic + mixed-case i18n keys (collision risk / hard-to-debug overrides).

The newly added RBAC strings include very generic keys ("access", "assign", "remove", "user", "group", etc.) and also both "access" and "Access" (Line 1482 vs Line 1516). Even if JSON parsing “works”, these keys are prone to accidental reuse across features and subtle key mismatches in code.

Recommended: namespace these keys (e.g., rbac-access, rbac-assign, rbac-user, …) and pick one consistent casing.

src/layouts/app.vue (1)

10-25: Avoid (… as any)?.use_new_rbac; prefer typed store + stable filter key.

Two concerns:

  • (organizationStore.currentOrganization as any)?.use_new_rbac can silently break if the field name/type changes.
  • baseAppTabs.filter(t => t.label !== 'access') is brittle if labels change (or get localized); filtering on a stable route key is safer.
supabase/functions/_backend/public/channel/get.ts (1)

15-55: Consider updating helper function context types for consistency.

The internal helper functions getAll and getOne still use the plain Context type while the exported get function uses Context<MiddlewareKeyVariables>. While this works since these helpers are only called from get, updating them to Context<MiddlewareKeyVariables> would improve type consistency and enable better IDE support for context variables.

♻️ Optional consistency improvement
-async function getAll(c: Context, body: GetDevice, apikey: Database['public']['Tables']['apikeys']['Row']) {
+async function getAll(c: Context<MiddlewareKeyVariables>, body: GetDevice, apikey: Database['public']['Tables']['apikeys']['Row']) {
-async function getOne(c: Context, body: GetDevice, apikey: Database['public']['Tables']['apikeys']['Row']) {
+async function getOne(c: Context<MiddlewareKeyVariables>, body: GetDevice, apikey: Database['public']['Tables']['apikeys']['Row']) {
supabase/functions/_backend/public/channel/post.ts (1)

26-40: Optional: Update helper function context type.

Similar to get.ts, the findVersion helper uses plain Context while the exported function uses Context<MiddlewareKeyVariables>. Consider updating for consistency.

supabase/functions/_backend/public/bundle/create.ts (1)

24-132: Large commented-out code block.

This ~100 line block of commented code for URL verification could be removed or moved to a separate utility if the feature is not planned for near-term re-enablement. Keeping it adds maintenance burden.

src/components/dashboard/AppAccess.vue (2)

176-202: N+1 query pattern when enriching role bindings.

Each binding triggers a separate query to fetch user email or group name. For organizations with many bindings, this could cause performance issues. Consider batching the user/group lookups.

♻️ Suggested optimization
// Batch fetch all user emails
const userIds = appBindings
  .filter((b: RoleBinding) => b.principal_type === 'user')
  .map((b: RoleBinding) => b.principal_id)

const { data: usersData } = await supabase
  .from('users')
  .select('id, email')
  .in('id', userIds)

const userMap = new Map(usersData?.map(u => [u.id, u.email]) || [])

// Then map bindings using the lookup map

361-385: Duplicate initialization logic between watch and onMounted.

The same sequence of fetchAppDetails, checkRbacEnabled, and conditional fetches appears in both the watcher and onMounted. Extract to a shared function for DRY.

♻️ Suggested refactor
+async function initializeData() {
+  await fetchAppDetails()
+  await checkRbacEnabled()
+  if (useNewRbac.value) {
+    await Promise.all([
+      fetchAppRoleBindings(),
+      fetchAvailableAppRoles(),
+      fetchAvailableMembers(),
+      fetchAvailableGroups(),
+    ])
+  }
+}

-watch(() => props.appId, async () => {
-  await fetchAppDetails()
-  await checkRbacEnabled()
-  if (useNewRbac.value) {
-    await Promise.all([
-      fetchAppRoleBindings(),
-      fetchAvailableAppRoles(),
-      fetchAvailableMembers(),
-      fetchAvailableGroups(),
-    ])
-  }
-})
+watch(() => props.appId, initializeData)

-onMounted(async () => {
-  await fetchAppDetails()
-  await checkRbacEnabled()
-  if (useNewRbac.value) {
-    await Promise.all([
-      fetchAppRoleBindings(),
-      fetchAvailableAppRoles(),
-      fetchAvailableMembers(),
-      fetchAvailableGroups(),
-    ])
-  }
-})
+onMounted(initializeData)
supabase/functions/_backend/private/role_bindings.ts (2)

4-12: Use createHono from utils/hono.ts for app initialization.

Per coding guidelines, all Hono application initialization should use createHono from utils/hono.ts instead of directly instantiating new Hono(). This ensures consistent middleware setup across all backend endpoints.

Suggested fix
-import { Hono } from 'hono/tiny'
-import { middlewareAuth, useCors } from '../utils/hono.ts'
+import { createHono, middlewareAuth, useCors } from '../utils/hono.ts'
...
-export const app = new Hono<MiddlewareKeyVariables>()
-
-app.use('/', useCors)
-app.use('/', middlewareAuth)
+export const app = createHono()
+app.use('*', middlewareAuth)

Based on learnings, createHono from utils/hono.ts should be used for all Hono framework application initialization and routing.


68-71: Use structured logging with cloudlog instead of console.error.

Per coding guidelines, backend logging should use cloudlog({ requestId: c.get('requestId'), message: '...' }) for structured logging.

Suggested fix
+import { cloudlog } from '../utils/cloudflare.ts'
...
 catch (error) {
-    console.error('Error fetching role bindings:', error)
+    cloudlog({ requestId: c.get('requestId'), message: 'Error fetching role bindings', error })
     return c.json({ error: 'Internal server error' }, 500)
 }
src/services/permissions.ts (1)

169-222: Consider batch RPC optimization for future iterations.

These helper functions execute N parallel RPC calls. For typical use cases with 2-5 permissions, this is acceptable. If permission checks grow, consider adding a batch RPC endpoint (e.g., rbac_check_permissions_batch) to reduce round trips.

supabase/functions/_backend/utils/hono_middleware.ts (2)

65-93: Consider consolidating DB queries to reduce connection overhead.

The middleware opens two separate database connections: one for resolving app_id → orgId and another for checking rbac_is_enabled_for_org. These could be combined into a single connection/query when orgId is resolved from app_id.

♻️ Suggested consolidation
       if (appId) {
         let pgClient
         try {
           pgClient = getPgClient(c, true)
           const drizzleClient = getDrizzleClient(pgClient)
-          const appResult = await drizzleClient
-            .select({ ownerOrg: schema.apps.owner_org })
-            .from(schema.apps)
-            .where(eq(schema.apps.app_id, appId))
-            .limit(1)
-          if (appResult.length > 0 && appResult[0].ownerOrg) {
-            orgId = appResult[0].ownerOrg
+          // Resolve org and check RBAC in one query
+          const result = await drizzleClient.execute(
+            sql`SELECT a.owner_org, public.rbac_is_enabled_for_org(a.owner_org) as rbac_enabled
+                FROM public.apps a WHERE a.app_id = ${appId} LIMIT 1`,
+          )
+          if (result.rows.length > 0) {
+            orgId = (result.rows[0] as any).owner_org
+            c.set('resolvedOrgId', orgId)
+            c.set('rbacEnabled', (result.rows[0] as any).rbac_enabled === true)
+            // Skip the second DB call below
           }
         }
         // ... rest of error handling
       }
     }
-
-    // If we have an orgId, check if RBAC is enabled
-    if (orgId) {
+    // Only check RBAC separately if orgId was provided via resolver (not from app_id)
+    if (orgId && c.get('rbacEnabled') === undefined) {

424-432: Redundant null check on subkey.

The if (subkey) check on line 424 is redundant since the function returns early on lines 416-418 if subkey is null. The code works correctly, but the check adds unnecessary cognitive load.

♻️ Simplify by removing redundant check
       if (!subkey) {
         cloudlog({ requestId: c.get('requestId'), message: 'Invalid subkey', subkey_id })
         return quickError(401, 'invalid_subkey', 'Invalid subkey')
       }
       if (subkey?.limited_to_apps && subkey?.limited_to_apps.length === 0 && subkey?.limited_to_orgs && subkey?.limited_to_orgs.length === 0) {
         cloudlog({ requestId: c.get('requestId'), message: 'Invalid subkey, no limited apps or orgs', subkey })
         return quickError(401, 'invalid_subkey', 'Invalid subkey, no limited apps or orgs')
       }
-      if (subkey) {
-        c.set('subkey', subkey)
-        // Override auth context with subkey for RBAC
-        c.set('auth', {
-          userId: apikey!.user_id,
-          authType: 'apikey',
-          apikey: subkey,
-        } as AuthInfo)
-      }
+      c.set('subkey', subkey)
+      // Override auth context with subkey for RBAC
+      c.set('auth', {
+        userId: apikey!.user_id,
+        authType: 'apikey',
+        apikey: subkey,
+      } as AuthInfo)
     }
src/constants/organizationTabs.ts (1)

8-10: Duplicate icon import.

IconSecurity (line 8) and IconShield (line 9) both import from ~icons/heroicons/shield-check. Either reuse IconSecurity for the role-assignments tab, or rename one import to clarify the distinction.

♻️ Remove duplicate import
 import IconInfo from '~icons/heroicons/information-circle'
 import IconSecurity from '~icons/heroicons/shield-check'
-import IconShield from '~icons/heroicons/shield-check'
 import IconUserGroup from '~icons/heroicons/user-group'
 import IconUsers from '~icons/heroicons/users'
 
 export const organizationTabs: Tab[] = [
   { label: 'general', key: '/settings/organization', icon: IconInfo },
   { label: 'members', key: '/settings/organization/members', icon: IconUsers },
   // Security tab is added dynamically in settings.vue for super_admins only
   { label: 'security', key: '/settings/organization/security', icon: IconSecurity },
   { label: 'audit-logs', key: '/settings/organization/auditlogs', icon: IconAudit },
   { label: 'groups', key: '/settings/organization/groups', icon: IconUserGroup },
-  { label: 'role-assignments', key: '/settings/organization/role-assignments', icon: IconShield },
+  { label: 'role-assignments', key: '/settings/organization/role-assignments', icon: IconSecurity },

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@RBAC_SYSTEM.md`:
- Around line 613-622: The doc is inconsistent about bundle scoping: the "Bundle
Permissions (scope: `app`)" header conflicts with other references to a real
bundle scope (e.g., role names like bundle_admin,
role_bindings.scope_type='bundle', p_bundle_id and bundle-only propagation).
Pick one model and make the text consistent: if bundles are a distinct scope,
change the header and note to "scope: bundle", update examples/notes to
reference role_bindings.scope_type='bundle' and p_bundle_id propagation
behavior; if bundles are actually app-scoped, remove bundle-scoped terminology
(bundle_admin role, role_bindings.scope_type='bundle', p_bundle_id) and explain
bundle permissions as app-scoped with corresponding role names and propagation
rules. Ensure the permissions table entries (e.g., bundle.read, bundle.update,
bundle.delete) and the explanatory note match the chosen scope model.

In `@supabase/functions/_backend/private/groups.ts`:
- Around line 391-399: Declare targetUserId before you assign to it to avoid
creating an implicit global or ReferenceError; e.g., add a variable declaration
like "let targetUserId: string | undefined" before the try/assignment and then
set "targetUserId = parsedBody.data?.user_id" as shown, keeping the subsequent
checks that call isUuid(targetUserId) and c.json intact so targetUserId is also
available in any surrounding catch/logging blocks.
- Around line 31-82: The comments in this file (for example the comment above
the GET handler at app.get('/:org_id') that reads "Récupérer les groupes" and
other French comments around handlers and blocks) must be converted to English
to satisfy the linter; update all French comment text to equivalent English
phrases (e.g., "List groups of an org" / "Retrieve groups") across this file
including the other comment locations noted (lines near 84, 118, 148, 166, 219,
237, 290, 308, 371, 401, 431, 484, 499) while leaving code and identifiers
(app.get, checkPermission, getPgClient, getDrizzleClient, cloudlogErr,
closeClient, etc.) unchanged. Ensure comments remain brief and descriptive and
run the linter to confirm the pipeline error is resolved.

In `@supabase/seed.sql`:
- Around line 546-549: The seed inserts in app_versions reference app_id
'com.test2.app' but there is no corresponding row in the apps INSERT, causing a
FK violation; update the apps INSERT block to include a new apps row for
'com.test2.app' (matching the org_id '34a8c55d-2d0f-4652-a43f-684c7a9403ac' and
appropriate defaults used by other app rows) so the app_versions entries that
call out 'com.test2.app' can reference an existing apps.id; ensure the new apps
entry uses the same column ordering and value types as the other app rows in
that INSERT.
♻️ Duplicate comments (3)
messages/vi.json (1)

1462-1473: Role labels are well-translated; previous feedback incorporated.

The "role-app-reader" now correctly uses "Người xem ứng dụng" as suggested in a previous review. All role translations follow consistent patterns:

  • App roles: "Quản trị viên/Nhà phát triển/Người tải lên/Người xem ứng dụng"
  • Org roles: "Quản trị viên cấp cao/Quản trị viên/Quản lý thanh toán/Thành viên"
messages/hi.json (1)

1137-1137: Minor grammar refinement suggestion.

The translation is functional, but there's a small grammatical polish opportunity: removing the extra "को" before "देखने" would read more naturally.

Optional tweak
-  "plans-super-only": "केवल सुपर व्यवस्थापकों को ही योजनाओं और बिलिंग को देखने की अनुमति है",
+  "plans-super-only": "केवल सुपर व्यवस्थापकों को ही योजनाओं और बिलिंग देखने की अनुमति है",
RBAC_SYSTEM.md (1)

154-163: Update documentation SQL for rbac_has_permission, rbac_check_permission_direct, and rbac_check_permission to match actual migration signatures.

The documented function signatures in RBAC_SYSTEM.md (lines 750–765, 844–999) include a p_bundle_id parameter that does not exist in the migration (supabase/migrations/20251222140030_rbac_system.sql). Additionally, rbac_check_permission_direct parameters p_org_id and p_app_id are not optional in the migration, contradicting their documented DEFAULT NULL clauses. The permissions table CHECK constraint also shows string literals in the doc ('platform', 'org', ...) rather than the function calls used in the migration (public.rbac_scope_platform(), etc.).

🧹 Nitpick comments (3)
supabase/seed.sql (1)

551-558: Consider adding app_versions_meta entry for com.test2.app version 16.

The app_versions_meta table has entries for versions 3-7, 10, and 13, but the newly added version 16 (com.test2.app version 1.0.0) doesn't have a corresponding metadata entry. While builtin and unknown versions (14, 15) don't need metadata, the actual version (16) might benefit from having consistent test data.

♻️ Optional: Add metadata for version 16
     (10, NOW(), 'com.demoadmin.app', NOW(), 'admin123', 1500000),
-    (13, NOW(), 'com.stats.app', NOW(), 'stats123', 850000);
+    (13, NOW(), 'com.stats.app', NOW(), 'stats123', 850000),
+    (16, NOW(), 'com.test2.app', NOW(), 'test2123', 950000);
supabase/functions/_backend/public/organization/delete.ts (1)

61-95: Add explicit return type and include org_id in success log for consistency.

Two minor improvements:

  1. The function lacks an explicit return type annotation—adding : Promise<boolean> improves readability and consistency with deleteOrgImages.
  2. The success log at line 93 omits org_id, while all error logs include it. Adding it ensures consistent log structure for filtering/correlation.
Suggested changes
-async function deleteOrgAppImages(storage: StorageBucket, orgId: string, folderName: string, requestId?: string) {
+async function deleteOrgAppImages(storage: StorageBucket, orgId: string, folderName: string, requestId?: string): Promise<boolean> {
-  cloudlog({ requestId, message: 'deleted org app images', count: appFiles.length, folder: folderName })
+  cloudlog({ requestId, message: 'deleted org app images', org_id: orgId, count: appFiles.length, folder: folderName })
tests/rbac-permissions.test.ts (1)

118-131: Redundant flag resets inside transactional tests (and consider explicit NULL casts).
Because each test is wrapped in a transaction that’s rolled back, the extra “reset to legacy mode” updates are unnecessary noise. Also consider casting the last arg as NULL::text to avoid any future overload/ambiguity.

Proposed simplification
 describe('RBAC mode (use_new_rbac = true)', () => {
   beforeEach(async () => {
     // Enable RBAC globally for tests
     await query(`
       UPDATE public.rbac_settings SET use_new_rbac = true WHERE id = 1;
     `)
   })
 
-  afterEach(async () => {
-    // Reset to legacy mode
-    await query(`
-      UPDATE public.rbac_settings SET use_new_rbac = false WHERE id = 1;
-    `)
-  })
-
   it('should check permissions via RBAC system when enabled', async () => {
     // ...
   })
 })
-            NULL
+            NULL::text

Also applies to: 151-197, 236-273

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b35f9e0 and 3b5bbf3.

📒 Files selected for processing (9)
  • RBAC_SYSTEM.md
  • messages/de.json
  • messages/hi.json
  • messages/vi.json
  • supabase/functions/_backend/private/groups.ts
  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/migrations/20251222140030_rbac_system.sql
  • supabase/seed.sql
  • tests/rbac-permissions.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • messages/de.json
🧰 Additional context used
📓 Path-based instructions (10)
supabase/functions/_backend/**/*.{ts,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

supabase/functions/_backend/**/*.{ts,js}: Backend code must be placed in supabase/functions/_backend/ as shared code deployed to Cloudflare Workers (API/Plugin/Files workers), Supabase Edge Functions, and other platforms
Use createHono from utils/hono.ts for all Hono framework application initialization and routing
All database operations must use getPgClient() or getDrizzleClient() from utils/pg.ts for PostgreSQL access during active migration to Cloudflare D1
All Hono endpoint handlers must accept Context<MiddlewareKeyVariables> and use c.get('requestId'), c.get('apikey'), and c.get('auth') for request context
Use structured logging with cloudlog({ requestId: c.get('requestId'), message: '...' }) for all backend logging
Use middlewareAPISecret for internal API endpoints and middlewareKey for external API keys; validate against owner_org in the apikeys table
Check c.get('auth')?.authType to determine authentication type ('apikey' vs 'jwt') in backend endpoints
Use Drizzle ORM query patterns with schema from postgress_schema.ts for all database operations; use aliasV2() for self-joins or multiple table references

Files:

  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/private/groups.ts
supabase/functions/**/*.{ts,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Backend ESLint must pass before commit; run bun lint:backend for backend files

Files:

  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/private/groups.ts
**/*.{vue,ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Run bun lint to lint Vue, TypeScript, and JavaScript files; use bun lint:fix to auto-fix issues

Files:

  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/private/groups.ts
  • tests/rbac-permissions.test.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use single quotes and no semicolons per @antfu/eslint-config rules

Files:

  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/private/groups.ts
  • tests/rbac-permissions.test.ts
supabase/functions/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

supabase/functions/**/*.ts: Never use the Supabase admin SDK with service key for user-facing APIs; always use client SDK with user authentication to enforce RLS policies. Admin SDK should only be used for internal operations (triggers, CRON jobs, etc.)
When admin access is unavoidable for a user-facing endpoint, sanitize all user inputs carefully to prevent PostgREST query injection

Files:

  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/private/groups.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Run bun lint or lint/format command before validating any backend or frontend task to ensure consistent formatting

Files:

  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/private/groups.ts
  • tests/rbac-permissions.test.ts
supabase/functions/_backend/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Place core backend logic in supabase/functions/_backend/ with plugins, private, public, triggers, and utils subdirectories

Files:

  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/private/groups.ts
supabase/seed.sql

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Seed database with supabase db reset to apply all migrations and test data from supabase/seed.sql

Update supabase/seed.sql for new or evolved tests; keep fixtures focused on current behavior while leaving committed migrations unchanged

Files:

  • supabase/seed.sql
tests/**/*.{ts,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Backend tests must use helpers from tests/test-utils.ts including getEndpointUrl(path) for correct worker routing and USE_CLOUDFLARE_WORKERS=true for CF Workers testing

Files:

  • tests/rbac-permissions.test.ts
tests/**/*.test.ts

📄 CodeRabbit inference engine (AGENTS.md)

tests/**/*.test.ts: ALL TEST FILES RUN IN PARALLEL - design tests accordingly with proper isolation and use it.concurrent() to maximize parallelism within the same file
Create dedicated seed data for tests that modify shared resources instead of reusing existing data; use unique naming conventions prefixed with test file name or feature being tested
Only reuse existing seed data if you only READ the data without modifying it, or create your OWN child resources under it without modifying the parent
Backend tests located in tests/ directory use Vitest test runner and require running Supabase instance

Files:

  • tests/rbac-permissions.test.ts
🧠 Learnings (29)
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use `middlewareAPISecret` for internal API endpoints and `middlewareKey` for external API keys; validate against `owner_org` in the `apikeys` table

Applied to files:

  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/private/groups.ts
📚 Learning: 2025-12-24T14:11:10.256Z
Learnt from: WcaleNieWolny
Repo: Cap-go/capgo PR: 1300
File: supabase/migrations/20251224103713_2fa_enforcement.sql:409-539
Timestamp: 2025-12-24T14:11:10.256Z
Learning: In supabase/migrations for get_orgs_v6 and get_orgs_v7: The inner functions with user_id parameter (get_orgs_v6(uuid) and get_orgs_v7(uuid)) should NOT be granted to anon/authenticated roles as this allows any user to query other users' organizations; only the no-argument wrapper functions should be public as they perform authentication checks.

Applied to files:

  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/private/groups.ts
  • supabase/seed.sql
  • RBAC_SYSTEM.md
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use structured logging with `cloudlog({ requestId: c.get('requestId'), message: '...' })` for all backend logging

Applied to files:

  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/private/groups.ts
📚 Learning: 2025-12-23T01:19:15.067Z
Learnt from: riderx
Repo: Cap-go/capgo PR: 1297
File: src/components/dashboard/DeploymentBanner.vue:77-79
Timestamp: 2025-12-23T01:19:15.067Z
Learning: In the Cap-go codebase, the 'owner' role exists only at the organization level (used in src/stores/organization.ts), not as an app-level permission. App-level permissions use the user_min_right database enum which includes: read, upload, write, admin, super_admin (but NOT owner). When checking app permissions, never include 'owner' in the role checks.

Applied to files:

  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/private/groups.ts
  • RBAC_SYSTEM.md
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/migrations/*.sql : Use `get_identity_org_appid()` for RLS policies when app_id exists on the table instead of `get_identity()` directly

Applied to files:

  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/seed.sql
  • RBAC_SYSTEM.md
📚 Learning: 2025-12-25T11:22:19.594Z
Learnt from: WcaleNieWolny
Repo: Cap-go/capgo PR: 1300
File: supabase/migrations/20251224103713_2fa_enforcement.sql:85-96
Timestamp: 2025-12-25T11:22:19.594Z
Learning: In the 2FA enforcement implementation for supabase/migrations: When an org has enforcing_2fa=true, all users including super_admins must have 2FA enabled before accessing any org functions (including check_org_members_2fa_enabled); this is intentional behavior to ensure consistent security enforcement without exceptions for admins.

Applied to files:

  • supabase/functions/_backend/public/organization/delete.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : All Hono endpoint handlers must accept `Context<MiddlewareKeyVariables>` and use `c.get('requestId')`, `c.get('apikey')`, and `c.get('auth')` for request context

Applied to files:

  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/private/groups.ts
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/functions/_backend/**/*.ts : Place core backend logic in supabase/functions/_backend/ with plugins, private, public, triggers, and utils subdirectories

Applied to files:

  • supabase/functions/_backend/private/groups.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use `createHono` from `utils/hono.ts` for all Hono framework application initialization and routing

Applied to files:

  • supabase/functions/_backend/private/groups.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Check `c.get('auth')?.authType` to determine authentication type ('apikey' vs 'jwt') in backend endpoints

Applied to files:

  • supabase/functions/_backend/private/groups.ts
  • RBAC_SYSTEM.md
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Backend code must be placed in `supabase/functions/_backend/` as shared code deployed to Cloudflare Workers (API/Plugin/Files workers), Supabase Edge Functions, and other platforms

Applied to files:

  • supabase/functions/_backend/private/groups.ts
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/functions/**/*.ts : Never use the Supabase admin SDK with service key for user-facing APIs; always use client SDK with user authentication to enforce RLS policies. Admin SDK should only be used for internal operations (triggers, CRON jobs, etc.)

Applied to files:

  • supabase/functions/_backend/private/groups.ts
  • RBAC_SYSTEM.md
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : All database operations must use `getPgClient()` or `getDrizzleClient()` from `utils/pg.ts` for PostgreSQL access during active migration to Cloudflare D1

Applied to files:

  • supabase/functions/_backend/private/groups.ts
  • RBAC_SYSTEM.md
  • tests/rbac-permissions.test.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to cloudflare_workers/api/index.ts : API Worker (port 8787) routes: `/bundle`, `/app`, `/device`, `/channel`, `/private/*`, `/triggers`

Applied to files:

  • supabase/functions/_backend/private/groups.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/utils/postgress_schema.ts : Schema definitions must be placed in `utils/postgress_schema.ts` using Drizzle ORM and never edited in committed migration files

Applied to files:

  • supabase/functions/_backend/private/groups.ts
  • RBAC_SYSTEM.md
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use Drizzle ORM query patterns with `schema` from `postgress_schema.ts` for all database operations; use `aliasV2()` for self-joins or multiple table references

Applied to files:

  • supabase/functions/_backend/private/groups.ts
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/functions/**/*.ts : When admin access is unavoidable for a user-facing endpoint, sanitize all user inputs carefully to prevent PostgREST query injection

Applied to files:

  • supabase/functions/_backend/private/groups.ts
  • RBAC_SYSTEM.md
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/**/*.{ts,js} : Backend ESLint must pass before commit; run `bun lint:backend` for backend files

Applied to files:

  • supabase/functions/_backend/private/groups.ts
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/seed.sql : Update supabase/seed.sql for new or evolved tests; keep fixtures focused on current behavior while leaving committed migrations unchanged

Applied to files:

  • supabase/seed.sql
  • RBAC_SYSTEM.md
  • tests/rbac-permissions.test.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/seed.sql : Seed database with `supabase db reset` to apply all migrations and test data from `supabase/seed.sql`

Applied to files:

  • supabase/seed.sql
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/migrations/*.sql : Never edit previously committed migrations; treat new seeding in migrations as part of the current feature

Applied to files:

  • supabase/seed.sql
  • RBAC_SYSTEM.md
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/migrations/*.sql : Only use `get_identity_org_allowed()` as a last resort when the table genuinely has no app_id column and there is no way to join to get an app_id

Applied to files:

  • supabase/seed.sql
  • RBAC_SYSTEM.md
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Backend tests modify local database; always reset database with `supabase db reset` before running tests to ensure clean state

Applied to files:

  • supabase/seed.sql
  • tests/rbac-permissions.test.ts
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/migrations/*.sql : Use Supabase CLI for every migration and operational task; avoid manual changes through dashboard or direct SQL

Applied to files:

  • RBAC_SYSTEM.md
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to tests/**/*.test.ts : Create dedicated seed data for tests that modify shared resources instead of reusing existing data; use unique naming conventions prefixed with test file name or feature being tested

Applied to files:

  • tests/rbac-permissions.test.ts
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to tests/**/*.test.ts : ALL TEST FILES RUN IN PARALLEL - design tests accordingly with proper isolation and use `it.concurrent()` to maximize parallelism within the same file

Applied to files:

  • tests/rbac-permissions.test.ts
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to tests/**/*.test.ts : Backend tests located in tests/ directory use Vitest test runner and require running Supabase instance

Applied to files:

  • tests/rbac-permissions.test.ts
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to tests/**/*.test.ts : Only reuse existing seed data if you only READ the data without modifying it, or create your OWN child resources under it without modifying the parent

Applied to files:

  • tests/rbac-permissions.test.ts
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to playwright/e2e/**/*.spec.ts : Use Playwright MCP suite to cover customer-facing flows; add scenarios under playwright/e2e and run with `bun run test:front` before shipping UI changes

Applied to files:

  • tests/rbac-permissions.test.ts
🧬 Code graph analysis (3)
supabase/functions/_backend/public/organization/delete.ts (3)
supabase/functions/_backend/utils/hono.ts (3)
  • MiddlewareKeyVariables (27-44)
  • simpleError (268-270)
  • quickError (245-258)
supabase/functions/_backend/utils/logging.ts (1)
  • cloudlog (3-15)
supabase/functions/_backend/utils/rbac.ts (1)
  • checkPermission (241-314)
supabase/functions/_backend/private/groups.ts (5)
supabase/functions/_backend/utils/hono.ts (1)
  • MiddlewareKeyVariables (27-44)
supabase/functions/_backend/utils/version.ts (1)
  • version (1-1)
supabase/functions/_backend/utils/rbac.ts (1)
  • checkPermission (241-314)
supabase/functions/_backend/utils/pg.ts (3)
  • getPgClient (187-218)
  • getDrizzleClient (220-222)
  • closeClient (275-280)
supabase/functions/_backend/utils/postgres_schema.ts (2)
  • groups (169-177)
  • schema (210-225)
tests/rbac-permissions.test.ts (1)
tests/test-utils.ts (3)
  • POSTGRES_URL (7-7)
  • ORG_ID (43-43)
  • USER_ID (46-46)
🪛 GitHub Actions: Run tests
supabase/functions/_backend/public/organization/delete.ts

[error] 1-1: CI step failed with exit code 2

supabase/functions/_backend/private/groups.ts

[error] 31-31: groupes should be groups, grouped, or groupees


[error] 53-53: groupes should be groups, grouped, or groupees


[error] 84-84: groupe should be group or grouped


[error] 118-118: groupe should be group or grouped


[error] 148-148: groupe should be group or grouped


[error] 166-166: groupe should be group or grouped


[error] 219-219: groupe should be group or grouped


[error] 237-237: groupe should be group or grouped


[error] 290-290: groupe should be group or grouped


[error] 308-308: groupe should be group or grouped


[error] 371-371: groupe should be group or grouped


[error] 484-484: existant should be existent


[error] 1-1: CI step failed with exit code 2

supabase/seed.sql

[error] 1-1: CI step failed with exit code 2

messages/vi.json

[error] 1-1: CI step failed with exit code 2

messages/hi.json

[error] 1-1: CI step failed with exit code 2

RBAC_SYSTEM.md

[error] 1-1: CI step failed with exit code 2

tests/rbac-permissions.test.ts

[error] 1-1: CI step failed with exit code 2

🪛 GitHub Check: Run tests
supabase/functions/_backend/private/groups.ts

[warning] 308-308:
"groupe" should be "group" or "grouped".


[warning] 290-290:
"groupe" should be "group" or "grouped".


[warning] 237-237:
"groupe" should be "group" or "grouped".


[warning] 219-219:
"groupe" should be "group" or "grouped".


[warning] 166-166:
"groupe" should be "group" or "grouped".


[warning] 148-148:
"groupe" should be "group" or "grouped".


[warning] 118-118:
"groupe" should be "group" or "grouped".


[warning] 84-84:
"groupe" should be "group" or "grouped".


[warning] 53-53:
"groupes" should be "groups" or "grouped" or "groupees".


[warning] 31-31:
"groupes" should be "groups" or "grouped" or "groupees".

🪛 LanguageTool
RBAC_SYSTEM.md

[style] ~802-~802: This adverb was used twice in the sentence. Consider removing one of them or replacing them with a synonym.
Context: ...that bundle - Downward propagation only (no upward propagation) 7. *Return...

(ADVERB_REPETITION_PREMIUM)

🔇 Additional comments (21)
messages/vi.json (1)

1217-1224: RBAC UI strings look good; previous feedback addressed.

The translations are consistent with Vietnamese UI conventions. The "rbac-system-enabled" value has been shortened as previously suggested, and the phrasing for migration-related strings ("các vai trò kế thừa vẫn hiển thị trong quá trình di chuyển/chuyển đổi") is clear.

supabase/functions/_backend/private/groups.ts (6)

1-30: LGTM — Imports and initialization follow backend conventions.

The file correctly uses createHono from utils/hono.ts, imports DB utilities properly, and includes useful validation helpers (isUuid, parseJsonBody). Based on learnings, this follows the required patterns for supabase/functions/_backend/**/*.ts.


84-146: LGTM — Create group endpoint properly validates and creates non-system groups.

The endpoint correctly:

  • Validates auth and permissions before parsing body
  • Ensures is_system: false for user-created groups
  • Uses proper DB client lifecycle

148-217: LGTM — Update endpoint correctly protects system groups and handles partial updates.

Good pattern: fetches group first to resolve org_id for permission check, blocks system group modification, and handles partial updates by preserving existing values when fields are undefined.


256-270: LGTM — Atomic deletion with transaction properly cleans up role bindings.

The transaction ensures both role_bindings cleanup and groups deletion happen atomically, preventing data inconsistency if either operation fails. This addresses the previous concern about non-atomic deletes.


461-526: LGTM — Member removal endpoint follows proper patterns.

Clean implementation with proper auth, UUID validation, group lookup, and permission check before deletion.


324-333: schema.users is properly exported in postgres_schema.ts.

The users table is correctly exported as a named export at line 106. The query in groups.ts correctly uses this export in the innerJoin, and no changes are needed.

messages/hi.json (4)

1217-1224: LGTM! RBAC keys are well-translated and terminology is now consistent.

The past review concern about "सुपर एडमिन" vs "सुपर व्यवस्थापक" inconsistency has been properly addressed — Line 1223 now correctly uses "सुपर व्यवस्थापक" to match the established terminology throughout the file.


1256-1256: LGTM!

The grammar has been updated to include "के … ही" construction as previously suggested. The translation now reads naturally.


1462-1473: LGTM! Role-related keys are well-translated.

The new RBAC role translations maintain consistent terminology:

  • Uses "व्यवस्थापक" for admin roles throughout
  • Clear Hindi equivalents for all app-level and org-level roles
  • Confirmation prompt for role removal is appropriately phrased

1-3: JSON syntax is valid; pipeline failure is unrelated to this translation file.

The messages/hi.json file is syntactically correct with proper structure. The inlang I18n validation rules configured in project.inlang/settings.json are not executed in the CI pipeline (the linting step is disabled pending an npm issue fix), so the exit code 2 failure originates from another CI step such as typo checks, linting, or backend tests—not from this translation file.

supabase/seed.sql (3)

46-53: LGTM! RBAC table initialization is correctly ordered.

The truncation order respects foreign key constraints (role_bindings → group_members → groups), and the upsert pattern for rbac_settings ensures deterministic RBAC state across test runs.


484-485: LGTM! New org membership entries for RBAC testing.

These entries add cross-org membership scenarios (admin user with admin role in Demo org, test user with write role in Test2 org), which are useful for testing the new RBAC permission system.


1025-1042: Critical: Undeclared v_org variable causes syntax error.

In PL/pgSQL, "all variables used in a block must be declared in the declarations section of the block. (The only exceptions are that the loop variable of a FOR loop iterating over a range of integer values is automatically declared as an integer variable, and likewise the loop variable of a FOR loop iterating over a cursor's result is automatically declared as a record variable.)"

Line 1036 uses v_org as a record variable in a FOR loop over a query result, but v_org is not declared in the DECLARE block (lines 1025-1026). This will fail with an error like "loop variable of loop over rows must be a record or row variable".

This is the likely cause of the pipeline failure.

🐛 Proposed fix: Declare v_org as RECORD
 DO $$
 DECLARE
     v_migration_result jsonb;
+    v_org RECORD;
 BEGIN
     -- Execute seeding functions
     PERFORM public.reset_and_seed_data();
⛔ Skipped due to learnings
Learnt from: WcaleNieWolny
Repo: Cap-go/capgo PR: 1300
File: supabase/migrations/20251224103713_2fa_enforcement.sql:409-539
Timestamp: 2025-12-24T14:11:10.256Z
Learning: In supabase/migrations for get_orgs_v6 and get_orgs_v7: The inner functions with user_id parameter (get_orgs_v6(uuid) and get_orgs_v7(uuid)) should NOT be granted to anon/authenticated roles as this allows any user to query other users' organizations; only the no-argument wrapper functions should be public as they perform authentication checks.
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/seed.sql : Seed database with `supabase db reset` to apply all migrations and test data from `supabase/seed.sql`
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/seed.sql : Update supabase/seed.sql for new or evolved tests; keep fixtures focused on current behavior while leaving committed migrations unchanged
supabase/functions/_backend/public/organization/delete.ts (4)

1-14: LGTM on imports and type definitions.

The imports correctly use MiddlewareKeyVariables for typed context, checkPermission for RBAC authorization, and cloudlog for structured logging, aligning with coding guidelines.


15-59: Well-implemented storage cleanup with proper error propagation.

This implementation correctly addresses the past review concerns: storage API errors are now properly checked via { data, error } destructuring, and the function returns a boolean to propagate failure status to the caller. The per-entry error logging with requestId and org_id provides good observability.


97-125: LGTM on RBAC-integrated organization deletion.

The function correctly:

  • Uses checkPermission with the dedicated 'org.delete' permission (addressing the past review concern about using a semantic permission)
  • Throws a structured error when image cleanup fails (preventing orphaned images)
  • Uses supabaseApikey for the database deletion, respecting RLS policies per coding guidelines

97-97: No issues identified. The code is syntactically correct, follows established backend patterns used throughout the codebase, and properly implements the organization deletion logic with permission checks and image cleanup.

tests/rbac-permissions.test.ts (2)

14-45: Per-test DB transactions + cleanup look solid (parallel-safe).
Using a dedicated PoolClient with BEGIN in beforeEach and ROLLBACK+release() in afterEach is the right isolation strategy for tests/**/*.test.ts running in parallel.


11-13: This review comment is incorrect—TEST_APP_ID is already properly seeded.

The app com.demo.app is explicitly seeded in supabase/seed.sql (line 528) with the exact ORG_ID and USER_ID from test-utils.ts. The user also has the required super_admin role binding in that organization (seed.sql line 477). All tests in this file are read-only SELECT queries against this seeded data, wrapped in transactions for proper isolation. No additional app creation or fixture setup is needed.

Likely an incorrect or invalid review comment.

RBAC_SYSTEM.md (1)

825-1040: Clear client-safety guidance: wrapper RPC + GRANTs are well documented.
Calling out rbac_check_permission() as the client-safe wrapper (and keeping _direct service-role only) plus explicit REVOKE/GRANT examples is a strong guardrail against future regressions.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@AGENTS.md`:
- Line 196: Update the AGENTS.md line that references the Conventional Commits
URL by wrapping the bare URL in angle brackets; replace "Conventional Commits
v1.0.0 (https://www.conventionalcommits.org/en/v1.0.0/)" with text that keeps
the label "Conventional Commits v1.0.0" but surrounds the URL with angle
brackets so the markdown becomes "Conventional Commits v1.0.0
(<https://www.conventionalcommits.org/en/v1.0.0/>)" to satisfy markdownlint.

In `@src/components/tables/AccessTable.vue`:
- Around line 140-144: Move the capture of the selected role until after the
user has interacted with the dialog: don’t create roleSnapshot from
selectedRole.value before awaiting dialogStore.onDialogDismiss(); instead await
dialogStore.onDialogDismiss(), return undefined if dismissed, then read
selectedRole.value into roleSnapshot and return it (this ensures changeUserRole
uses the user-selected value).
♻️ Duplicate comments (2)
supabase/functions/_backend/private/groups.ts (1)

391-399: Critical: Missing variable declaration causes ReferenceError in strict mode.

Line 391 assigns to targetUserId without a declaration keyword (const/let). This will throw a ReferenceError in strict mode or create an implicit global otherwise. Since targetUserId is also referenced in the catch block (line 449) for logging, declare it with let before the try block.

🐛 Proposed fix
 app.post('/:group_id/members', async (c: Context<MiddlewareKeyVariables>) => {
   const groupId = c.req.param('group_id')
   const userId = c.get('auth')?.userId
+  let targetUserId: string | undefined

   if (!userId) {
     return c.json({ error: 'Unauthorized' }, 401)

Then at line 391:

-    targetUserId = parsedBody.data?.user_id
+    targetUserId = parsedBody.data?.user_id as string | undefined
src/components/tables/AccessTable.vue (1)

100-102: RPC response type cast still present.

The (data as any) cast on line 101 indicates the RPC return type isn't yet available in the generated types. After RBAC migrations are applied, run bun types to regenerate TypeScript types and replace this cast with proper typing. Based on learnings, this step is required after schema changes.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3b5bbf3 and ea44875.

📒 Files selected for processing (3)
  • AGENTS.md
  • src/components/tables/AccessTable.vue
  • supabase/functions/_backend/private/groups.ts
🧰 Additional context used
📓 Path-based instructions (14)
supabase/functions/_backend/**/*.{ts,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

supabase/functions/_backend/**/*.{ts,js}: Backend code must be placed in supabase/functions/_backend/ as shared code deployed to Cloudflare Workers (API/Plugin/Files workers), Supabase Edge Functions, and other platforms
Use createHono from utils/hono.ts for all Hono framework application initialization and routing
All database operations must use getPgClient() or getDrizzleClient() from utils/pg.ts for PostgreSQL access during active migration to Cloudflare D1
All Hono endpoint handlers must accept Context<MiddlewareKeyVariables> and use c.get('requestId'), c.get('apikey'), and c.get('auth') for request context
Use structured logging with cloudlog({ requestId: c.get('requestId'), message: '...' }) for all backend logging
Use middlewareAPISecret for internal API endpoints and middlewareKey for external API keys; validate against owner_org in the apikeys table
Check c.get('auth')?.authType to determine authentication type ('apikey' vs 'jwt') in backend endpoints
Use Drizzle ORM query patterns with schema from postgress_schema.ts for all database operations; use aliasV2() for self-joins or multiple table references

Files:

  • supabase/functions/_backend/private/groups.ts
supabase/functions/**/*.{ts,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Backend ESLint must pass before commit; run bun lint:backend for backend files

Files:

  • supabase/functions/_backend/private/groups.ts
**/*.{vue,ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Run bun lint to lint Vue, TypeScript, and JavaScript files; use bun lint:fix to auto-fix issues

Files:

  • supabase/functions/_backend/private/groups.ts
  • src/components/tables/AccessTable.vue
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use single quotes and no semicolons per @antfu/eslint-config rules

Files:

  • supabase/functions/_backend/private/groups.ts
supabase/functions/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

supabase/functions/**/*.ts: Never use the Supabase admin SDK with service key for user-facing APIs; always use client SDK with user authentication to enforce RLS policies. Admin SDK should only be used for internal operations (triggers, CRON jobs, etc.)
When admin access is unavoidable for a user-facing endpoint, sanitize all user inputs carefully to prevent PostgREST query injection

Files:

  • supabase/functions/_backend/private/groups.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Run bun lint or lint/format command before validating any backend or frontend task to ensure consistent formatting

Files:

  • supabase/functions/_backend/private/groups.ts
  • src/components/tables/AccessTable.vue
supabase/functions/_backend/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Place core backend logic in supabase/functions/_backend/ with plugins, private, public, triggers, and utils subdirectories

Files:

  • supabase/functions/_backend/private/groups.ts
src/**/*.vue

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.vue: Use Vue 3 <script setup> syntax exclusively for all Vue component scripts
Use Tailwind utility classes for layout and spacing in Vue components
Use DaisyUI components (d-btn, d-input, d-card) for interactive elements in Vue components
Use Konsta components ONLY for safe area helpers (top/bottom insets) in Vue components; avoid other uses
Use useRoute() from vue-router to access route parameters and useRouter() for programmatic navigation in Vue components

src/**/*.vue: Konsta components are reserved only for safe area helpers; avoid importing konsta anywhere else in the app
Use Pinia stores for state management in Vue components

Files:

  • src/components/tables/AccessTable.vue
src/**/*.{ts,tsx,vue,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use ~/ alias for imports from src/ directory in frontend TypeScript and Vue components

Files:

  • src/components/tables/AccessTable.vue
src/**/*.{vue,ts,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Frontend ESLint must pass before commit; run bun lint:fix to auto-fix issues in frontend files

Files:

  • src/components/tables/AccessTable.vue
**/*.vue

📄 CodeRabbit inference engine (AGENTS.md)

**/*.vue: Use Vue 3 with Composition API and <script setup> syntax for frontend components
Use TailwindCSS utility classes and DaisyUI (d- prefixed classes) for styling instead of bespoke CSS

Files:

  • src/components/tables/AccessTable.vue
src/**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Use path alias ~/ to map to src/ for imports in TypeScript and JavaScript files

Files:

  • src/components/tables/AccessTable.vue
src/**/*.{vue,css}

📄 CodeRabbit inference engine (AGENTS.md)

Mirror the palette from src/styles/style.css when introducing new UI; use deep slate bases with Extract azure highlight (#119eff) and soft radii

Files:

  • src/components/tables/AccessTable.vue
src/components/**/*.vue

📄 CodeRabbit inference engine (AGENTS.md)

Place reusable Vue components in src/components/ directory

Files:

  • src/components/tables/AccessTable.vue
🧠 Learnings (26)
📚 Learning: 2026-01-09T18:23:34.200Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-09T18:23:34.200Z
Learning: Read AGENTS.md for all development guidelines, commands, architecture details, and coding conventions for this repository

Applied to files:

  • AGENTS.md
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: ALL sections in a PR created by AI agents MUST be marked with '(AI generated)' - failure to mark is a violation of transparency requirements

Applied to files:

  • AGENTS.md
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/**/*.{ts,js} : Backend ESLint must pass before commit; run `bun lint:backend` for backend files

Applied to files:

  • AGENTS.md
  • supabase/functions/_backend/private/groups.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/migrations/**/*.sql : Database migrations must be created with `supabase migration new <feature_slug>` and never modify previously committed migrations

Applied to files:

  • AGENTS.md
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/migrations/*.sql : Use Supabase CLI for every migration and operational task; avoid manual changes through dashboard or direct SQL

Applied to files:

  • AGENTS.md
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to src/**/*.{vue,ts,js} : Frontend ESLint must pass before commit; run `bun lint:fix` to auto-fix issues in frontend files

Applied to files:

  • AGENTS.md
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Backend code must be placed in `supabase/functions/_backend/` as shared code deployed to Cloudflare Workers (API/Plugin/Files workers), Supabase Edge Functions, and other platforms

Applied to files:

  • AGENTS.md
  • supabase/functions/_backend/private/groups.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : All database operations must use `getPgClient()` or `getDrizzleClient()` from `utils/pg.ts` for PostgreSQL access during active migration to Cloudflare D1

Applied to files:

  • AGENTS.md
  • supabase/functions/_backend/private/groups.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use `createHono` from `utils/hono.ts` for all Hono framework application initialization and routing

Applied to files:

  • supabase/functions/_backend/private/groups.ts
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/functions/_backend/**/*.ts : Place core backend logic in supabase/functions/_backend/ with plugins, private, public, triggers, and utils subdirectories

Applied to files:

  • supabase/functions/_backend/private/groups.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use `middlewareAPISecret` for internal API endpoints and `middlewareKey` for external API keys; validate against `owner_org` in the `apikeys` table

Applied to files:

  • supabase/functions/_backend/private/groups.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Check `c.get('auth')?.authType` to determine authentication type ('apikey' vs 'jwt') in backend endpoints

Applied to files:

  • supabase/functions/_backend/private/groups.ts
  • src/components/tables/AccessTable.vue
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/functions/**/*.ts : Never use the Supabase admin SDK with service key for user-facing APIs; always use client SDK with user authentication to enforce RLS policies. Admin SDK should only be used for internal operations (triggers, CRON jobs, etc.)

Applied to files:

  • supabase/functions/_backend/private/groups.ts
📚 Learning: 2025-12-24T14:11:10.256Z
Learnt from: WcaleNieWolny
Repo: Cap-go/capgo PR: 1300
File: supabase/migrations/20251224103713_2fa_enforcement.sql:409-539
Timestamp: 2025-12-24T14:11:10.256Z
Learning: In supabase/migrations for get_orgs_v6 and get_orgs_v7: The inner functions with user_id parameter (get_orgs_v6(uuid) and get_orgs_v7(uuid)) should NOT be granted to anon/authenticated roles as this allows any user to query other users' organizations; only the no-argument wrapper functions should be public as they perform authentication checks.

Applied to files:

  • supabase/functions/_backend/private/groups.ts
  • src/components/tables/AccessTable.vue
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/utils/postgress_schema.ts : Schema definitions must be placed in `utils/postgress_schema.ts` using Drizzle ORM and never edited in committed migration files

Applied to files:

  • supabase/functions/_backend/private/groups.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use Drizzle ORM query patterns with `schema` from `postgress_schema.ts` for all database operations; use `aliasV2()` for self-joins or multiple table references

Applied to files:

  • supabase/functions/_backend/private/groups.ts
  • src/components/tables/AccessTable.vue
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/functions/**/*.ts : When admin access is unavoidable for a user-facing endpoint, sanitize all user inputs carefully to prevent PostgREST query injection

Applied to files:

  • supabase/functions/_backend/private/groups.ts
  • src/components/tables/AccessTable.vue
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/seed.sql : Update supabase/seed.sql for new or evolved tests; keep fixtures focused on current behavior while leaving committed migrations unchanged

Applied to files:

  • supabase/functions/_backend/private/groups.ts
📚 Learning: 2025-12-23T01:19:15.067Z
Learnt from: riderx
Repo: Cap-go/capgo PR: 1297
File: src/components/dashboard/DeploymentBanner.vue:77-79
Timestamp: 2025-12-23T01:19:15.067Z
Learning: In the Cap-go codebase, the 'owner' role exists only at the organization level (used in src/stores/organization.ts), not as an app-level permission. App-level permissions use the user_min_right database enum which includes: read, upload, write, admin, super_admin (but NOT owner). When checking app permissions, never include 'owner' in the role checks.

Applied to files:

  • supabase/functions/_backend/private/groups.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : All Hono endpoint handlers must accept `Context<MiddlewareKeyVariables>` and use `c.get('requestId')`, `c.get('apikey')`, and `c.get('auth')` for request context

Applied to files:

  • supabase/functions/_backend/private/groups.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use structured logging with `cloudlog({ requestId: c.get('requestId'), message: '...' })` for all backend logging

Applied to files:

  • supabase/functions/_backend/private/groups.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to cloudflare_workers/api/index.ts : API Worker (port 8787) routes: `/bundle`, `/app`, `/device`, `/channel`, `/private/*`, `/triggers`

Applied to files:

  • supabase/functions/_backend/private/groups.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Run `bun types` after schema changes to regenerate TypeScript types from Supabase

Applied to files:

  • src/components/tables/AccessTable.vue
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/migrations/*.sql : Use `get_identity_org_appid()` for RLS policies when app_id exists on the table instead of `get_identity()` directly

Applied to files:

  • src/components/tables/AccessTable.vue
📚 Learning: 2025-05-29T18:21:17.703Z
Learnt from: WcaleNieWolny
Repo: Cap-go/capgo PR: 1107
File: src/components/dashboard/AppSetting.vue:85-182
Timestamp: 2025-05-29T18:21:17.703Z
Learning: In setUpdateChannelSync function in src/components/dashboard/AppSetting.vue, the database update code at the end is intentionally outside the conflict-resolution if block and should run for cases where sync is being toggled without conflicts or after conflicts are resolved. The return statements inside the dialog handling correctly prevent database updates when the user cancels.

Applied to files:

  • src/components/tables/AccessTable.vue
📚 Learning: 2025-12-23T01:19:04.593Z
Learnt from: riderx
Repo: Cap-go/capgo PR: 1297
File: src/components/dashboard/DeploymentBanner.vue:77-79
Timestamp: 2025-12-23T01:19:04.593Z
Learning: In the Cap-go codebase, ensure that app permission checks never include the role 'owner'. App-level permissions should be based on the user_min_right enum with values: read, upload, write, admin, super_admin (and NOT owner). This pattern applies across Vue components that perform permission checks; if you find a check referencing 'owner' for app-level access, replace it with the appropriate user_min_right value and keep organization-level owner handling in organization.ts.

Applied to files:

  • src/components/tables/AccessTable.vue
🪛 markdownlint-cli2 (0.18.1)
AGENTS.md

196-196: Bare URL used

(MD034, no-bare-urls)

🔇 Additional comments (16)
AGENTS.md (1)

190-197: Helpful editorial clarifications for development standards.

These additions provide clear guidance on code comments and commit message conventions, improving consistency across the codebase. The requirement for English comments and adherence to Conventional Commits are both reasonable standards that will benefit maintainability and collaboration.

supabase/functions/_backend/private/groups.ts (7)

1-30: LGTM! Imports and initialization follow backend conventions.

The file correctly uses createHono from utils/hono.ts, imports the proper database utilities (getPgClient, getDrizzleClient, closeClient), and includes structured logging with cloudlogErr. The UUID validation and JSON parsing helpers are well-implemented.


31-82: LGTM! Well-structured endpoint with proper auth, validation, and resource cleanup.

The endpoint correctly validates auth and UUID format before permission checks, uses the proper database client lifecycle pattern with finally block cleanup, and includes structured error logging with requestId.


84-146: LGTM! Create group endpoint properly validates inputs and enforces admin permissions.

JSON parsing occurs after authentication and authorization checks, following the recommended pattern from previous reviews.


148-217: LGTM! Update endpoint correctly protects system groups and validates ownership.

The endpoint properly fetches the group first to obtain org_id for permission checking, and includes protection against modifying system groups.


256-270: LGTM! Atomic deletion with transaction ensures data consistency.

The transactional handling of role_bindings and groups deletion addresses the previous concern about non-atomic operations. This prevents orphaned role bindings if the group deletion fails.


290-351: LGTM! Members endpoint correctly joins user data and enforces read permissions.

The inner join with schema.users properly enriches the response with email addresses. Permission checking uses org.read_members which is appropriate for read-only access.


461-526: LGTM! Member removal endpoint properly validates both IDs and enforces admin permissions.

Unlike the POST endpoint, targetUserId here is correctly declared via c.req.param('user_id'). The endpoint follows the established pattern for auth, validation, and cleanup.

src/components/tables/AccessTable.vue (8)

1-19: LGTM!

Imports follow the ~/ alias convention and use Vue 3 Composition API correctly. Props are properly typed.


20-61: LGTM!

The RoleBinding interface is well-defined, and appRoleOptions provides a clean computed list of translated app-level roles.


63-83: LGTM!

Properly handles the stale app.value case by setting to undefined on error or miss, and resets canUpdateUserRoles appropriately.


147-197: LGTM!

Permission backstop and role validation are now in place. The role lookup and update logic is sound, with proper error handling and user feedback.


199-240: LGTM!

The dialog dismissal pattern correctly handles all cases including backdrop clicks and Escape key. The lastButtonRole check ensures deletion only proceeds on explicit confirmation.


242-263: LGTM!

The data loading flow correctly sequences loadAppInfo before fetchData, and the watcher with immediate: true ensures data loads on mount.


265-339: LGTM!

The filtering logic handles null values safely, and the dynamic columns correctly gate action buttons based on permission state.


342-383: LGTM!

Template correctly uses Tailwind utilities and DaisyUI components per coding guidelines. The Teleport now uses dialogOptions.id for reliable modal detection.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
supabase/functions/_backend/triggers/on_user_delete.ts (2)

358-447: Avoid “all-or-nothing” cleanup: don’t abort on one lookup failure; don’t let one failing promise cancel the rest.

  • The repeated if (!X) return c.json(BRES) (Line 359-408) means a transient failure in any single lookup prevents subscription/Bento cleanup for sole-admin orgs.
  • await Promise.all(promises) (Line 433) means one failing cancelSubscription/unsubscribeBento/storage deletion can abort all remaining cleanup work.
Proposed resilient pattern
-  const legacyOrgIds = await fetchLegacySuperAdminOrgIds(c, supabase, record.id)
-  if (!legacyOrgIds) {
-    return c.json(BRES)
-  }
+  const legacyOrgIds = (await fetchLegacySuperAdminOrgIds(c, supabase, record.id)) ?? []
@@
-  const directRbacBindings = await fetchDirectRbacBindings(c, supabase, record.id)
-  if (!directRbacBindings) {
-    return c.json(BRES)
-  }
+  const directRbacBindings = (await fetchDirectRbacBindings(c, supabase, record.id)) ?? []
@@
-  const groupIds = await fetchUserGroupIds(c, supabase, record.id)
-  if (!groupIds) {
-    return c.json(BRES)
-  }
+  const groupIds = (await fetchUserGroupIds(c, supabase, record.id)) ?? []
@@
-  const groupRbacBindings = await fetchGroupRbacBindings(c, supabase, groupIds)
-  if (!groupRbacBindings) {
-    return c.json(BRES)
-  }
+  const groupRbacBindings = (await fetchGroupRbacBindings(c, supabase, groupIds)) ?? []
@@
-  await Promise.all(promises)
+  const results = await Promise.allSettled(promises)
+  for (const r of results) {
+    if (r.status === 'rejected') {
+      logFailure(c, 'user deletion cleanup task failed', r.reason)
+    }
+  }

449-456: Don’t log full user records (PII risk).

cloudlog({ ..., message: 'record', record }) (Line 451) will likely ship user PII (email, etc.) to logs. Prefer logging only stable identifiers (e.g., user_id, maybe a redacted email domain if truly needed).

Proposed change
-  cloudlog({ requestId: c.get('requestId'), message: 'record', record })
+  cloudlog({ requestId: c.get('requestId'), message: 'on_user_delete: received', user_id: record.id })
supabase/functions/_backend/private/delete_failed_version.ts (1)

2-2: Use createHono instead of new Hono() per coding guidelines.

Line 16 uses new Hono<MiddlewareKeyVariables>() directly instead of createHono from utils/hono.ts. As per coding guidelines, all Hono framework application initialization should use createHono to ensure consistent request ID handling, logging, and error formatting across the codebase.

Proposed fix
 import type { MiddlewareKeyVariables } from '../utils/hono.ts'
-import { Hono } from 'hono/tiny'
-import { parseBody, quickError, simpleError } from '../utils/hono.ts'
+import { createHono, parseBody, quickError, simpleError } from '../utils/hono.ts'
 import { middlewareKey } from '../utils/hono_middleware.ts'
 import { cloudlog } from '../utils/logging.ts'
 import { logsnag } from '../utils/logsnag.ts'
 import { checkPermission } from '../utils/rbac.ts'
 import { s3 } from '../utils/s3.ts'
 import { supabaseApikey } from '../utils/supabase.ts'
+import { version } from '../utils/version.ts'
 
 ...
 
-export const app = new Hono<MiddlewareKeyVariables>()
+export const app = createHono('', version)

Also applies to: 16-16

🤖 Fix all issues with AI agents
In `@RBAC_SYSTEM.md`:
- Around line 193-198: There is a contradiction: one place states that org_admin
"cannot delete channels" while other places (the org_admin example listing "all
channel.* permissions" and the permission matrix granting channel.delete to
org_admin) allow channel deletion; decide which rule is correct and make all
three spots consistent — if org_admin should not delete channels, remove
channel.delete from the org_admin example list and from the permission matrix
row that grants channel.delete to org_admin and update any nearby explanatory
text; if org_admin should be allowed to delete channels, remove or reword the
"cannot delete channels" sentence to reflect that and ensure the example list
and permission matrix remain aligned.

In `@supabase/functions/_backend/public/statistics/index.ts`:
- Line 688: Replace the non-error logger call cloudlogErr(...) used to record
organization IDs with the informational structured logger cloudlog(...) so that
only true errors go to error logs; locate the call to cloudlogErr({ requestId:
c.get('requestId'), message: 'orgs', data: orgIds }) and swap it to cloudlog
while preserving the same structured payload (requestId, message, data) and
context so organization IDs are recorded as informational logs rather than
errors.

In `@supabase/functions/_backend/utils/hono_middleware.ts`:
- Around line 18-25: getAppIdFromRequest can throw synchronously because
Request.clone() throws if the body was consumed; replace the current .catch() on
the promise with a synchronous try/catch around the clone+json call inside
getAppIdFromRequest (i.e., wrap c.req.raw.clone().json() in try { const body =
await c.req.raw.clone().json(); } catch (err) { return null; } ), then use
body?.app_id if present; ensure the catch handles the synchronous TypeError
thrown by clone and any JSON parse errors so the function returns null on
failure.
♻️ Duplicate comments (2)
supabase/functions/_backend/private/role_bindings.ts (2)

122-140: Missing validation for apikey principal type.

The function validates user and group principals but falls through to success for apikey without any validation. This could allow creating role bindings for non-existent or unauthorized API keys.

Proposed fix to add apikey validation
   if (principalType === 'group') {
     const [group] = await drizzle
       .select()
       .from(schema.groups)
       .where(
         and(
           eq(schema.groups.id, principalId),
           eq(schema.groups.org_id, orgId),
         ),
       )
       .limit(1)

     if (!group) {
       return { ok: false, status: 400, error: 'Group not found in this org' }
     }
   }

+  if (principalType === 'apikey') {
+    const [apikey] = await drizzle
+      .select({ id: schema.apikeys.id })
+      .from(schema.apikeys)
+      .where(
+        and(
+          eq(schema.apikeys.id, principalId),
+          eq(schema.apikeys.owner_org, orgId),
+        ),
+      )
+      .limit(1)
+
+    if (!apikey) {
+      return { ok: false, status: 400, error: 'API key not found in this org' }
+    }
+  }
+
   return { ok: true, data: null }
 }

356-374: Add is_direct check before allowing deletion.

The endpoint allows deleting any role binding, but inherited bindings (where is_direct is false) come from group membership and typically shouldn't be deleted directly. Deleting an inherited binding could cause inconsistent state.

Proposed fix to prevent deleting inherited bindings
     const [binding] = await drizzle
       .select()
       .from(schema.role_bindings)
       .where(eq(schema.role_bindings.id, bindingId))
       .limit(1)

     if (!binding) {
       return c.json({ error: 'Role binding not found' }, 404)
     }

+    if (!binding.is_direct) {
+      return c.json({ error: 'Cannot delete inherited role binding - remove user from group instead' }, 400)
+    }
+
     if (!(await checkPermission(c, 'org.update_user_roles', { orgId: binding.org_id }))) {
       return c.json({ error: 'Forbidden - Admin rights required' }, 403)
     }
🧹 Nitpick comments (8)
supabase/functions/_backend/public/statistics/index.ts (1)

597-609: LGTM with a note on redundancy.

The unified RBAC check for org.read is correctly implemented. The additional limited_to_orgs and limited_to_apps checks at lines 601-609 provide a defensive layer. If the underlying rbac_check_permission_direct SQL function already handles these apikey restrictions, consider removing these redundant checks in a follow-up to simplify the code.

supabase/functions/_backend/public/build/upload.ts (1)

14-18: Type annotation should use Context<MiddlewareKeyVariables> per coding guidelines.

The function uses c.get('requestId'), c.get('auth'), and c.get('capgkey') via checkPermission, which require the MiddlewareKeyVariables type. The bare Context type doesn't guarantee these context values are available.

As per coding guidelines, all Hono endpoint handlers must accept Context<MiddlewareKeyVariables>.

♻️ Suggested fix
+import type { MiddlewareKeyVariables } from '../../utils/hono.ts'

 export async function tusProxy(
-  c: Context,
+  c: Context<MiddlewareKeyVariables>,
   jobId: string,
   apikey: Database['public']['Tables']['apikeys']['Row'],
 ): Promise<Response> {
supabase/functions/_backend/triggers/on_user_delete.ts (1)

4-12: Use createHono for app initialization (guideline compliance).

This file is under supabase/functions/_backend/** but initializes Hono via new Hono(...) (Line 11) instead of the required createHono from utils/hono.ts.

Proposed change
-import { Hono } from 'hono/tiny'
+import { createHono } from '../utils/hono.ts'
@@
-export const app = new Hono<MiddlewareKeyVariables>()
+export const app = createHono()
supabase/functions/_backend/utils/hono_middleware.ts (1)

280-283: Validate x-limited-key-id parsing (avoid silently treating invalid values as “no subkey”).

Number(headerValue) can produce NaN (Line 282), and if (subkey_id) will treat it as falsy, skipping subkey validation entirely. Consider either:

  • return Number.isFinite(n) ? n : null, or
  • returning a quickError(400, ...) if the header is present but invalid.

Also applies to: 406-441, 487-533

supabase/functions/_backend/utils/rbac.ts (1)

460-469: Consider making the fallback explicit or throwing for unknown prefixes.

The getScopeTypeFromPermission function returns 'org' as a default fallback for unknown permission prefixes. While this is safe, consider whether throwing an error would be more appropriate to catch typos or invalid permissions early.

Optional: Make fallback behavior explicit
 export function getScopeTypeFromPermission(permission: Permission): ScopeType {
   if (permission.startsWith('org.'))
     return 'org'
   if (permission.startsWith('app.') || permission.startsWith('bundle.'))
     return 'app'
   if (permission.startsWith('channel.'))
     return 'channel'
   if (permission.startsWith('platform.'))
     return 'platform'
-  return 'org' // Default fallback
+  // Since Permission is a union type, this should be unreachable
+  // If we get here, throw to catch unexpected permissions early
+  throw new Error(`Unknown permission scope for: ${permission}`)
 }
supabase/functions/_backend/private/delete_failed_version.ts (1)

25-34: LGTM on RBAC integration, but _userId appears unused now.

The RBAC check using checkPermission(c, 'bundle.delete', { appId: body.app_id }) is correct. However, _userId fetched via RPC on line 25-26 appears to be unused after the RBAC refactor. Consider whether this RPC call is still needed.

Verify if get_user_id RPC call is still needed
#!/bin/bash
# Check if _userId is used anywhere in the function after line 25
cat supabase/functions/_backend/private/delete_failed_version.ts | grep -n "_userId" | head -20
supabase/functions/_backend/utils/org_email_notifications.ts (2)

105-163: Multi-source RBAC membership resolution with expiration filtering.

The implementation correctly:

  • Queries user-level role bindings with expiration checks
  • Queries group-level role bindings and resolves group members
  • Filters expired bindings using new Date(expiresAt) <= now / > now
  • Accumulates unique user IDs in a Set to prevent duplicates across sources

However, consider consolidating the expiration filtering logic to reduce duplication:

♻️ Optional: Extract expiration check helper
function isBindingExpired(expiresAt: string | null | undefined): boolean {
  return !!expiresAt && new Date(expiresAt) <= new Date()
}

117-124: Type assertions suggest missing type generation.

Multiple (binding as any) assertions are used to access expires_at and principal_id fields. This indicates the Supabase types may not be fully generated for the new RBAC tables.

Consider regenerating Supabase types to include the RBAC schema (role_bindings, group_members), which would remove the need for unsafe type assertions and improve type safety:

# Typically run from project root
bunx supabase gen types typescript --local > src/types/supabase.types.ts

Also applies to: 140-145

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c54baf8 and 97622c3.

📒 Files selected for processing (26)
  • AGENTS.md
  • RBAC_SYSTEM.md
  • src/components/tables/AccessTable.vue
  • supabase/functions/_backend/private/delete_failed_version.ts
  • supabase/functions/_backend/private/download_link.ts
  • supabase/functions/_backend/private/groups.ts
  • supabase/functions/_backend/private/invite_new_user_to_org.ts
  • supabase/functions/_backend/private/role_bindings.ts
  • supabase/functions/_backend/private/upload_link.ts
  • supabase/functions/_backend/public/app/delete.ts
  • supabase/functions/_backend/public/build/cancel.ts
  • supabase/functions/_backend/public/build/request.ts
  • supabase/functions/_backend/public/build/start.ts
  • supabase/functions/_backend/public/build/status.ts
  • supabase/functions/_backend/public/build/upload.ts
  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/public/organization/get.ts
  • supabase/functions/_backend/public/organization/members/get.ts
  • supabase/functions/_backend/public/organization/put.ts
  • supabase/functions/_backend/public/statistics/index.ts
  • supabase/functions/_backend/triggers/on_user_delete.ts
  • supabase/functions/_backend/utils/hono_middleware.ts
  • supabase/functions/_backend/utils/org_email_notifications.ts
  • supabase/functions/_backend/utils/pg.ts
  • supabase/functions/_backend/utils/rbac.ts
  • supabase/seed.sql
✅ Files skipped from review due to trivial changes (1)
  • supabase/functions/_backend/utils/pg.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • supabase/functions/_backend/private/groups.ts
  • supabase/functions/_backend/public/app/delete.ts
  • supabase/functions/_backend/public/build/status.ts
  • supabase/functions/_backend/private/invite_new_user_to_org.ts
  • src/components/tables/AccessTable.vue
  • supabase/functions/_backend/public/build/cancel.ts
  • supabase/functions/_backend/public/build/start.ts
  • supabase/functions/_backend/private/download_link.ts
  • supabase/functions/_backend/private/upload_link.ts
🧰 Additional context used
📓 Path-based instructions (8)
supabase/functions/_backend/**/*.{ts,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

supabase/functions/_backend/**/*.{ts,js}: Backend code must be placed in supabase/functions/_backend/ as shared code deployed to Cloudflare Workers (API/Plugin/Files workers), Supabase Edge Functions, and other platforms
Use createHono from utils/hono.ts for all Hono framework application initialization and routing
All database operations must use getPgClient() or getDrizzleClient() from utils/pg.ts for PostgreSQL access during active migration to Cloudflare D1
All Hono endpoint handlers must accept Context<MiddlewareKeyVariables> and use c.get('requestId'), c.get('apikey'), and c.get('auth') for request context
Use structured logging with cloudlog({ requestId: c.get('requestId'), message: '...' }) for all backend logging
Use middlewareAPISecret for internal API endpoints and middlewareKey for external API keys; validate against owner_org in the apikeys table
Check c.get('auth')?.authType to determine authentication type ('apikey' vs 'jwt') in backend endpoints
Use Drizzle ORM query patterns with schema from postgress_schema.ts for all database operations; use aliasV2() for self-joins or multiple table references

Files:

  • supabase/functions/_backend/public/build/request.ts
  • supabase/functions/_backend/triggers/on_user_delete.ts
  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/public/statistics/index.ts
  • supabase/functions/_backend/private/role_bindings.ts
  • supabase/functions/_backend/utils/hono_middleware.ts
  • supabase/functions/_backend/public/organization/put.ts
  • supabase/functions/_backend/utils/rbac.ts
  • supabase/functions/_backend/public/organization/members/get.ts
  • supabase/functions/_backend/private/delete_failed_version.ts
  • supabase/functions/_backend/public/organization/get.ts
  • supabase/functions/_backend/public/build/upload.ts
  • supabase/functions/_backend/utils/org_email_notifications.ts
supabase/functions/**/*.{ts,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Backend ESLint must pass before commit; run bun lint:backend for backend files

Files:

  • supabase/functions/_backend/public/build/request.ts
  • supabase/functions/_backend/triggers/on_user_delete.ts
  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/public/statistics/index.ts
  • supabase/functions/_backend/private/role_bindings.ts
  • supabase/functions/_backend/utils/hono_middleware.ts
  • supabase/functions/_backend/public/organization/put.ts
  • supabase/functions/_backend/utils/rbac.ts
  • supabase/functions/_backend/public/organization/members/get.ts
  • supabase/functions/_backend/private/delete_failed_version.ts
  • supabase/functions/_backend/public/organization/get.ts
  • supabase/functions/_backend/public/build/upload.ts
  • supabase/functions/_backend/utils/org_email_notifications.ts
**/*.{vue,ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Run bun lint to lint Vue, TypeScript, and JavaScript files; use bun lint:fix to auto-fix issues

Files:

  • supabase/functions/_backend/public/build/request.ts
  • supabase/functions/_backend/triggers/on_user_delete.ts
  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/public/statistics/index.ts
  • supabase/functions/_backend/private/role_bindings.ts
  • supabase/functions/_backend/utils/hono_middleware.ts
  • supabase/functions/_backend/public/organization/put.ts
  • supabase/functions/_backend/utils/rbac.ts
  • supabase/functions/_backend/public/organization/members/get.ts
  • supabase/functions/_backend/private/delete_failed_version.ts
  • supabase/functions/_backend/public/organization/get.ts
  • supabase/functions/_backend/public/build/upload.ts
  • supabase/functions/_backend/utils/org_email_notifications.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use single quotes and no semicolons per @antfu/eslint-config rules

Files:

  • supabase/functions/_backend/public/build/request.ts
  • supabase/functions/_backend/triggers/on_user_delete.ts
  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/public/statistics/index.ts
  • supabase/functions/_backend/private/role_bindings.ts
  • supabase/functions/_backend/utils/hono_middleware.ts
  • supabase/functions/_backend/public/organization/put.ts
  • supabase/functions/_backend/utils/rbac.ts
  • supabase/functions/_backend/public/organization/members/get.ts
  • supabase/functions/_backend/private/delete_failed_version.ts
  • supabase/functions/_backend/public/organization/get.ts
  • supabase/functions/_backend/public/build/upload.ts
  • supabase/functions/_backend/utils/org_email_notifications.ts
supabase/functions/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

supabase/functions/**/*.ts: Never use the Supabase admin SDK with service key for user-facing APIs; always use client SDK with user authentication to enforce RLS policies. Admin SDK should only be used for internal operations (triggers, CRON jobs, etc.)
When admin access is unavoidable for a user-facing endpoint, sanitize all user inputs carefully to prevent PostgREST query injection

Files:

  • supabase/functions/_backend/public/build/request.ts
  • supabase/functions/_backend/triggers/on_user_delete.ts
  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/public/statistics/index.ts
  • supabase/functions/_backend/private/role_bindings.ts
  • supabase/functions/_backend/utils/hono_middleware.ts
  • supabase/functions/_backend/public/organization/put.ts
  • supabase/functions/_backend/utils/rbac.ts
  • supabase/functions/_backend/public/organization/members/get.ts
  • supabase/functions/_backend/private/delete_failed_version.ts
  • supabase/functions/_backend/public/organization/get.ts
  • supabase/functions/_backend/public/build/upload.ts
  • supabase/functions/_backend/utils/org_email_notifications.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Run bun lint or lint/format command before validating any backend or frontend task to ensure consistent formatting

Files:

  • supabase/functions/_backend/public/build/request.ts
  • supabase/functions/_backend/triggers/on_user_delete.ts
  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/public/statistics/index.ts
  • supabase/functions/_backend/private/role_bindings.ts
  • supabase/functions/_backend/utils/hono_middleware.ts
  • supabase/functions/_backend/public/organization/put.ts
  • supabase/functions/_backend/utils/rbac.ts
  • supabase/functions/_backend/public/organization/members/get.ts
  • supabase/functions/_backend/private/delete_failed_version.ts
  • supabase/functions/_backend/public/organization/get.ts
  • supabase/functions/_backend/public/build/upload.ts
  • supabase/functions/_backend/utils/org_email_notifications.ts
supabase/functions/_backend/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Place core backend logic in supabase/functions/_backend/ with plugins, private, public, triggers, and utils subdirectories

Files:

  • supabase/functions/_backend/public/build/request.ts
  • supabase/functions/_backend/triggers/on_user_delete.ts
  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/public/statistics/index.ts
  • supabase/functions/_backend/private/role_bindings.ts
  • supabase/functions/_backend/utils/hono_middleware.ts
  • supabase/functions/_backend/public/organization/put.ts
  • supabase/functions/_backend/utils/rbac.ts
  • supabase/functions/_backend/public/organization/members/get.ts
  • supabase/functions/_backend/private/delete_failed_version.ts
  • supabase/functions/_backend/public/organization/get.ts
  • supabase/functions/_backend/public/build/upload.ts
  • supabase/functions/_backend/utils/org_email_notifications.ts
supabase/seed.sql

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Seed database with supabase db reset to apply all migrations and test data from supabase/seed.sql

Update supabase/seed.sql for new or evolved tests; keep fixtures focused on current behavior while leaving committed migrations unchanged

Files:

  • supabase/seed.sql
🧠 Learnings (28)
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Check `c.get('auth')?.authType` to determine authentication type ('apikey' vs 'jwt') in backend endpoints

Applied to files:

  • supabase/functions/_backend/public/build/request.ts
  • supabase/functions/_backend/triggers/on_user_delete.ts
  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/public/statistics/index.ts
  • supabase/functions/_backend/private/role_bindings.ts
  • supabase/functions/_backend/utils/hono_middleware.ts
  • supabase/functions/_backend/public/organization/put.ts
  • supabase/functions/_backend/utils/rbac.ts
  • supabase/functions/_backend/public/organization/members/get.ts
  • supabase/functions/_backend/private/delete_failed_version.ts
  • supabase/functions/_backend/public/organization/get.ts
  • supabase/functions/_backend/public/build/upload.ts
  • RBAC_SYSTEM.md
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use `middlewareAPISecret` for internal API endpoints and `middlewareKey` for external API keys; validate against `owner_org` in the `apikeys` table

Applied to files:

  • supabase/functions/_backend/public/build/request.ts
  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/public/statistics/index.ts
  • supabase/functions/_backend/private/role_bindings.ts
  • supabase/functions/_backend/utils/hono_middleware.ts
  • supabase/functions/_backend/public/organization/put.ts
  • supabase/functions/_backend/utils/rbac.ts
  • supabase/functions/_backend/public/organization/members/get.ts
  • supabase/functions/_backend/private/delete_failed_version.ts
  • supabase/functions/_backend/public/organization/get.ts
  • supabase/functions/_backend/public/build/upload.ts
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/functions/**/*.ts : Never use the Supabase admin SDK with service key for user-facing APIs; always use client SDK with user authentication to enforce RLS policies. Admin SDK should only be used for internal operations (triggers, CRON jobs, etc.)

Applied to files:

  • supabase/functions/_backend/public/build/request.ts
  • supabase/functions/_backend/triggers/on_user_delete.ts
  • supabase/functions/_backend/public/statistics/index.ts
  • supabase/functions/_backend/private/role_bindings.ts
  • supabase/functions/_backend/utils/hono_middleware.ts
  • supabase/functions/_backend/public/organization/put.ts
  • supabase/functions/_backend/utils/rbac.ts
  • supabase/functions/_backend/public/organization/members/get.ts
  • supabase/functions/_backend/private/delete_failed_version.ts
  • supabase/functions/_backend/public/organization/get.ts
  • supabase/functions/_backend/public/build/upload.ts
  • RBAC_SYSTEM.md
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : All Hono endpoint handlers must accept `Context<MiddlewareKeyVariables>` and use `c.get('requestId')`, `c.get('apikey')`, and `c.get('auth')` for request context

Applied to files:

  • supabase/functions/_backend/public/build/request.ts
  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/public/statistics/index.ts
  • supabase/functions/_backend/private/role_bindings.ts
  • supabase/functions/_backend/utils/hono_middleware.ts
  • supabase/functions/_backend/public/organization/put.ts
  • supabase/functions/_backend/utils/rbac.ts
  • supabase/functions/_backend/public/organization/members/get.ts
  • supabase/functions/_backend/private/delete_failed_version.ts
  • supabase/functions/_backend/public/organization/get.ts
  • supabase/functions/_backend/public/build/upload.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use structured logging with `cloudlog({ requestId: c.get('requestId'), message: '...' })` for all backend logging

Applied to files:

  • supabase/functions/_backend/public/build/request.ts
  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/public/statistics/index.ts
  • supabase/functions/_backend/private/role_bindings.ts
  • supabase/functions/_backend/utils/hono_middleware.ts
  • supabase/functions/_backend/utils/rbac.ts
  • supabase/functions/_backend/public/build/upload.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use `createHono` from `utils/hono.ts` for all Hono framework application initialization and routing

Applied to files:

  • supabase/functions/_backend/public/build/request.ts
  • supabase/functions/_backend/public/statistics/index.ts
  • supabase/functions/_backend/private/role_bindings.ts
  • supabase/functions/_backend/utils/hono_middleware.ts
  • supabase/functions/_backend/public/organization/put.ts
  • supabase/functions/_backend/utils/rbac.ts
  • supabase/functions/_backend/public/organization/members/get.ts
  • supabase/functions/_backend/private/delete_failed_version.ts
  • supabase/functions/_backend/public/organization/get.ts
  • supabase/functions/_backend/public/build/upload.ts
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/functions/**/*.ts : When admin access is unavoidable for a user-facing endpoint, sanitize all user inputs carefully to prevent PostgREST query injection

Applied to files:

  • supabase/functions/_backend/public/build/request.ts
  • supabase/functions/_backend/private/role_bindings.ts
  • supabase/functions/_backend/utils/hono_middleware.ts
  • supabase/functions/_backend/public/build/upload.ts
  • RBAC_SYSTEM.md
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/functions/_backend/**/*.ts : Place core backend logic in supabase/functions/_backend/ with plugins, private, public, triggers, and utils subdirectories

Applied to files:

  • supabase/functions/_backend/triggers/on_user_delete.ts
  • supabase/functions/_backend/public/statistics/index.ts
  • supabase/functions/_backend/private/role_bindings.ts
  • supabase/functions/_backend/utils/rbac.ts
  • supabase/functions/_backend/public/build/upload.ts
📚 Learning: 2025-12-24T14:11:10.256Z
Learnt from: WcaleNieWolny
Repo: Cap-go/capgo PR: 1300
File: supabase/migrations/20251224103713_2fa_enforcement.sql:409-539
Timestamp: 2025-12-24T14:11:10.256Z
Learning: In supabase/migrations for get_orgs_v6 and get_orgs_v7: The inner functions with user_id parameter (get_orgs_v6(uuid) and get_orgs_v7(uuid)) should NOT be granted to anon/authenticated roles as this allows any user to query other users' organizations; only the no-argument wrapper functions should be public as they perform authentication checks.

Applied to files:

  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/public/statistics/index.ts
  • supabase/functions/_backend/private/role_bindings.ts
  • supabase/functions/_backend/utils/hono_middleware.ts
  • supabase/seed.sql
  • supabase/functions/_backend/public/organization/put.ts
  • supabase/functions/_backend/utils/rbac.ts
  • supabase/functions/_backend/public/organization/members/get.ts
  • supabase/functions/_backend/public/organization/get.ts
  • supabase/functions/_backend/utils/org_email_notifications.ts
  • RBAC_SYSTEM.md
📚 Learning: 2025-12-23T01:19:15.067Z
Learnt from: riderx
Repo: Cap-go/capgo PR: 1297
File: src/components/dashboard/DeploymentBanner.vue:77-79
Timestamp: 2025-12-23T01:19:15.067Z
Learning: In the Cap-go codebase, the 'owner' role exists only at the organization level (used in src/stores/organization.ts), not as an app-level permission. App-level permissions use the user_min_right database enum which includes: read, upload, write, admin, super_admin (but NOT owner). When checking app permissions, never include 'owner' in the role checks.

Applied to files:

  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/public/statistics/index.ts
  • supabase/functions/_backend/private/role_bindings.ts
  • supabase/functions/_backend/utils/rbac.ts
  • RBAC_SYSTEM.md
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/migrations/*.sql : Use `get_identity_org_appid()` for RLS policies when app_id exists on the table instead of `get_identity()` directly

Applied to files:

  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/seed.sql
  • supabase/functions/_backend/public/organization/put.ts
  • RBAC_SYSTEM.md
📚 Learning: 2025-12-25T11:22:19.594Z
Learnt from: WcaleNieWolny
Repo: Cap-go/capgo PR: 1300
File: supabase/migrations/20251224103713_2fa_enforcement.sql:85-96
Timestamp: 2025-12-25T11:22:19.594Z
Learning: In the 2FA enforcement implementation for supabase/migrations: When an org has enforcing_2fa=true, all users including super_admins must have 2FA enabled before accessing any org functions (including check_org_members_2fa_enabled); this is intentional behavior to ensure consistent security enforcement without exceptions for admins.

Applied to files:

  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/seed.sql
  • supabase/functions/_backend/public/organization/put.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Backend code must be placed in `supabase/functions/_backend/` as shared code deployed to Cloudflare Workers (API/Plugin/Files workers), Supabase Edge Functions, and other platforms

Applied to files:

  • supabase/functions/_backend/private/role_bindings.ts
  • supabase/functions/_backend/utils/hono_middleware.ts
  • AGENTS.md
  • supabase/functions/_backend/utils/rbac.ts
  • supabase/functions/_backend/public/build/upload.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/**/*.{ts,js} : Backend ESLint must pass before commit; run `bun lint:backend` for backend files

Applied to files:

  • supabase/functions/_backend/private/role_bindings.ts
  • AGENTS.md
  • supabase/functions/_backend/utils/rbac.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use Drizzle ORM query patterns with `schema` from `postgress_schema.ts` for all database operations; use `aliasV2()` for self-joins or multiple table references

Applied to files:

  • supabase/functions/_backend/private/role_bindings.ts
  • supabase/functions/_backend/utils/hono_middleware.ts
  • supabase/functions/_backend/public/organization/put.ts
  • supabase/functions/_backend/utils/rbac.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : All database operations must use `getPgClient()` or `getDrizzleClient()` from `utils/pg.ts` for PostgreSQL access during active migration to Cloudflare D1

Applied to files:

  • supabase/functions/_backend/private/role_bindings.ts
  • supabase/functions/_backend/utils/hono_middleware.ts
  • supabase/functions/_backend/public/organization/put.ts
  • AGENTS.md
  • supabase/functions/_backend/utils/rbac.ts
  • supabase/functions/_backend/public/build/upload.ts
  • RBAC_SYSTEM.md
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/utils/postgress_schema.ts : Schema definitions must be placed in `utils/postgress_schema.ts` using Drizzle ORM and never edited in committed migration files

Applied to files:

  • supabase/functions/_backend/private/role_bindings.ts
  • RBAC_SYSTEM.md
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/seed.sql : Update supabase/seed.sql for new or evolved tests; keep fixtures focused on current behavior while leaving committed migrations unchanged

Applied to files:

  • supabase/seed.sql
  • RBAC_SYSTEM.md
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/seed.sql : Seed database with `supabase db reset` to apply all migrations and test data from `supabase/seed.sql`

Applied to files:

  • supabase/seed.sql
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/migrations/*.sql : Never edit previously committed migrations; treat new seeding in migrations as part of the current feature

Applied to files:

  • supabase/seed.sql
  • RBAC_SYSTEM.md
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/migrations/*.sql : Use Supabase CLI for every migration and operational task; avoid manual changes through dashboard or direct SQL

Applied to files:

  • supabase/seed.sql
  • AGENTS.md
  • RBAC_SYSTEM.md
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/migrations/*.sql : Only use `get_identity_org_allowed()` as a last resort when the table genuinely has no app_id column and there is no way to join to get an app_id

Applied to files:

  • supabase/seed.sql
  • supabase/functions/_backend/public/organization/put.ts
  • RBAC_SYSTEM.md
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Backend tests modify local database; always reset database with `supabase db reset` before running tests to ensure clean state

Applied to files:

  • supabase/seed.sql
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Test accounts available after `supabase db reset`: `testcapgo.app` / `testtest` (demo user) and `admincapgo.app` / `adminadmin` (admin user)

Applied to files:

  • supabase/seed.sql
📚 Learning: 2026-01-09T18:23:34.200Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-09T18:23:34.200Z
Learning: Read AGENTS.md for all development guidelines, commands, architecture details, and coding conventions for this repository

Applied to files:

  • AGENTS.md
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: ALL sections in a PR created by AI agents MUST be marked with '(AI generated)' - failure to mark is a violation of transparency requirements

Applied to files:

  • AGENTS.md
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/migrations/**/*.sql : Database migrations must be created with `supabase migration new <feature_slug>` and never modify previously committed migrations

Applied to files:

  • AGENTS.md
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to src/**/*.{vue,ts,js} : Frontend ESLint must pass before commit; run `bun lint:fix` to auto-fix issues in frontend files

Applied to files:

  • AGENTS.md
🧬 Code graph analysis (11)
supabase/functions/_backend/public/build/request.ts (1)
supabase/functions/_backend/utils/rbac.ts (1)
  • checkPermission (240-313)
supabase/functions/_backend/triggers/on_user_delete.ts (3)
supabase/functions/_backend/utils/logging.ts (1)
  • cloudlog (3-15)
supabase/functions/_backend/utils/postgres_schema.ts (1)
  • orgs (95-104)
supabase/functions/_backend/utils/hono.ts (1)
  • BRES (156-156)
supabase/functions/_backend/public/organization/delete.ts (3)
supabase/functions/_backend/utils/hono.ts (1)
  • MiddlewareKeyVariables (27-44)
supabase/functions/_backend/utils/logging.ts (1)
  • cloudlog (3-15)
supabase/functions/_backend/utils/rbac.ts (1)
  • checkPermission (240-313)
supabase/functions/_backend/public/statistics/index.ts (3)
supabase/functions/_backend/utils/rbac.ts (1)
  • checkPermission (240-313)
supabase/functions/_backend/utils/hono.ts (1)
  • quickError (245-258)
supabase/functions/_backend/utils/logging.ts (1)
  • cloudlogErr (29-41)
supabase/functions/_backend/private/role_bindings.ts (7)
supabase/functions/_backend/private/groups.ts (1)
  • app (26-26)
supabase/functions/_backend/utils/hono.ts (2)
  • createHono (158-217)
  • MiddlewareKeyVariables (27-44)
supabase/functions/_backend/utils/version.ts (1)
  • version (1-1)
supabase/functions/_backend/utils/pg.ts (3)
  • getDrizzleClient (220-222)
  • getPgClient (187-218)
  • closeClient (275-280)
supabase/functions/_backend/utils/postgres_schema.ts (1)
  • schema (210-225)
supabase/functions/_backend/utils/rbac.ts (1)
  • checkPermission (240-313)
supabase/functions/_backend/utils/logging.ts (2)
  • cloudlog (3-15)
  • cloudlogErr (29-41)
supabase/functions/_backend/utils/hono_middleware.ts (3)
supabase/functions/_backend/utils/logging.ts (1)
  • cloudlog (3-15)
supabase/functions/_backend/utils/hono.ts (1)
  • honoFactory (52-52)
supabase/functions/_backend/utils/supabase.ts (2)
  • checkKey (1132-1166)
  • checkKeyById (1172-1192)
supabase/functions/_backend/public/organization/put.ts (4)
supabase/functions/_backend/utils/hono.ts (2)
  • parseBody (272-283)
  • MiddlewareKeyVariables (27-44)
src/types/supabase.types.ts (1)
  • Database (9-3209)
supabase/functions/_backend/utils/supabase.ts (2)
  • supabaseApikey (86-103)
  • apikeyHasOrgRightWithPolicy (267-285)
supabase/functions/_backend/utils/rbac.ts (1)
  • checkPermission (240-313)
supabase/functions/_backend/private/delete_failed_version.ts (3)
supabase/functions/_backend/utils/supabase.ts (1)
  • supabaseApikey (86-103)
supabase/functions/_backend/utils/hono.ts (1)
  • quickError (245-258)
supabase/functions/_backend/utils/rbac.ts (1)
  • checkPermission (240-313)
supabase/functions/_backend/public/organization/get.ts (5)
supabase/functions/_backend/utils/hono.ts (3)
  • parseBody (272-283)
  • simpleError (268-270)
  • MiddlewareKeyVariables (27-44)
supabase/functions/_backend/utils/supabase.ts (1)
  • supabaseApikey (86-103)
supabase/functions/_backend/utils/rbac.ts (1)
  • checkPermission (240-313)
supabase/functions/_backend/utils/utils.ts (1)
  • fetchLimit (13-13)
scripts/snippet/cloudflare-snippet-filter-appid.js (1)
  • body (65-65)
supabase/functions/_backend/public/build/upload.ts (1)
supabase/functions/_backend/utils/rbac.ts (1)
  • checkPermission (240-313)
supabase/functions/_backend/utils/org_email_notifications.ts (2)
supabase/functions/_backend/utils/logging.ts (1)
  • cloudlog (3-15)
supabase/functions/_backend/utils/postgres_schema.ts (1)
  • users (106-119)
🪛 LanguageTool
RBAC_SYSTEM.md

[style] ~802-~802: This adverb was used twice in the sentence. Consider removing one of them or replacing them with a synonym.
Context: ...that bundle - Downward propagation only (no upward propagation) 7. *Return...

(ADVERB_REPETITION_PREMIUM)

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/types/supabase.types.ts (1)

3946-4010: ALLOWED_STATS_ACTIONS is missing three enum values, creating a data consistency issue.
The enum in src/types/supabase.types.ts includes backend_refusal, disablePlatformElectron, and keyMismatch, but ALLOWED_STATS_ACTIONS in supabase/functions/_backend/plugins/stats_actions.ts excludes them. These values are actively used in supabase/functions/_backend/utils/update.ts via sendStatsAndDevice(), but external API requests will reject them due to the incomplete validation. Update ALLOWED_STATS_ACTIONS to include all three missing values.

🤖 Fix all issues with AI agents
In `@supabase/functions/_backend/public/statistics/index.ts`:
- Around line 558-561: The quickError calls in the authorization checks (see the
checkPermission call and subsequent throw quickError usage) are currently
including c.get('auth') in the 401 payload; remove that sensitive object from
the error payloads (do not pass c.get('auth') into quickError) and instead
either omit the data field entirely or include only non-sensitive identifiers
(e.g., c.get('auth')?.userId or null). Update every occurrence of throw
quickError(...) that passes c.get('auth') (including the similar block around
checkPermission at the later lines) to use a safe/empty payload.
♻️ Duplicate comments (3)
RBAC_SYSTEM.md (1)

427-437: Clarify “except billing and deletions” for org_admin (channels are deletable per tables).

Right now “except billing and deletions” (Line 435) can be read as “cannot delete channels”, but org_admin includes channel.delete in the permissions tables. Consider tightening wording to “except billing changes and app deletion” (or similar).

src/types/supabase.types.ts (1)

2991-3057: get_orgs_v7.required_encryption_key typed non-null—confirm SQL guarantees it.
orgs.required_encryption_key is string | null, so get_orgs_v7 returning string is only safe if the SQL uses something like COALESCE(...) or filters to non-null.

#!/bin/bash
set -euo pipefail

# Confirm get_orgs_v7 selects/COALESCEs required_encryption_key and check its GRANTs
rg -n --hidden -S '\bget_orgs_v7\b' supabase/migrations
rg -n --hidden -S '\brequired_encryption_key\b' supabase/migrations
rg -n --hidden -S '\bGRANT\b.*\bget_orgs_v7\b' supabase/migrations
supabase/functions/_backend/utils/supabase.types.ts (1)

2991-3057: get_orgs_v7.required_encryption_key non-null typing: confirm SQL guarantees it.
Avoid backend code treating the encryption key as always present unless the query enforces that invariant.

#!/bin/bash
set -euo pipefail

rg -n --hidden -S '\bget_orgs_v7\b|\brequired_encryption_key\b' supabase/migrations
🧹 Nitpick comments (2)
RBAC_SYSTEM.md (2)

223-246: Consider stronger integrity between org_id and referenced app_id/channel_id/bundle_id.

role_bindings requires org_id for app/channel/bundle scopes, but nothing here prevents mismatches (e.g., an app_id belonging to a different org than org_id). If not already enforced elsewhere, consider a trigger (or validation in the write-path) to keep these consistent.

Also applies to: 258-289


1461-1477: Async watchEffect example can race; consider showing an invalidation-safe pattern.

As written, rapid scope changes can interleave async results and overwrite newer values. If you want the example to be robust, add onCleanup cancellation or use a watch on scope keys with a request counter.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 97622c3 and 350c0ca.

📒 Files selected for processing (5)
  • RBAC_SYSTEM.md
  • src/types/supabase.types.ts
  • supabase/functions/_backend/private/groups.ts
  • supabase/functions/_backend/public/statistics/index.ts
  • supabase/functions/_backend/utils/supabase.types.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • supabase/functions/_backend/private/groups.ts
🧰 Additional context used
📓 Path-based instructions (10)
supabase/functions/_backend/**/*.{ts,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

supabase/functions/_backend/**/*.{ts,js}: Backend code must be placed in supabase/functions/_backend/ as shared code deployed to Cloudflare Workers (API/Plugin/Files workers), Supabase Edge Functions, and other platforms
Use createHono from utils/hono.ts for all Hono framework application initialization and routing
All database operations must use getPgClient() or getDrizzleClient() from utils/pg.ts for PostgreSQL access during active migration to Cloudflare D1
All Hono endpoint handlers must accept Context<MiddlewareKeyVariables> and use c.get('requestId'), c.get('apikey'), and c.get('auth') for request context
Use structured logging with cloudlog({ requestId: c.get('requestId'), message: '...' }) for all backend logging
Use middlewareAPISecret for internal API endpoints and middlewareKey for external API keys; validate against owner_org in the apikeys table
Check c.get('auth')?.authType to determine authentication type ('apikey' vs 'jwt') in backend endpoints
Use Drizzle ORM query patterns with schema from postgress_schema.ts for all database operations; use aliasV2() for self-joins or multiple table references

Files:

  • supabase/functions/_backend/public/statistics/index.ts
  • supabase/functions/_backend/utils/supabase.types.ts
supabase/functions/**/*.{ts,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Backend ESLint must pass before commit; run bun lint:backend for backend files

Files:

  • supabase/functions/_backend/public/statistics/index.ts
  • supabase/functions/_backend/utils/supabase.types.ts
**/*.{vue,ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Run bun lint to lint Vue, TypeScript, and JavaScript files; use bun lint:fix to auto-fix issues

Files:

  • supabase/functions/_backend/public/statistics/index.ts
  • src/types/supabase.types.ts
  • supabase/functions/_backend/utils/supabase.types.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use single quotes and no semicolons per @antfu/eslint-config rules

Files:

  • supabase/functions/_backend/public/statistics/index.ts
  • src/types/supabase.types.ts
  • supabase/functions/_backend/utils/supabase.types.ts
supabase/functions/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

supabase/functions/**/*.ts: Never use the Supabase admin SDK with service key for user-facing APIs; always use client SDK with user authentication to enforce RLS policies. Admin SDK should only be used for internal operations (triggers, CRON jobs, etc.)
When admin access is unavoidable for a user-facing endpoint, sanitize all user inputs carefully to prevent PostgREST query injection

Files:

  • supabase/functions/_backend/public/statistics/index.ts
  • supabase/functions/_backend/utils/supabase.types.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Run bun lint or lint/format command before validating any backend or frontend task to ensure consistent formatting

Files:

  • supabase/functions/_backend/public/statistics/index.ts
  • src/types/supabase.types.ts
  • supabase/functions/_backend/utils/supabase.types.ts
supabase/functions/_backend/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Place core backend logic in supabase/functions/_backend/ with plugins, private, public, triggers, and utils subdirectories

Files:

  • supabase/functions/_backend/public/statistics/index.ts
  • supabase/functions/_backend/utils/supabase.types.ts
src/**/*.{ts,tsx,vue,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use ~/ alias for imports from src/ directory in frontend TypeScript and Vue components

Files:

  • src/types/supabase.types.ts
src/**/*.{vue,ts,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Frontend ESLint must pass before commit; run bun lint:fix to auto-fix issues in frontend files

Files:

  • src/types/supabase.types.ts
src/**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Use path alias ~/ to map to src/ for imports in TypeScript and JavaScript files

Files:

  • src/types/supabase.types.ts
🧠 Learnings (21)
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Check `c.get('auth')?.authType` to determine authentication type ('apikey' vs 'jwt') in backend endpoints

Applied to files:

  • supabase/functions/_backend/public/statistics/index.ts
  • RBAC_SYSTEM.md
  • src/types/supabase.types.ts
  • supabase/functions/_backend/utils/supabase.types.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use `middlewareAPISecret` for internal API endpoints and `middlewareKey` for external API keys; validate against `owner_org` in the `apikeys` table

Applied to files:

  • supabase/functions/_backend/public/statistics/index.ts
  • src/types/supabase.types.ts
  • supabase/functions/_backend/utils/supabase.types.ts
📚 Learning: 2025-12-23T01:19:15.067Z
Learnt from: riderx
Repo: Cap-go/capgo PR: 1297
File: src/components/dashboard/DeploymentBanner.vue:77-79
Timestamp: 2025-12-23T01:19:15.067Z
Learning: In the Cap-go codebase, the 'owner' role exists only at the organization level (used in src/stores/organization.ts), not as an app-level permission. App-level permissions use the user_min_right database enum which includes: read, upload, write, admin, super_admin (but NOT owner). When checking app permissions, never include 'owner' in the role checks.

Applied to files:

  • supabase/functions/_backend/public/statistics/index.ts
  • RBAC_SYSTEM.md
  • src/types/supabase.types.ts
  • supabase/functions/_backend/utils/supabase.types.ts
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/functions/_backend/**/*.ts : Place core backend logic in supabase/functions/_backend/ with plugins, private, public, triggers, and utils subdirectories

Applied to files:

  • supabase/functions/_backend/public/statistics/index.ts
  • src/types/supabase.types.ts
  • supabase/functions/_backend/utils/supabase.types.ts
📚 Learning: 2025-12-24T14:11:10.256Z
Learnt from: WcaleNieWolny
Repo: Cap-go/capgo PR: 1300
File: supabase/migrations/20251224103713_2fa_enforcement.sql:409-539
Timestamp: 2025-12-24T14:11:10.256Z
Learning: In supabase/migrations for get_orgs_v6 and get_orgs_v7: The inner functions with user_id parameter (get_orgs_v6(uuid) and get_orgs_v7(uuid)) should NOT be granted to anon/authenticated roles as this allows any user to query other users' organizations; only the no-argument wrapper functions should be public as they perform authentication checks.

Applied to files:

  • supabase/functions/_backend/public/statistics/index.ts
  • RBAC_SYSTEM.md
  • src/types/supabase.types.ts
  • supabase/functions/_backend/utils/supabase.types.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use `createHono` from `utils/hono.ts` for all Hono framework application initialization and routing

Applied to files:

  • supabase/functions/_backend/public/statistics/index.ts
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/functions/**/*.ts : Never use the Supabase admin SDK with service key for user-facing APIs; always use client SDK with user authentication to enforce RLS policies. Admin SDK should only be used for internal operations (triggers, CRON jobs, etc.)

Applied to files:

  • supabase/functions/_backend/public/statistics/index.ts
  • RBAC_SYSTEM.md
  • supabase/functions/_backend/utils/supabase.types.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use structured logging with `cloudlog({ requestId: c.get('requestId'), message: '...' })` for all backend logging

Applied to files:

  • supabase/functions/_backend/public/statistics/index.ts
  • RBAC_SYSTEM.md
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : All Hono endpoint handlers must accept `Context<MiddlewareKeyVariables>` and use `c.get('requestId')`, `c.get('apikey')`, and `c.get('auth')` for request context

Applied to files:

  • supabase/functions/_backend/public/statistics/index.ts
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/functions/**/*.ts : When admin access is unavoidable for a user-facing endpoint, sanitize all user inputs carefully to prevent PostgREST query injection

Applied to files:

  • RBAC_SYSTEM.md
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/migrations/*.sql : Use `get_identity_org_appid()` for RLS policies when app_id exists on the table instead of `get_identity()` directly

Applied to files:

  • RBAC_SYSTEM.md
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : All database operations must use `getPgClient()` or `getDrizzleClient()` from `utils/pg.ts` for PostgreSQL access during active migration to Cloudflare D1

Applied to files:

  • RBAC_SYSTEM.md
  • supabase/functions/_backend/utils/supabase.types.ts
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/migrations/*.sql : Only use `get_identity_org_allowed()` as a last resort when the table genuinely has no app_id column and there is no way to join to get an app_id

Applied to files:

  • RBAC_SYSTEM.md
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/migrations/*.sql : Use Supabase CLI for every migration and operational task; avoid manual changes through dashboard or direct SQL

Applied to files:

  • RBAC_SYSTEM.md
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/utils/postgress_schema.ts : Schema definitions must be placed in `utils/postgress_schema.ts` using Drizzle ORM and never edited in committed migration files

Applied to files:

  • RBAC_SYSTEM.md
  • supabase/functions/_backend/utils/supabase.types.ts
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/migrations/*.sql : Never edit previously committed migrations; treat new seeding in migrations as part of the current feature

Applied to files:

  • RBAC_SYSTEM.md
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/seed.sql : Update supabase/seed.sql for new or evolved tests; keep fixtures focused on current behavior while leaving committed migrations unchanged

Applied to files:

  • RBAC_SYSTEM.md
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use Drizzle ORM query patterns with `schema` from `postgress_schema.ts` for all database operations; use `aliasV2()` for self-joins or multiple table references

Applied to files:

  • RBAC_SYSTEM.md
  • src/types/supabase.types.ts
  • supabase/functions/_backend/utils/supabase.types.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Backend code must be placed in `supabase/functions/_backend/` as shared code deployed to Cloudflare Workers (API/Plugin/Files workers), Supabase Edge Functions, and other platforms

Applied to files:

  • supabase/functions/_backend/utils/supabase.types.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Run `bun types` after schema changes to regenerate TypeScript types from Supabase

Applied to files:

  • supabase/functions/_backend/utils/supabase.types.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Use shared backend code from `supabase/functions/_backend/` across all deployment platforms; never create platform-specific implementations outside this directory

Applied to files:

  • supabase/functions/_backend/utils/supabase.types.ts
🧬 Code graph analysis (2)
supabase/functions/_backend/public/statistics/index.ts (1)
supabase/functions/_backend/utils/rbac.ts (1)
  • checkPermission (240-313)
supabase/functions/_backend/utils/supabase.types.ts (1)
src/types/supabase.types.ts (1)
  • Database (9-3807)
🪛 LanguageTool
RBAC_SYSTEM.md

[style] ~802-~802: This adverb was used twice in the sentence. Consider removing one of them or replacing them with a synonym.
Context: ...that bundle - Downward propagation only (no upward propagation) 7. *Return...

(ADVERB_REPETITION_PREMIUM)

🔇 Additional comments (20)
RBAC_SYSTEM.md (2)

154-170: Double-check whether public.permissions.bundle_id is actually intended (and matches migrations).

Having bundle_id on the permission definition table is atypical (permissions are usually global; scoping happens via role_bindings + check inputs). If the real schema doesn’t include it (or it’s always NULL), consider removing it from the doc or adding a one-liner explaining why it exists.


825-842: Good: explicit “public wrapper / private inner function” + GRANT guidance.

This is the right pattern to prevent user-id probing while keeping the client RPC ergonomic.

Also applies to: 971-999

supabase/functions/_backend/public/statistics/index.ts (3)

9-10: Nice simplification: funneling access checks through checkPermission.


586-606: org.read gate + apikey limited_to_* validation looks consistent.


667-706: No security issues found—get_user_org_ids() correctly handles both JWT and apikey authentication.

The function is implemented as a SECURITY DEFINER wrapper that explicitly checks for API keys first (validating against the apikeys table and using the associated user_id), then falls back to public.get_identity() for JWT authentication. This dual-path design safely supports both authentication methods without relying solely on auth.uid().

Additionally, the security pattern from prior learnings is correctly applied: the inner variants get_orgs_v6(uuid) and get_orgs_v7(uuid) (which accept a user_id parameter) are revoked from anon and authenticated roles and only granted to postgres and service_role, while the no-argument wrapper functions are properly exposed to authenticated users.

src/types/supabase.types.ts (12)

9-34: Good addition of graphql_public schema typing; keep it in sync with generated constants.
No concerns with the structure (Tables/Views/Functions/Enums/CompositeTypes) and the empty Constants.graphql_public.Enums placeholder.

Also applies to: 3926-3930


1226-1299: New groups / group_members tables look coherent (no surrogate id for membership is fine).
Relationships to groups.id and users.id are consistent with the intended RBAC model.


1435-1515: No action needed—orgs.use_new_rbac has a database DEFAULT.

The column is added in migration 20251222140030_rbac_system.sql line 527 with DEFAULT false, so new org creation and updates do not require the field to be explicitly provided. The type definition is correct: Row requires it (always present with default), Insert makes it optional (clients omit it, DB uses default). A per-org + global feature flag rollout strategy is already implemented via rbac_is_enabled_for_org().


1608-1628: rbac_settings table is intentionally global (singleton)—TypeScript types are correct.
The migration confirms this via the CHECK (id = 1) constraint and explicit comment: "Singleton row to flip RBAC on globally without touching org records." The per-org footgun risk is mitigated by the OR logic (COALESCE(v_org_enabled, false) OR COALESCE(v_global_enabled, false)), which allows each org to independently enable RBAC regardless of the global flag.


2633-2652: RPC type signatures verified as correct. The TypeScript types for count_non_compliant_bundles and delete_non_compliant_bundles accurately match their SQL function definitions. Args are properly mapped (uuid → string, text DEFAULT NULL → optional string), return types are correct (TABLE fields map to object properties, bigint → number), and the callable signature includes proper authorization checks in the SQL implementation.


621-687: channels.rbac_id is properly constrained—NOT NULL and UNIQUE constraints confirmed in schema.

The database migration (20251222140030_rbac_system.sql) establishes channels.rbac_id as NOT NULL with a UNIQUE constraint (channels_rbac_id_key) and default gen_random_uuid(). The foreign key in role_bindings.channel_id correctly references channels(rbac_id). The TypeScript types accurately reflect this: Row requires rbac_id, while Insert marks it optional (since the database provides the default).


37-79: No action needed. The database schema for apikeys.rbac_id is properly configured with a DEFAULT gen_random_uuid() and a NOT NULL constraint (added in migration 20251222140030_rbac_system.sql with backfill for existing rows). The TypeScript typing correctly reflects this: Row.rbac_id: string (required, because DB enforces NOT NULL) and Insert.rbac_id?: string (optional, because DB provides DEFAULT). The typing is safe.

Likely an incorrect or invalid review comment.


1516-1550: No changes needed—scope_type is a text column with constraints, not a Postgres enum.

The permissions.scope_type column is defined in the migration as text NOT NULL CHECK (scope_type IN (...)), not as a Postgres enum type. The current string typing is correct and appropriate for this text column with a CHECK constraint.

Likely an incorrect or invalid review comment.


2546-2575: No action needed — check_min_rights_legacy is not granted to any roles and is only called internally from wrapper functions, confirming proper security architecture.

Likely an incorrect or invalid review comment.


1629-1715: FK design is intentional and properly constrained—no changes needed.

The role_bindings.channel_idchannels(rbac_id reference is a deliberate pattern: rbac_id is a stable, immutable UUID generated for RBAC scope binding, separate from the channel's business identifier. The rbac_id column is properly constrained as UNIQUE NOT NULL with the comment "Stable UUID to bind RBAC roles to channel scope." This is a sound architectural choice for polymorphic RBAC systems.


3134-3139: get_user_org_ids() is correctly implemented with proper authentication and scoping; no security issues found.

The function is a no-arg, authenticated-only function that properly scopes results to the current user by checking API keys first, then falling back to get_identity() (auth.uid()). It raises an exception if unauthenticated. The SECURITY DEFINER usage is intentional and correct—it's necessary to bypass RLS and directly query role_bindings and org_users tables while still filtering by the authenticated user's ID. The function accepts no parameters, preventing cross-user data leakage, and is granted only to the "authenticated" role, not "anon".

Likely an incorrect or invalid review comment.


3286-3293: TypeScript types document is_user_app_admin and is_user_org_admin as public RPC callables, but these functions lack explicit GRANTs to any role—creating a mismatch between the type definitions and actual runtime accessibility.

Both is_user_app_admin(p_user_id, p_app_id) and is_user_org_admin(p_user_id, p_org_id) are defined as SECURITY DEFINER helper functions at lines 2870 and 2892 in the migrations and are used internally within RLS policies (always called with auth.uid()). No explicit GRANT statements exist for these functions to authenticated or anon roles.

The core concern is valid: these parameterized functions should never be directly callable by users to avoid privilege escalation. However, the solution is to either:

  • Remove them from the TypeScript RPC type definitions if they are internal-only helpers, or
  • Create zero-argument wrapper functions and GRANT those instead

Ensure the TypeScript types in src/types/supabase.types.ts (lines 3286–3293) accurately reflect only functions that are explicitly granted for public use.

supabase/functions/_backend/utils/supabase.types.ts (3)

9-34: graphql_public schema typing addition is fine; keep generation source consistent with frontend types.


37-79: No action needed—database defaults and backfills are in place.

Migration 20251222140030_rbac_system.sql properly defines rbac_id with DEFAULT gen_random_uuid(), backfills existing rows, and sets the column NOT NULL. Similarly, use_new_rbac columns in both orgs and rbac_settings tables have NOT NULL DEFAULT false. The TypeScript types correctly reflect these constraints: rbac_id and use_new_rbac are non-optional in Row (reading) but optional in Insert (writing), matching the database behavior.

Likely an incorrect or invalid review comment.


2546-2575: No security issues found—RPC GRANTs are properly least-privilege.

The functions mentioned are either not granted to public roles (check_min_rights_legacy, user_has_role_in_app, user_has_app_update_user_roles default to private) or have explicit REVOKE statements in place. Specifically:

  • get_orgs_v6(uuid) and get_orgs_v7(uuid): REVOKE ALL from anon/authenticated; GRANT EXECUTE only to postgres and service_role
  • check_min_rights_legacy, user_has_role_in_app, user_has_app_update_user_roles: No explicit GRANTs issued (default deny)
  • rbac_check_permission: GRANTED to authenticated without caller-controlled user_id parameter
  • rbac_check_permission_direct: GRANTED to service_role only

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@sonarqubecloud
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants