Skip to content

feat: enforce default channel validation (max 3, one per platform)#1387

Closed
Copilot wants to merge 5 commits intofeat/add-electron-platformfrom
copilot/sub-pr-1383-again
Closed

feat: enforce default channel validation (max 3, one per platform)#1387
Copilot wants to merge 5 commits intofeat/add-electron-platformfrom
copilot/sub-pr-1383-again

Conversation

Copy link
Contributor

Copilot AI commented Jan 8, 2026

Summary

Implements backend validation to prevent public channel conflicts: maximum 3 public channels per app, and only one public channel per platform (iOS/Android/Electron). Users can now have either one universal default channel OR up to three platform-specific channels without ambiguity.

Implementation:

  • Validation logic (utils/supabase.ts): validatePublicChannels() runs pre-upsert, checks channel count and platform uniqueness, throws typed errors (max_public_channels, duplicate_platform_{ios|android|electron})
  • Channel API (public/channel/post.ts): Added electron field support to match iOS/Android pattern
  • Test coverage (channel_default_validation.test.ts): Valid configs (single multi-platform, three single-platform, mixed), invalid configs (4+ channels, platform duplicates), update scenarios
  • Documentation (docs/DEFAULT_CHANNEL_VALIDATION.md): Rules reference, error messages, migration guidance

Example validation:

// ✓ Valid: One universal channel
{ public: true, ios: true, android: true, electron: true }

// ✓ Valid: Three platform-specific
[
  { public: true, ios: true, android: false, electron: false },
  { public: true, ios: false, android: true, electron: false },
  { public: true, ios: false, android: false, electron: true }
]

// ✗ Invalid: Duplicate iOS in public channels
[
  { public: true, ios: true, ... },
  { public: true, ios: true, ... }  // throws duplicate_platform_ios
]

Private channels unaffected. Validation uses final channel state (merges update with existing values).

Test plan

  1. Run bun test:backend to verify validation test suite
  2. Create app with 3 public channels (different platforms) → succeeds
  3. Attempt 4th public channel → receives max_public_channels error
  4. Create 2 public channels with same platform → receives duplicate_platform_* error
  5. Update existing public channel to conflict → validation prevents

Screenshots

N/A - Backend validation only

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

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI self-assigned this Jan 8, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 8, 2026

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


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

