Skip to content

feat: add demo app feature for non-technical onboarding#1480

Merged
riderx merged 13 commits into
mainfrom
riderx/demo-app-onboarding
Jan 22, 2026
Merged

feat: add demo app feature for non-technical onboarding#1480
riderx merged 13 commits into
mainfrom
riderx/demo-app-onboarding

Conversation

@riderx
Copy link
Copy Markdown
Member

@riderx riderx commented Jan 21, 2026

Summary

Add a demo app creation feature that allows non-technical users to explore Capgo during onboarding without needing CLI setup. Demo apps are automatically deleted after 14 days and skip onboarding emails.

Test plan

  1. Start onboarding flow as a new user
  2. On the first step, scroll to the bottom to see "Invite Teammate" option
  3. Below that, see "Create Demo App" option as the last choice
  4. Click "Create Demo App" button and verify success message
  5. Navigate to the dashboard and confirm the demo app appears with "Demo App" name
  6. Verify demo apps are deleted automatically after 14 days (via cron job at 03:00 UTC)

Screenshots

Demo app option appears last in the onboarding flow, after "Invite Teammate" option.

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.
  • My change has adequate E2E test coverage.
  • I have tested my code manually with the onboarding flow

Summary by CodeRabbit

  • New Features

    • Demo app creation option added to onboarding with loading, success, and two error states.
    • Localized demo onboarding strings added across 17 languages.
  • Behavior

    • Demo apps auto-expire and are cleaned up after 14 days.
    • Demo apps suppress onboarding emails and external notifications.
    • Dashboard navigation now forces a refresh after demo creation so demo metrics appear immediately.

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

Copilot AI review requested due to automatic review settings January 21, 2026 17:56
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 21, 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 demo-app onboarding flow: new localized strings, a frontend demo option and createDemoApp call, a serverless /app/demo endpoint that provisions demo apps (apps/versions/channels/devices/metrics/builds), DB migration to auto-clean demo apps, and trigger/stats changes to treat demo apps specially.

Changes

Cohort / File(s) Change summary
Localization
messages/*.json
messages/en.json, messages/de.json, messages/es.json, messages/fr.json, messages/hi.json, messages/id.json, messages/it.json, messages/ja.json, messages/ko.json, messages/pl.json, messages/pt-br.json, messages/ru.json, messages/tr.json, messages/vi.json, messages/zh-cn.json
Added seven onboarding-demo-* keys (option-title, option-subtitle, option-cta, loading, success, error-create, error-no-org) across locales.
Frontend — Onboarding UI
src/components/dashboard/StepsApp.vue
Added isDemoLoading, createDemoApp() async call, demo option button/UI, loading indicator and onboarding step progression/navigation on success.
Frontend — Dashboard refresh & navigation
src/components/dashboard/Usage.vue, src/pages/app/index.vue
Added forced refresh handling via ?refresh=true, cache-clear on refresh, and delayed navigation (~500ms) to ensure fresh dashboard data after demo creation.
Backend — Public API
supabase/functions/_backend/public/app/demo.ts, supabase/functions/_backend/public/app/index.ts
New /app/demo endpoint and core createDemoApp implementation that provisions demo app, versions, channels, devices, synthetic metrics, build_requests, invalidates caches, and returns app_id.
Backend — Triggers & utils
supabase/functions/_backend/triggers/on_app_create.ts, supabase/functions/_backend/triggers/on_deploy_history_create.ts, supabase/functions/_backend/triggers/on_version_create.ts, supabase/functions/_backend/utils/demo.ts
Added demo detection (is_demo flag / isAppDemo helper) and short-circuited onboarding/notification flows for demo apps; added DEMO_APP_PREFIX and isAppDemo() utility.
Database migration
supabase/migrations/20260121000000_add_demo_app_support.sql
Added public.cleanup_expired_demo_apps() to delete demo apps older than 14 days and registered a daily cron task; restricted execute to service_role.
Backend — Stats/logs
supabase/functions/_backend/utils/stats.ts
Added demo-mode synthetic log generation and branching so demo apps return fabricated stats/logs instead of live backend reads.
Minor
supabase/functions/_backend/utils/org_email_notifications.ts
Removed a blank line; no behavioral changes.

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant Frontend as StepsApp.vue
  participant API as /app/demo (serverless)
  participant DB as Database
  participant Trigger as on_app_create / on_version_create
  participant Dashboard as Dashboard (Usage)

  User->>Frontend: Click "Create demo app"
  Frontend->>API: POST /app/demo { owner_org }
  API->>DB: Insert app, versions, channels, devices, metrics, build_requests
  DB-->>API: OK { app_id }
  API->>DB: Invalidate app_metrics_cache
  API-->>Frontend: { status: success, app_id }
  Note over DB,Trigger: on_app_create fires with is_demo=true
  Trigger->>Trigger: Skip onboarding emails/store-info and notification flows
  Frontend->>Frontend: advance onboarding step
  Frontend->>Dashboard: Navigate to /app/{app_id}?refresh=true
  Dashboard->>DB: Full reload (forces fresh data)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Cap-go/capgo#1416 — Overlaps trigger handlers and demo-related trigger changes (on_app_create / on_version_create modifications).
  • Cap-go/capgo#1127 — Related changes to deploy-history/trigger handling that intersect on_deploy_history_create logic.

Suggested labels

enhancement

"I hopped in code with joyful cheer,
I spawned a demo for folks to steer.
Two weeks to play, then gone from here—
Metrics hum and buttons cheer! 🐇🎮"

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main feature being added: a demo app feature for onboarding, which aligns with the comprehensive changes across localization, UI components, backend functions, and database migrations.
Description check ✅ Passed The description covers the summary, test plan, and checklist, though it lacks screenshots (mentioned but not provided) and doesn't address documentation updates. However, core sections are present and substantive.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 SQLFluff (4.0.0)
supabase/migrations/20260121000000_add_demo_app_support.sql

User Error: No dialect was specified. You must configure a dialect or specify one on the command line using --dialect after the command. Available dialects:
ansi, athena, bigquery, clickhouse, databricks, db2, doris, duckdb, exasol, flink, greenplum, hive, impala, mariadb, materialize, mysql, oracle, postgres, redshift, snowflake, soql, sparksql, sqlite, starrocks, teradata, trino, tsql, vertica


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
Copy Markdown

@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: 79ef2119c6

ℹ️ 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 +60 to +63
app.post('/demo', middlewareKey(['all', 'write']), async (c) => {
const body = await getBodyOrQuery<CreateDemoApp>(c)
const apikey = c.get('apikey') as Database['public']['Tables']['apikeys']['Row']
return createDemoApp(c, body, apikey)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Allow JWT auth for demo app creation endpoint

The new /app/demo handler is guarded with middlewareKey, which only validates API keys, but the onboarding UI calls supabase.functions.invoke('app/demo') from the browser and supplies a user JWT (typically as Authorization: Bearer ...) rather than a capgkey. middlewareKey treats that header as an API key and will reject it, so normal users will get 401s and cannot create demo apps. Consider switching this route to middlewareV2/middlewareAuth or explicitly passing an API key from the client.

Useful? React with 👍 / 👎.

Comment on lines +37 to +38
-- Delete the app (cascades will handle related tables)
DELETE FROM public.apps WHERE app_id = app_record.app_id;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid orphaned data when deleting expired demo apps

The cleanup function deletes only from public.apps and relies on cascades, but app deletion elsewhere explicitly removes related rows (devices, stats, bandwidth/storage usage, daily_* metrics, etc.) that do not have FK cascades. Deleting demo apps directly will therefore leave orphaned records and stale metrics/storage data for expired demos. Consider reusing the same cleanup logic as deleteApp (or add the missing table deletions) to keep data consistent.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
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 adds a demo app creation feature to help non-technical users explore Capgo during onboarding without requiring CLI setup. Demo apps are automatically deleted after 14 days via a cron job.

Changes:

  • Added database schema for demo apps with is_demo flag and demo_expires_at timestamp, plus automated cleanup via cron job
  • Created backend endpoint /app/demo for creating demo apps with proper authorization checks
  • Modified on_app_create trigger to skip onboarding emails and store info for demo apps
  • Added UI option in onboarding flow to create demo apps as an alternative to CLI setup
  • Added translations for demo app feature in 11 languages

Reviewed changes

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

Show a summary per file
File Description
supabase/migrations/20260121000000_add_demo_app_support.sql Adds database columns, cleanup function, and cron task for demo app auto-deletion
supabase/functions/_backend/triggers/on_app_create.ts Detects demo apps and skips onboarding emails/store info
supabase/functions/_backend/public/app/index.ts Registers new /demo endpoint with proper middleware
supabase/functions/_backend/public/app/demo.ts Implements demo app creation logic with authorization
src/components/dashboard/StepsApp.vue Adds demo app creation button to onboarding UI
messages/*.json Adds translations for demo app UI text in 11 languages

Comment on lines +47 to +53
-- Get total count of deleted apps
SELECT COUNT(*) INTO deleted_count
FROM public.apps
WHERE is_demo = true
AND demo_expires_at IS NOT NULL
AND demo_expires_at < NOW();

Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

The migration query at lines 47-52 is attempting to count deleted apps after they have already been deleted in the loop above. This count will always return 0 because the apps were already deleted in the loop (lines 27-44). This appears to be leftover debugging code that serves no purpose. Consider removing this query.

Suggested change
-- Get total count of deleted apps
SELECT COUNT(*) INTO deleted_count
FROM public.apps
WHERE is_demo = true
AND demo_expires_at IS NOT NULL
AND demo_expires_at < NOW();

Copilot uses AI. Check for mistakes.

const { data: appData, error: appError } = await supabase
.from('apps')
.insert(appInsert as any) // Type assertion needed until types are regenerated
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

The type assertion 'as any' is used to bypass TypeScript checking for the is_demo and demo_expires_at fields. While this is understandable before types are regenerated, the comment indicates this is a temporary workaround. Ensure that 'bun types' is run after this migration is applied to regenerate TypeScript types from the database schema, and then this type assertion can be removed.

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +87
// Create the default versions (unknown and builtin) - same as on_app_create trigger
// We do this directly to ensure we have the version IDs immediately
const { data: versionsData, error: versionsError } = await supabase
.from('app_versions')
.upsert([
{
owner_org: body.owner_org,
deleted: true,
name: 'unknown',
app_id: appId,
},
{
owner_org: body.owner_org,
deleted: true,
name: 'builtin',
app_id: appId,
},
], { onConflict: 'name,app_id', ignoreDuplicates: true })
.select()

if (versionsError) {
cloudlog({ requestId, message: 'Error creating default versions', error: versionsError })
// Don't fail - the trigger might have already created them
}

cloudlog({ requestId, message: 'Default versions created', versionsData })

Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

The demo app creation duplicates the default version creation logic that is already handled by the on_app_create trigger. This means both the trigger and this function will attempt to create the 'unknown' and 'builtin' versions, which could lead to race conditions or duplicate key conflicts. The upsert with ignoreDuplicates helps, but this creates unnecessary redundancy. Consider either waiting for the trigger to complete or removing the trigger for demo apps.

Suggested change
// Create the default versions (unknown and builtin) - same as on_app_create trigger
// We do this directly to ensure we have the version IDs immediately
const { data: versionsData, error: versionsError } = await supabase
.from('app_versions')
.upsert([
{
owner_org: body.owner_org,
deleted: true,
name: 'unknown',
app_id: appId,
},
{
owner_org: body.owner_org,
deleted: true,
name: 'builtin',
app_id: appId,
},
], { onConflict: 'name,app_id', ignoreDuplicates: true })
.select()
if (versionsError) {
cloudlog({ requestId, message: 'Error creating default versions', error: versionsError })
// Don't fail - the trigger might have already created them
}
cloudlog({ requestId, message: 'Default versions created', versionsData })
// Default versions ('unknown' and 'builtin') are created by the on_app_create trigger.
// After the app insert completes, we can safely query for the 'unknown' version.

Copilot uses AI. Check for mistakes.
Comment on lines +81 to +100
if (versionsError) {
cloudlog({ requestId, message: 'Error creating default versions', error: versionsError })
// Don't fail - the trigger might have already created them
}

cloudlog({ requestId, message: 'Default versions created', versionsData })

// Get the 'unknown' version ID to use for the channel
const { data: unknownVersion, error: unknownVersionError } = await supabase
.from('app_versions')
.select('id')
.eq('app_id', appId)
.eq('name', 'unknown')
.eq('owner_org', body.owner_org)
.single()

if (unknownVersionError || !unknownVersion) {
cloudlog({ requestId, message: 'Error getting unknown version', error: unknownVersionError })
throw simpleError('cannot_get_unknown_version', 'Cannot get unknown version', { error: unknownVersionError })
}
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

Error handling for the versions creation is inconsistent with the rest of the function. When versionsError occurs, it logs but doesn't throw an error (line 82-83), assuming the trigger might have created them. However, if the unknown version query fails (line 97-99), it throws an error. This creates a scenario where: if both the trigger and this function fail to create versions, the function will continue past the version creation error but then fail when trying to retrieve the 'unknown' version. This could leave the demo app in an incomplete state. Consider making the error handling consistent - either both should throw or both should gracefully handle the case where the trigger already succeeded.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +134
import type { Context } from 'hono'
import type { Database } from '../../utils/supabase.types.ts'
import { simpleError } from '../../utils/hono.ts'
import { cloudlog } from '../../utils/logging.ts'
import { hasOrgRightApikey, supabaseAdmin, updateOrCreateChannel } from '../../utils/supabase.ts'

export interface CreateDemoApp {
owner_org: string
}

export async function createDemoApp(c: Context, body: CreateDemoApp, apikey: Database['public']['Tables']['apikeys']['Row']): Promise<Response> {
const requestId = c.get('requestId')

if (!body.owner_org) {
throw simpleError('missing_owner_org', 'Missing owner_org', { body })
}

// Check if the user is allowed to create an app in this organization
const userId = apikey.user_id
if (!(await hasOrgRightApikey(c, body.owner_org, userId, 'write', c.get('capgkey') as string))) {
throw simpleError('cannot_access_organization', 'You can\'t access this organization', { org_id: body.owner_org })
}

// Generate a unique demo app_id
const shortId = crypto.randomUUID().slice(0, 8)
const appId = `com.demo.${shortId}.app`

// Calculate expiry date (14 days from now)
const expiresAt = new Date()
expiresAt.setDate(expiresAt.getDate() + 14)

cloudlog({ requestId, message: 'Creating demo app', appId, owner_org: body.owner_org, expires_at: expiresAt.toISOString() })

// Create the app with is_demo flag and expiry date
// Use admin client to set the demo fields (RLS doesn't expose these to regular users)
const supabase = supabaseAdmin(c)
const appInsert = {
owner_org: body.owner_org,
app_id: appId,
icon_url: '',
name: 'Demo App',
retention: 2592000,
default_upload_channel: 'production',
is_demo: true,
demo_expires_at: expiresAt.toISOString(),
}

const { data: appData, error: appError } = await supabase
.from('apps')
.insert(appInsert as any) // Type assertion needed until types are regenerated
.select()
.single()

if (appError) {
cloudlog({ requestId, message: 'Error creating demo app', error: appError })
throw simpleError('cannot_create_demo_app', 'Cannot create demo app', { supabaseError: appError })
}

cloudlog({ requestId, message: 'Demo app created', appData })

// Create the default versions (unknown and builtin) - same as on_app_create trigger
// We do this directly to ensure we have the version IDs immediately
const { data: versionsData, error: versionsError } = await supabase
.from('app_versions')
.upsert([
{
owner_org: body.owner_org,
deleted: true,
name: 'unknown',
app_id: appId,
},
{
owner_org: body.owner_org,
deleted: true,
name: 'builtin',
app_id: appId,
},
], { onConflict: 'name,app_id', ignoreDuplicates: true })
.select()

if (versionsError) {
cloudlog({ requestId, message: 'Error creating default versions', error: versionsError })
// Don't fail - the trigger might have already created them
}

cloudlog({ requestId, message: 'Default versions created', versionsData })

// Get the 'unknown' version ID to use for the channel
const { data: unknownVersion, error: unknownVersionError } = await supabase
.from('app_versions')
.select('id')
.eq('app_id', appId)
.eq('name', 'unknown')
.eq('owner_org', body.owner_org)
.single()

if (unknownVersionError || !unknownVersion) {
cloudlog({ requestId, message: 'Error getting unknown version', error: unknownVersionError })
throw simpleError('cannot_get_unknown_version', 'Cannot get unknown version', { error: unknownVersionError })
}

cloudlog({ requestId, message: 'Unknown version found', unknownVersion })

// Create the production channel pointing to 'unknown' version
const channelInsert: Database['public']['Tables']['channels']['Insert'] = {
created_by: apikey.user_id,
app_id: appId,
name: 'production',
public: true,
disable_auto_update_under_native: true,
disable_auto_update: 'major',
ios: true,
android: true,
electron: true,
allow_device_self_set: false,
allow_emulator: true,
allow_device: true,
allow_dev: true,
allow_prod: true,
version: unknownVersion.id,
owner_org: body.owner_org,
}

await updateOrCreateChannel(c, channelInsert)

cloudlog({ requestId, message: 'Production channel created for demo app' })

return c.json({
status: 'ok',
app_id: appId,
name: 'Demo App',
message: 'Demo app created successfully. You can now explore the Capgo dashboard!',
})
}
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

The endpoint lacks comprehensive test coverage. Given that there are existing test files for app operations (app.test.ts, app-error-cases.test.ts, app-permissions.test.ts), this new demo app creation endpoint should have similar test coverage to verify: 1) successful demo app creation, 2) proper setting of is_demo flag and expiry date, 3) permission checks, 4) error cases, and 5) that onboarding emails are correctly skipped for demo apps.

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +134
export async function createDemoApp(c: Context, body: CreateDemoApp, apikey: Database['public']['Tables']['apikeys']['Row']): Promise<Response> {
const requestId = c.get('requestId')

if (!body.owner_org) {
throw simpleError('missing_owner_org', 'Missing owner_org', { body })
}

// Check if the user is allowed to create an app in this organization
const userId = apikey.user_id
if (!(await hasOrgRightApikey(c, body.owner_org, userId, 'write', c.get('capgkey') as string))) {
throw simpleError('cannot_access_organization', 'You can\'t access this organization', { org_id: body.owner_org })
}

// Generate a unique demo app_id
const shortId = crypto.randomUUID().slice(0, 8)
const appId = `com.demo.${shortId}.app`

// Calculate expiry date (14 days from now)
const expiresAt = new Date()
expiresAt.setDate(expiresAt.getDate() + 14)

cloudlog({ requestId, message: 'Creating demo app', appId, owner_org: body.owner_org, expires_at: expiresAt.toISOString() })

// Create the app with is_demo flag and expiry date
// Use admin client to set the demo fields (RLS doesn't expose these to regular users)
const supabase = supabaseAdmin(c)
const appInsert = {
owner_org: body.owner_org,
app_id: appId,
icon_url: '',
name: 'Demo App',
retention: 2592000,
default_upload_channel: 'production',
is_demo: true,
demo_expires_at: expiresAt.toISOString(),
}

const { data: appData, error: appError } = await supabase
.from('apps')
.insert(appInsert as any) // Type assertion needed until types are regenerated
.select()
.single()

if (appError) {
cloudlog({ requestId, message: 'Error creating demo app', error: appError })
throw simpleError('cannot_create_demo_app', 'Cannot create demo app', { supabaseError: appError })
}

cloudlog({ requestId, message: 'Demo app created', appData })

// Create the default versions (unknown and builtin) - same as on_app_create trigger
// We do this directly to ensure we have the version IDs immediately
const { data: versionsData, error: versionsError } = await supabase
.from('app_versions')
.upsert([
{
owner_org: body.owner_org,
deleted: true,
name: 'unknown',
app_id: appId,
},
{
owner_org: body.owner_org,
deleted: true,
name: 'builtin',
app_id: appId,
},
], { onConflict: 'name,app_id', ignoreDuplicates: true })
.select()

if (versionsError) {
cloudlog({ requestId, message: 'Error creating default versions', error: versionsError })
// Don't fail - the trigger might have already created them
}

cloudlog({ requestId, message: 'Default versions created', versionsData })

// Get the 'unknown' version ID to use for the channel
const { data: unknownVersion, error: unknownVersionError } = await supabase
.from('app_versions')
.select('id')
.eq('app_id', appId)
.eq('name', 'unknown')
.eq('owner_org', body.owner_org)
.single()

if (unknownVersionError || !unknownVersion) {
cloudlog({ requestId, message: 'Error getting unknown version', error: unknownVersionError })
throw simpleError('cannot_get_unknown_version', 'Cannot get unknown version', { error: unknownVersionError })
}

cloudlog({ requestId, message: 'Unknown version found', unknownVersion })

// Create the production channel pointing to 'unknown' version
const channelInsert: Database['public']['Tables']['channels']['Insert'] = {
created_by: apikey.user_id,
app_id: appId,
name: 'production',
public: true,
disable_auto_update_under_native: true,
disable_auto_update: 'major',
ios: true,
android: true,
electron: true,
allow_device_self_set: false,
allow_emulator: true,
allow_device: true,
allow_dev: true,
allow_prod: true,
version: unknownVersion.id,
owner_org: body.owner_org,
}

await updateOrCreateChannel(c, channelInsert)

cloudlog({ requestId, message: 'Production channel created for demo app' })

return c.json({
status: 'ok',
app_id: appId,
name: 'Demo App',
message: 'Demo app created successfully. You can now explore the Capgo dashboard!',
})
}
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

The demo app creation endpoint lacks rate limiting or quota restrictions. A user could potentially create unlimited demo apps by repeatedly calling this endpoint, which could lead to resource exhaustion or database bloat. Consider adding: 1) a limit on the number of active demo apps per organization, 2) rate limiting on the endpoint, or 3) a cooldown period between demo app creations.

Copilot uses AI. Check for mistakes.
owner_org: body.owner_org,
}

await updateOrCreateChannel(c, channelInsert)
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

The call to updateOrCreateChannel is not wrapped in error handling, but this function can throw errors (it uses .throwOnError() internally). If the channel creation fails, the function will throw an uncaught error, leaving the demo app in an inconsistent state (app created but no production channel). Consider wrapping this in a try-catch block and either rolling back the app creation or returning a more informative error message to the user.

Suggested change
await updateOrCreateChannel(c, channelInsert)
try {
await updateOrCreateChannel(c, channelInsert)
}
catch (error) {
cloudlog({ requestId, message: 'Error creating production channel for demo app', error })
throw simpleError('cannot_create_production_channel', 'Cannot create production channel for demo app', { error })
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
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 `@supabase/functions/_backend/public/app/demo.ts`:
- Around line 11-21: The createDemoApp handler currently types its context
generically and expects an apikey param; change the signature to accept
Context<MiddlewareKeyVariables> and stop taking apikey as a function parameter,
instead obtain the API key from the context via c.get('apikey'); update uses of
apikey in the function (e.g. derive userId with apikey.user_id) and pass the
retrieved key into hasOrgRightApikey (keeping the existing call to
hasOrgRightApikey), and adjust any callers or exports to match the new signature
so typings remain consistent with backend conventions.

In `@supabase/migrations/20260121000000_add_demo_app_support.sql`:
- Around line 62-99: The migration currently inserts a cron job directly into
public.cron_tasks for 'cleanup_expired_demo_apps'; instead create a new
migration that registers this task via the existing process_all_cron_tasks flow
instead of direct INSERT. Update the new migration to call or append the
scheduling entry into the process_all_cron_tasks routine (or its configuration
table) so the system-managed registration runs (referencing the task name
'cleanup_expired_demo_apps' and target 'public.cleanup_expired_demo_apps()'),
and remove the direct INSERT/ON CONFLICT block from this file; ensure the new
migration sets run_at_hour/run_at_minute/run_at_second to 3/0/0 and preserves
description and task_type for process_all_cron_tasks to consume.

Comment thread supabase/functions/_backend/public/app/demo.ts Outdated
Comment on lines +62 to +99
-- Register cron task to run cleanup daily at 03:00:00 UTC
INSERT INTO public.cron_tasks (
name,
description,
task_type,
target,
batch_size,
second_interval,
minute_interval,
hour_interval,
run_at_hour,
run_at_minute,
run_at_second,
run_on_dow,
run_on_day
) VALUES (
'cleanup_expired_demo_apps',
'Delete demo apps that have expired (14 days after creation)',
'function',
'public.cleanup_expired_demo_apps()',
null, -- batch_size not needed for function type
null, -- second_interval
null, -- minute_interval
null, -- hour_interval
3, -- run_at_hour (03:00 UTC)
0, -- run_at_minute
0, -- run_at_second
null, -- run_on_dow (any day)
null -- run_on_day (any day)
)
ON CONFLICT (name) DO UPDATE SET
description = EXCLUDED.description,
task_type = EXCLUDED.task_type,
target = EXCLUDED.target,
run_at_hour = EXCLUDED.run_at_hour,
run_at_minute = EXCLUDED.run_at_minute,
run_at_second = EXCLUDED.run_at_second,
updated_at = NOW();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Register cron via process_all_cron_tasks instead of direct insert.

Guidelines require updating process_all_cron_tasks (in a new migration) rather than inserting new cron jobs directly into public.cron_tasks. Please move the scheduling logic there. As per coding guidelines, ...

🤖 Prompt for AI Agents
In `@supabase/migrations/20260121000000_add_demo_app_support.sql` around lines 62
- 99, The migration currently inserts a cron job directly into public.cron_tasks
for 'cleanup_expired_demo_apps'; instead create a new migration that registers
this task via the existing process_all_cron_tasks flow instead of direct INSERT.
Update the new migration to call or append the scheduling entry into the
process_all_cron_tasks routine (or its configuration table) so the
system-managed registration runs (referencing the task name
'cleanup_expired_demo_apps' and target 'public.cleanup_expired_demo_apps()'),
and remove the direct INSERT/ON CONFLICT block from this file; ensure the new
migration sets run_at_hour/run_at_minute/run_at_second to 3/0/0 and preserves
description and task_type for process_all_cron_tasks to consume.

Copy link
Copy Markdown
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 `@supabase/functions/_backend/triggers/on_deploy_history_create.ts`:
- Around line 49-60: Regenerate Supabase TypeScript types so the new
apps.is_demo column exists (run the suggested Supabase CLI command to regenerate
supabase/functions/_backend/utils/supabase.types.ts), and update the
on_deploy_history_create logic around the
supabaseAdmin(c).from('apps').select('is_demo').eq('app_id',
record.app_id').single() call to handle and log query errors instead of silently
ignoring them—capture the query error result and call cloudlog or processLogger
with requestId and the error before falling back to treating the app as
non-demo.

In `@supabase/functions/_backend/triggers/on_version_create.ts`:
- Around line 27-41: Regenerate the TypeScript Supabase types after the
migration so the is_demo field exists, and extract the duplicated demo-check
into a shared helper (e.g., isAppDemo in
supabase/functions/_backend/utils/demo.ts) that calls
supabaseAdmin(c).from('apps').select('is_demo').eq('app_id', appId).single(),
logs any error via cloudlog, returns true only when appData?.is_demo === true,
and returns false on error; then replace the inline logic in
on_version_create.ts and on_deploy_history_create.ts with a call to isAppDemo(c,
record.app_id) and add the new import.

Comment on lines +49 to +60
// Check if this is a demo app - skip notifications for demo apps
const { data: appData } = await supabaseAdmin(c)
.from('apps')
.select('is_demo')
.eq('app_id', record.app_id)
.single()

const isDemo = appData?.is_demo === true
if (isDemo) {
cloudlog({ requestId: c.get('requestId'), message: 'Demo app detected, skipping deploy notifications' })
return c.json(BRES)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

TypeScript types need regeneration after migration.

The pipeline fails because is_demo column doesn't exist in the TypeScript types yet. The database migration adds this column, but supabase.types.ts hasn't been regenerated to include it.

Run the Supabase CLI to regenerate types:

supabase gen types typescript --local > supabase/functions/_backend/utils/supabase.types.ts

Additionally, the query error is silently ignored. While the fallback (treating as non-demo) is safe, consider logging the error for observability:

Proposed fix with error logging
     // Check if this is a demo app - skip notifications for demo apps
-    const { data: appData } = await supabaseAdmin(c)
+    const { data: appData, error: appError } = await supabaseAdmin(c)
       .from('apps')
       .select('is_demo')
       .eq('app_id', record.app_id)
       .single()
 
+    if (appError) {
+      cloudlog({ requestId: c.get('requestId'), message: 'Error checking demo app status', appError })
+    }
+
     const isDemo = appData?.is_demo === true
🧰 Tools
🪛 GitHub Actions: Run tests

[error] 56-56: vue-tsc --noEmit: TS2339: Property 'is_demo' does not exist on type 'SelectQueryError<"column 'is_demo' does not exist on 'apps'.">'.

🤖 Prompt for AI Agents
In `@supabase/functions/_backend/triggers/on_deploy_history_create.ts` around
lines 49 - 60, Regenerate Supabase TypeScript types so the new apps.is_demo
column exists (run the suggested Supabase CLI command to regenerate
supabase/functions/_backend/utils/supabase.types.ts), and update the
on_deploy_history_create logic around the
supabaseAdmin(c).from('apps').select('is_demo').eq('app_id',
record.app_id').single() call to handle and log query errors instead of silently
ignoring them—capture the query error result and call cloudlog or processLogger
with requestId and the error before falling back to treating the app as
non-demo.

Comment thread supabase/functions/_backend/triggers/on_version_create.ts
@riderx riderx force-pushed the riderx/demo-app-onboarding branch 2 times, most recently from ca0d8e4 to 706127c Compare January 21, 2026 22:22
Copy link
Copy Markdown
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: 1

🤖 Fix all issues with AI agents
In `@supabase/functions/_backend/public/app/demo.ts`:
- Around line 162-175: The code in demo.ts currently uses supabaseAdmin(c) to
create the Supabase client for a user-facing endpoint; replace that with the
user-scoped client supabaseWithAuth(c, c.get('auth')!) so Row-Level Security is
enforced (same pattern used in delete.ts). Locate the supabase variable
assignment (supabase = supabaseAdmin(c)) and change it to call supabaseWithAuth
with the context and the auth object; ensure the endpoint still validates that
c.get('auth') is present before calling and remove any admin-only assumptions
(compare to bundle/create.ts for documented admin exceptions if needed).
♻️ Duplicate comments (1)
supabase/functions/_backend/public/app/demo.ts (1)

190-245: Fail fast if demo versions aren’t created.

The flow logs versionsError but continues, which can yield a “success” response with missing versions/channels/devices. Guarding on upsert failure and verifying required versions avoids incomplete demo apps.

🛠️ Suggested fix
   const { data: versionsData, error: versionsError } = await supabase
     .from('app_versions')
     .upsert(versionInserts, { onConflict: 'name,app_id', ignoreDuplicates: true })
     .select()

   if (versionsError) {
     cloudlog({ requestId, message: 'Error creating demo versions', error: versionsError })
+    throw simpleError('cannot_create_versions', 'Cannot create demo versions', { supabaseError: versionsError })
   }
-  else {
-    cloudlog({ requestId, message: 'Demo versions created', count: versionsData?.length })
-  }
+  cloudlog({ requestId, message: 'Demo versions created', count: versionsData?.length })

   const { data: allVersions, error: allVersionsError } = await supabase
     .from('app_versions')
     .select('id, name')
     .eq('app_id', appId)
     .eq('owner_org', body.owner_org)

   if (allVersionsError || !allVersions) {
     cloudlog({ requestId, message: 'Error getting versions', error: allVersionsError })
     throw simpleError('cannot_get_versions', 'Cannot get versions', { error: allVersionsError })
   }

   const versionMap = new Map(allVersions.map(v => [v.name, v.id]))
+  const requiredVersions = ['unknown', 'builtin', '1.0.0', '1.0.1', '1.1.0', '1.1.1', '1.2.0']
+  const missingVersions = requiredVersions.filter(v => !versionMap.has(v))
+  if (missingVersions.length) {
+    throw simpleError('missing_demo_versions', 'Demo versions missing', { missingVersions })
+  }

Comment on lines +162 to +175
cloudlog({ requestId, message: 'Creating demo app with demo data', appId, owner_org: body.owner_org })

const supabase = supabaseAdmin(c)

// Create the demo app
const appInsert: Database['public']['Tables']['apps']['Insert'] = {
owner_org: body.owner_org,
app_id: appId,
icon_url: '',
name: 'Demo App',
retention: 2592000,
default_upload_channel: 'production',
last_version: '1.2.0',
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the full function to understand the endpoint context
cat -n supabase/functions/_backend/public/app/demo.ts | head -200

Repository: Cap-go/capgo

Length of output: 8781


🏁 Script executed:

# Continue reading the file to see all operations performed with supabaseAdmin
cat -n supabase/functions/_backend/public/app/demo.ts | head -400 | tail -200

Repository: Cap-go/capgo

Length of output: 8473


🏁 Script executed:

# Continue reading to see the rest of the function and understand full scope
cat -n supabase/functions/_backend/public/app/demo.ts | tail -150

Repository: Cap-go/capgo

Length of output: 7008


🏁 Script executed:

# Search for other uses of supabaseAdmin in public endpoints
fd -t f 'demo.ts' supabase/functions/_backend/public -o | head -5

Repository: Cap-go/capgo

Length of output: 172


🏁 Script executed:

# Check if other public endpoints use supabaseAdmin or alternatives
grep -r "supabaseAdmin" supabase/functions/_backend/public --include="*.ts" | head -20

Repository: Cap-go/capgo

Length of output: 2074


🏁 Script executed:

# Check if getDrizzleClient is available and used in public endpoints
grep -r "getDrizzleClient\|getPgClient" supabase/functions/_backend/public --include="*.ts" | head -10

Repository: Cap-go/capgo

Length of output: 489


🏁 Script executed:

# Check the bundle/create.ts comment to understand the justification pattern
grep -A 5 -B 5 "RLS bypass needed" supabase/functions/_backend/public/bundle/create.ts

Repository: Cap-go/capgo

Length of output: 496


🏁 Script executed:

# Check what replication.ts does with these clients to understand the pattern
head -50 supabase/functions/_backend/public/replication.ts

Repository: Cap-go/capgo

Length of output: 1435


🏁 Script executed:

# Check the utils/supabase.ts to understand supabaseAdmin vs alternatives
grep -A 10 "export.*supabaseAdmin\|export.*supabaseApikey" supabase/functions/_backend/utils/supabase.ts | head -40

Repository: Cap-go/capgo

Length of output: 925


🏁 Script executed:

# Check if there's any explicit justification comment in demo.ts for using supabaseAdmin
grep -B 5 -A 2 "supabaseAdmin" supabase/functions/_backend/public/app/demo.ts

Repository: Cap-go/capgo

Length of output: 733


🏁 Script executed:

# Check bundle/create.ts to see how it justifies admin use
grep -B 10 -A 20 "RLS bypass needed" supabase/functions/_backend/public/bundle/create.ts | head -50

Repository: Cap-go/capgo

Length of output: 1094


🏁 Script executed:

# Check if supabaseApikey is used as alternative in similar endpoints for context
grep -B 5 -A 10 "supabaseApikey" supabase/functions/_backend/public/app/delete.ts | head -40

Repository: Cap-go/capgo

Length of output: 1464


🏁 Script executed:

# Verify the endpoint handler signature and check if it receives apikey
grep -A 20 "export.*createDemoApp\|router.*demo" supabase/functions/_backend/public/app/demo.ts | head -30

Repository: Cap-go/capgo

Length of output: 881


🏁 Script executed:

# Check the endpoint registration to see if apikey is available
find supabase/functions/_backend -name "*.ts" -type f -exec grep -l "createDemoApp" {} \;

Repository: Cap-go/capgo

Length of output: 150


🏁 Script executed:

# Check how createDemoApp is registered and called in index.ts
grep -B 5 -A 10 "createDemoApp" supabase/functions/_backend/public/app/index.ts

Repository: Cap-go/capgo

Length of output: 935


🏁 Script executed:

# Check middlewareV2 to understand what auth context is available
grep -A 30 "export.*middlewareV2" supabase/functions/_backend/utils/hono_middleware.ts | head -50

Repository: Cap-go/capgo

Length of output: 1033


🏁 Script executed:

# Check what foundAPIKey sets in context
grep -A 30 "async function foundAPIKey" supabase/functions/_backend/utils/hono_middleware.ts

Repository: Cap-go/capgo

Length of output: 1524


🏁 Script executed:

# Check if apikey is stored in context that can be retrieved
grep -A 10 "setApiKeyAuthContext" supabase/functions/_backend/utils/hono_middleware.ts

Repository: Cap-go/capgo

Length of output: 1238


🏁 Script executed:

# Also check what's in MiddlewareKeyVariables
grep -B 5 -A 15 "MiddlewareKeyVariables" supabase/functions/_backend/utils/hono.ts

Repository: Cap-go/capgo

Length of output: 3342


🏁 Script executed:

# Check if foundJWT also sets apikey context or if it only sets jwt
grep -A 40 "async function foundJWT" supabase/functions/_backend/utils/hono_middleware.ts | head -50

Repository: Cap-go/capgo

Length of output: 1588


🏁 Script executed:

# Check if there's a supabaseClient function that uses JWT for user-scoped access
grep -A 15 "export.*supabaseClient\|function supabaseClient" supabase/functions/_backend/utils/supabase.ts | head -30

Repository: Cap-go/capgo

Length of output: 552


Replace supabaseAdmin with user-scoped client to enforce RLS.

The endpoint has proper authentication (lines 145-156) and can use supabaseWithAuth(c, c.get('auth')!) to obtain a client that respects RLS policies. This pattern is already used successfully in similar endpoints like delete.ts. Per coding guidelines, admin SDK should only be used for internal operations (triggers, CRON jobs), not user-facing APIs. Without explicit justification (compare to bundle/create.ts which documents why admin access is necessary), this violates the stated security requirement.

🤖 Prompt for AI Agents
In `@supabase/functions/_backend/public/app/demo.ts` around lines 162 - 175, The
code in demo.ts currently uses supabaseAdmin(c) to create the Supabase client
for a user-facing endpoint; replace that with the user-scoped client
supabaseWithAuth(c, c.get('auth')!) so Row-Level Security is enforced (same
pattern used in delete.ts). Locate the supabase variable assignment (supabase =
supabaseAdmin(c)) and change it to call supabaseWithAuth with the context and
the auth object; ensure the endpoint still validates that c.get('auth') is
present before calling and remove any admin-only assumptions (compare to
bundle/create.ts for documented admin exceptions if needed).

Copy link
Copy Markdown
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 `@supabase/functions/_backend/utils/stats.ts`:
- Around line 177-184: The time-range parsing currently uses Number(...) which
yields NaN for ISO strings or reversed ranges and can produce negative timeSpan;
update the logic around rangeEnd, rangeStart, timeSpan, and numSequences to:
accept either numeric ms or ISO strings (try Number(params.end_date) and fall
back to Date.parse(params.end_date)), treat NaN as undefined and default to
Date.now() / rangeEnd-rangeStart defaults, normalize the interval by swapping if
start > end and clamp negative spans to a minimum (e.g., 1 minute) before
computing numSequences, and replace any uses of Number(params.limit) with
params.limit ?? <default> so an explicit 0 is respected (see the other
occurrences referenced around the limit handling).
- Around line 145-152: generateDemoLogs currently constructs a supabaseAdmin
client (supabaseAdmin(c)) which bypasses RLS; replace it with a user-scoped
client created via supabaseWithAuth and the request's auth context (use
c.get('auth')) so queries run under the authenticated user. Locate
generateDemoLogs and change the client creation from supabaseAdmin(c) to
supabaseWithAuth(c, auth) (or equivalent factory that accepts the auth object),
then pass that client into the existing .from('devices') query so the demo stats
endpoint in private/stats.ts enforces RLS correctly.
♻️ Duplicate comments (1)
supabase/functions/_backend/public/app/demo.ts (1)

164-165: RLS bypass via supabaseAdmin in a user-facing endpoint.

This endpoint is user-facing and should not use the admin client; it bypasses RLS. Please switch to a user-scoped Supabase client (or pg/drizzle) or add a documented exception with input sanitization. As per coding guidelines, this should be corrected.

🧹 Nitpick comments (1)
supabase/functions/_backend/public/app/demo.ts (1)

51-58: Avoid lexicographic version comparisons for demo data.

String comparisons like '1.2.0' >= '1.10.0' are incorrect and will break if versions grow. Consider a simple ordered list or a semver comparator to keep this robust.

♻️ Example with explicit ordering
+const DEMO_VERSION_ORDER = ['1.0.0', '1.0.1', '1.1.0', '1.1.1', '1.2.0']
+const isAtLeastDemoVersion = (versionName: string, min: string) => {
+  const current = DEMO_VERSION_ORDER.indexOf(versionName)
+  const target = DEMO_VERSION_ORDER.indexOf(min)
+  return current >= 0 && target >= 0 && current >= target
+}
@@
-  if (versionName >= '1.1.0') {
+  if (isAtLeastDemoVersion(versionName, '1.1.0')) {
@@
-  if (versionName >= '1.2.0') {
+  if (isAtLeastDemoVersion(versionName, '1.2.0')) {

Also applies to: 87-92

Comment thread supabase/functions/_backend/utils/stats.ts Outdated
Comment thread supabase/functions/_backend/utils/stats.ts Outdated
@riderx riderx force-pushed the riderx/demo-app-onboarding branch 2 times, most recently from 8f5e37c to 900b2be Compare January 22, 2026 01:07
riderx and others added 13 commits January 22, 2026 01:17
- Add demo app creation endpoint that allows users to explore Capgo without CLI setup
- Add database schema changes for demo app support (is_demo flag, demo_expires_at timestamp)
- Implement auto-deletion of demo apps after 14 days via cron job
- Skip onboarding emails and store setup for demo apps
- Add demo app creation UI to onboarding flow (displayed as last option after invite teammate)
- Add translations to all 15 supported languages
- Demo apps are created with production channel pointing to 'unknown' version

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Skip bundle:created emails for demo apps in on_version_create
- Skip bundle:deployed emails for demo apps in on_deploy_history_create

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Use middlewareV2 to support both JWT (browser) and API key auth
- Add proper error handling for channel creation in demo.ts
- Fix handler typing with MiddlewareKeyVariables and AuthInfo
- Use hasOrgRight instead of hasOrgRightApikey (works with supabaseAdmin)
- Remove type assertion now that types are regenerated
- Create shared isAppDemo helper in utils/demo.ts for DRY code
- Update cleanup function to properly delete all related data:
  - app_versions_meta, daily_version, version_usage
  - channel_devices, channels, devices
  - bandwidth_usage, storage_usage, device_usage
  - daily_mau, daily_bandwidth, daily_storage
  - stats, org_users, deploy_history, app_versions
- Remove dead code (useless count query after deletion)
- Update triggers to use shared isAppDemo helper with error logging

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
The migration adds is_demo and demo_expires_at columns to apps table,
but since the migration hasn't been applied to the remote DB yet,
we need to add the types manually to pass TypeScript compilation.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The backend functions use a separate types file from the frontend.
Added is_demo and demo_expires_at to both type files.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Instead of adding new columns to the database, use the app_id
prefix 'com.demo.' to identify demo apps:

- isAppDemo() is now a simple sync string check, no DB query
- Cleanup function uses app_id LIKE 'com.demo.%' and created_at
- No schema changes needed

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
More unique prefix to avoid conflicts with real apps.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Address CodeRabbit docstring coverage warning.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Insert demo data directly into daily_* tables instead of raw usage tables
  (raw tables require cron aggregation which delays data visibility)
- Invalidate app_metrics_cache after demo creation to bypass 5-minute cache
- Add 500ms delay before navigation to ensure DB writes commit
- Add refresh=true parameter to force frontend cache clearing
- Call reloadAllCharts() instead of loadData() when refresh param is present
  to fetch fresh data from backend instead of using stale store cache

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add generateDemoLogs() function in stats.ts to create realistic log entries
- Detect demo apps (com.capdemo. prefix) and return fake logs instead of querying
  Cloudflare Analytics Engine (which can't have fake timestamps in production)
- Demo logs include realistic action sequences: update flows, app lifecycle events,
  channel checks, and pings
- Logs are generated within the requested time range and respect filters
- Also includes manifest entries for demo versions in demo.ts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…l updates

- Add SHA256 hashes (64 hex chars) for all demo files
- Demonstrate differential updates:
  - Stable files (vendor.js, polyfills.js, assets) have same hash across versions
  - Changing files (main.js, index.html, styles.css) have different hash per version
  - Added files (dark-theme.css, dashboard.js, charts.js) only in versions >= minVersion
- Use realistic file sizes in manifest entries
- styles.css demonstrates partial stability (same hash for hotfix versions)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace supabaseAdmin with supabaseWithAuth in generateDemoLogs to
  enforce RLS policies for user-facing stats endpoint
- Add parseDateMs helper to handle both ms timestamps and ISO strings
- Normalize time range to handle reversed start/end dates
- Use ?? instead of || for limit to respect explicit 0 values
- Add RLS bypass justification comment in demo.ts for multi-table
  demo data creation (similar to bundle/create.ts pattern)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fixes style/padded-blocks linting error.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@riderx riderx force-pushed the riderx/demo-app-onboarding branch from 334c5af to 6a7f8b2 Compare January 22, 2026 01:18
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
2 Security Hotspots

See analysis details on SonarQube Cloud

@riderx riderx merged commit 77b0f3c into main Jan 22, 2026
9 of 11 checks passed
@riderx riderx deleted the riderx/demo-app-onboarding branch January 22, 2026 01:29
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