Copilot AI and others added 4 commits January 8, 2026 04:17
Co-authored-by: riderx <4084527+riderx@users.noreply.github.com>
Co-authored-by: riderx <4084527+riderx@users.noreply.github.com>
Co-authored-by: riderx <4084527+riderx@users.noreply.github.com>
Co-authored-by: riderx <4084527+riderx@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 8, 2026

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • 127.0.0.11
    • Triggering command: REDACTED, pid is -1 (packet block)
  • https://api.github.com/repos/supabase/cli/releases/latest
    • Triggering command: /home/REDACTED/.npm/_npx/aa8e5c70f9d8d161/node_modules/.bin/supabase supabase --version (http block)
  • npm.jsr.io
    • Triggering command: /usr/local/bin/node node /usr/local/bin/npm install (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI changed the title [WIP] Add Electron platform support for channels feat: enforce default channel validation (max 3, one per platform) Jan 8, 2026
Copilot AI requested a review from riderx January 8, 2026 04:26
@sonarqubecloud
Copy link

sonarqubecloud bot commented Jan 8, 2026

@riderx riderx marked this pull request as ready for review January 22, 2026 01:42
Copilot AI review requested due to automatic review settings January 22, 2026 01:42
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e78e10f5c8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +258 to +261
const finalPublic = update.public !== undefined ? update.public : (existingChannel?.public ?? DEFAULT_CHANNEL_PUBLIC)
const finalIos = update.ios !== undefined ? update.ios : (existingChannel?.ios ?? DEFAULT_CHANNEL_IOS)
const finalAndroid = update.android !== undefined ? update.android : (existingChannel?.android ?? DEFAULT_CHANNEL_ANDROID)
const finalElectron = update.electron !== undefined ? update.electron : (existingChannel?.electron ?? DEFAULT_CHANNEL_ELECTRON)

Choose a reason for hiding this comment

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

P2 Badge Use existing channel state when public omitted

The validation computes finalPublic/platform flags from existingChannel, but existingChannel is only fetched for the same created_by (see the query just above this block). If a teammate with app write rights updates a channel they didn’t create and omits public (common for partial updates), existingChannel is null, finalPublic falls back to false, and validatePublicChannels() is skipped. That allows updates like setting ios: true on a public channel without triggering duplicate-platform checks, so conflicting public channels can be created in collaborative apps. Consider fetching the existing channel by app_id+name regardless of creator (or otherwise ensuring finalPublic reflects the actual current row) so validation runs on all updates.

Useful? React with 👍 / 👎.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements backend validation rules for public (default) channels to prevent configuration conflicts and ensure proper platform distribution. The validation enforces a maximum of 3 public channels per app and restricts each platform (iOS/Android/Electron) to only one public channel.

Changes:

  • Added validatePublicChannels() function with comprehensive pre-upsert validation logic
  • Extended channel API to support electron field alongside existing iOS/Android fields
  • Created extensive test suite covering valid/invalid configurations, updates, and edge cases
  • Documented validation rules, error messages, API usage, and migration considerations

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
supabase/functions/_backend/utils/supabase.ts Core validation logic checking max channels and platform uniqueness, integrated into updateOrCreateChannel
supabase/functions/_backend/public/channel/post.ts Added electron field to channel creation API interface and payload
tests/channel_default_validation.test.ts Comprehensive test coverage for all validation scenarios including valid configs, limit violations, platform conflicts, and private channel independence
docs/DEFAULT_CHANNEL_VALIDATION.md Complete documentation of validation rules, examples, error messages, and migration notes
Comments suppressed due to low confidence (1)

supabase/functions/_backend/utils/supabase.ts:276

  • The upsert uses onConflict on 'app_id, name', but there's no visible unique constraint on these columns in the database schema. The code also checks for existing channels with app_id, name, AND created_by (line 234-236), which suggests channels might be unique per (app_id, name, created_by). If the database doesn't have a unique constraint on (app_id, name), the upsert may not work as expected and could create duplicate channels with the same name. Verify that a unique constraint exists on channels(app_id, name) in the database.
  return supabaseAdmin(c)
    .from('channels')
    .upsert(update, { onConflict: 'app_id, name' })
    .throwOnError()

Comment on lines +136 to +222
async function validatePublicChannels(
c: Context,
appId: string,
channelName: string,
isPublic: boolean,
ios: boolean,
android: boolean,
electron: boolean,
) {
// Only validate if the channel is being set to public
if (!isPublic) {
return
}

// Get all existing public channels for this app (excluding the current channel being updated)
const { data: publicChannels, error } = await supabaseAdmin(c)
.from('channels')
.select('id, name, ios, android, electron')
.eq('app_id', appId)
.eq('public', true)
.neq('name', channelName)

if (error) {
cloudlogErr({ requestId: c.get('requestId'), message: 'Error fetching public channels', error })
throw simpleError('db_error', 'Failed to validate public channels')
}

const existingPublicChannels = publicChannels || []

// Rule 1: Maximum 3 public channels per app
if (existingPublicChannels.length >= 3) {
cloudlogErr({
requestId: c.get('requestId'),
message: 'Maximum 3 public channels allowed per app',
appId,
existingCount: existingPublicChannels.length,
})
throw simpleError(
'max_public_channels',
'Maximum 3 public channels allowed per app. You can have one default channel for all platforms or up to three (one per platform: iOS, Android, Electron).',
)
}

// Rule 2: Maximum 1 public channel per platform
// Check for platform conflicts
for (const existingChannel of existingPublicChannels) {
if (ios && existingChannel.ios) {
cloudlogErr({
requestId: c.get('requestId'),
message: 'Platform conflict: iOS',
appId,
existingChannel: existingChannel.name,
newChannel: channelName,
})
throw simpleError(
'duplicate_platform_ios',
`Another public channel "${existingChannel.name}" already supports iOS platform. Only one public channel per platform is allowed.`,
)
}
if (android && existingChannel.android) {
cloudlogErr({
requestId: c.get('requestId'),
message: 'Platform conflict: Android',
appId,
existingChannel: existingChannel.name,
newChannel: channelName,
})
throw simpleError(
'duplicate_platform_android',
`Another public channel "${existingChannel.name}" already supports Android platform. Only one public channel per platform is allowed.`,
)
}
if (electron && existingChannel.electron) {
cloudlogErr({
requestId: c.get('requestId'),
message: 'Platform conflict: Electron',
appId,
existingChannel: existingChannel.name,
newChannel: channelName,
})
throw simpleError(
'duplicate_platform_electron',
`Another public channel "${existingChannel.name}" already supports Electron platform. Only one public channel per platform is allowed.`,
)
}
}
}
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

The validation logic does not prevent creating a public channel with all platforms disabled (ios=false, android=false, electron=false). Such a channel would be unusable by any device but would still count against the 3-channel limit and wouldn't trigger any validation errors. Consider adding validation to ensure at least one platform is enabled for public channels.

Copilot uses AI. Check for mistakes.
Comment on lines +412 to +447
describe('Private channels should not be affected', () => {
it('should allow multiple private channels regardless of platform', async () => {
const id9 = randomUUID()
const APPNAME9 = `com.app.private.${id9}`
await resetAndSeedAppData(APPNAME9)

// Create multiple private channels with same platforms - should all succeed
for (let i = 0; i < 5; i++) {
const response = await fetch(`${BASE_URL}/channel`, {
method: 'POST',
headers,
body: JSON.stringify({
app_id: APPNAME9,
channel: `private_channel_${i}`,
public: false,
ios: true,
android: true,
electron: true,
}),
})
expect(response.status).toBe(200)
}

// Verify all 5+ private channels exist (including seeded ones)
const { data: channels } = await getSupabaseClient()
.from('channels')
.select('*')
.eq('app_id', APPNAME9)
.eq('public', false)

expect(channels?.length).toBeGreaterThanOrEqual(5)

await resetAppData(APPNAME9)
})
})
})
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

Missing test coverage for converting a private channel to public when it would violate validation rules. Consider adding a test that creates 3 public channels, then creates a 4th private channel, and attempts to update that private channel to public (which should fail with max_public_channels error).

Copilot uses AI. Check for mistakes.
Comment on lines +150 to +177
// Get all existing public channels for this app (excluding the current channel being updated)
const { data: publicChannels, error } = await supabaseAdmin(c)
.from('channels')
.select('id, name, ios, android, electron')
.eq('app_id', appId)
.eq('public', true)
.neq('name', channelName)

if (error) {
cloudlogErr({ requestId: c.get('requestId'), message: 'Error fetching public channels', error })
throw simpleError('db_error', 'Failed to validate public channels')
}

const existingPublicChannels = publicChannels || []

// Rule 1: Maximum 3 public channels per app
if (existingPublicChannels.length >= 3) {
cloudlogErr({
requestId: c.get('requestId'),
message: 'Maximum 3 public channels allowed per app',
appId,
existingCount: existingPublicChannels.length,
})
throw simpleError(
'max_public_channels',
'Maximum 3 public channels allowed per app. You can have one default channel for all platforms or up to three (one per platform: iOS, Android, Electron).',
)
}
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

Potential race condition: If two concurrent requests attempt to create public channels that would together violate the validation rules, both could pass validation (each seeing only 2 existing channels) and both get created, resulting in 4 public channels. Consider adding a database-level constraint (unique partial index or check constraint) to enforce these rules at the database level, or using transaction-level row locking.

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,447 @@
import { randomUUID } from 'node:crypto'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { BASE_URL, headers, resetAndSeedAppData, resetAppData, getSupabaseClient, ORG_ID, USER_ID } from './test-utils.ts'
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

Unused imports ORG_ID, USER_ID.

Suggested change
import { BASE_URL, headers, resetAndSeedAppData, resetAppData, getSupabaseClient, ORG_ID, USER_ID } from './test-utils.ts'
import { BASE_URL, headers, resetAndSeedAppData, resetAppData, getSupabaseClient } from './test-utils.ts'

Copilot uses AI. Check for mistakes.
@riderx riderx closed this Jan 25, 2026
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

Comments