This file provides guidance to AI agents (Claude Code, Cursor, Copilot, etc.) when working with code in this repository.
bun serve:dev- Start local development server with local environmentbun serve- Start development server with default configurationbun build- Build production version of the web appbun mobile- Build for mobile and copy to Capacitor platformsbun dev-build- Build with development branch configuration
bun test:all- Run all backend testsbun test:backend- Run backend tests excluding CLI testsbun test:cli- Run CLI-specific testsbun test:local- Run tests with local CLI pathbun test:front- Run Playwright frontend testsLOCAL_CLI_PATH=true bun test:all:local- Run all tests with local CLI configuration
bun test:cloudflare:all- Run all tests against Cloudflare Workersbun test:cloudflare:backend- Run backend tests against Cloudflare Workersbun test:cloudflare:updates- Run update tests against Cloudflare Workers./scripts/start-cloudflare-workers.sh- Start local Cloudflare Workers for testing
Note: Cloudflare test suite is currently unstable and may not pass reliably.
See CLOUDFLARE_TESTING.md for detailed information on testing against Cloudflare Workers.
bun lint- Lint Vue, TypeScript, and JavaScript filesbun lint:fix- Auto-fix linting issuesbun lint:backend- Lint Supabase backend filesbun typecheck- Run TypeScript type checking with vue-tscbun types- Generate TypeScript types from Supabase
bun run supabase:start- Start local Supabase instance (worktree-isolated)bun run supabase:db:reset- Reset and seed local databasebun backend- Start Supabase functions locallybun reset- Reset Supabase database
- Framework: Vue 3 with Composition API and
<script setup>syntax - Build Tool: Vite with custom Rolldown integration
- Routing: File-based routing with unplugin-vue-router
- State Management: Pinia stores
- Styling: TailwindCSS with DaisyUI components
- Mobile: Capacitor for native mobile functionality
- Database: PostgreSQL via Supabase
- Edge Functions: Supabase Edge Functions (Deno runtime)
- API Deployment: Multi-platform deployment:
- Cloudflare Workers (primary, handles 99% of traffic)
- Supabase Functions (internal tasks, CRON jobs)
supabase/functions/_backend/- Core backend logicplugins/- Public plugin endpoints (updates, stats, channel_self)private/- Internal API endpoints (auth required)public/- Public API endpoints (app, bundle, device management)triggers/- Database triggers and CRON functionsutils/- Shared utilities and database schemas
- For understanding the current DB schema, prefer
supabase/schemas/prod.sql(schema dump) instead of scanning all migrations. - For schema changes, always edit or add files under
supabase/migrations/and treatsupabase/schemas/prod.sqlas read-only reference. - Supabase admin client + sign-in pitfall: if you call
supabaseAdmin.auth.signInWithPassword(...), that client becomes authenticated as the user (it is no longer a pure service-role client). Always use a separate admin client for sign-in, and keep a clean admin client for service-role writes. Example:const loginAdmin = supabaseAdmin(c)→ use forsignInWithPasswordconst adminClient = supabaseAdmin(c)→ use for admin writes
All API endpoints must follow these response patterns:
- Success with data:
return c.json(data)orreturn c.json(data, 200) - Success without data:
return c.json(BRES)whereBRES = { status: 'ok' }(import fromutils/hono.ts) - Errors: Use
return simpleError()orreturn quickError(status, ...)(import fromutils/hono.ts)
Do NOT use c.body(null, 204) for success responses. Always return JSON for consistency.
Capgo relies on two layered caches for plugin endpoints (/updates, /stats, /channel_self) and they depend on specific response codes/body shapes. Do not change these without updating the Cloudflare snippet + app status logic.
- Edge on-prem cache (Cloudflare Snippet):
cloudflare_workers/snippet/index.jscaches responses when it detects:429+{ error: 'on_premise_app' }(from/updatesor/channel_self), or{ isOnprem: true }(from/stats). The snippet stores cached responses using the worker'sCache-ControlTTL and serves them before routing.
- Edge plan-upgrade cache (Cloudflare Snippet): same file caches responses when it detects:
429+{ error: 'need_plan_upgrade' }.
- App status cache (Worker runtime):
supabase/functions/_backend/utils/appStatus.tsstoresonprem/cancelled/cloudfor 60s using the Cache API to short-circuit DB lookups.
Implication: Keep the 429 + error payloads for on-prem and plan-upgrade responses; otherwise the edge caches and status cache effectiveness are broken.
src/components/- Reusable Vue componentssrc/pages/- File-based route pagessrc/services/- API clients and external service integrationssrc/stores/- Pinia state management storessrc/layouts/- Page layout components
- Bun - Package manager and JavaScript runtime
- Docker - Required for Supabase local development
- Supabase CLI - Database and functions management
-
Front-facing instructions (docs, onboarding, tooltips, demos, and customer-facing help):
- Use
npxfor runnable command examples and keepnpxin those public snippets. - Do not mention internal execution tooling preferences in this customer-facing context.
- Use
-
Internal tooling and internal documentation:
- Prefer Bun tooling (
bun/bunx) for repository maintenance, scripts, and internal workflows.
- Prefer Bun tooling (
-
Capgo CLI references:
- When explicitly discussing the Capgo CLI command itself, always use
@latest. - Use the public shape like
npx @capgo/cli@latest ...for customer-facing command examples. - Use internal execution equivalents (for example,
bunx @capgo/cli@latest ...) only in internal tooling context.
- When explicitly discussing the Capgo CLI command itself, always use
- Install dependencies:
bun install - Start Supabase:
bun run supabase:start - Reset database with seed data:
bun run supabase:db:reset - Start frontend:
bun serve:dev
- Demo User:
test@capgo.app/testtest - Admin User:
admin@capgo.app/adminadmin
- Located in
tests/directory - Use Vitest test runner with custom configuration
- Require running Supabase instance
- Tests modify local database state
- CLI tests require
LOCAL_CLI_PATH=trueenvironment variable
- API endpoint tests (CRUD operations)
- CLI functionality tests
- Database trigger tests
- Integration tests with external services
ALL TEST FILES RUN IN PARALLEL. Tests within the same file run sequentially (unless explicitly configured otherwise), but different test files execute simultaneously. You MUST design tests accordingly.
Maximize parallelism: Use it.concurrent() instead of it() to run tests in parallel within the same file. More parallel tests = faster CI/CD.
When creating tests that interact with shared resources (users, apps, orgs, devices, channels, bundles, etc.), follow these rules:
You CAN reuse existing seed data IF:
- You only READ the data, not modify it
- You create your OWN child resources under it (e.g., reuse a user but create your own app/org for that user)
- The parent resource is not modified by your test or other tests
You MUST create dedicated seed data IF:
- Your test MODIFIES the resource (update, delete, change settings)
- Other tests also modify that same resource
- The resource state matters for your test assertions
Guidelines:
- Create dedicated seed data when needed - Add new test-specific entries in
supabase/seed.sqlwith unique identifiers - Use unique naming conventions - Prefix test data with the test file name or feature being tested (e.g.,
test_my_feature_user@capgo.app,com.test.myfeature.app) - Clean up is NOT enough - Even with cleanup, parallel test files might try to use the data simultaneously
Examples of what breaks parallel test files:
- Modifying the default
test@capgo.appuser's settings - Deleting or updating the default app
com.demo.app - Changing org settings on the shared test org
- Using hardcoded IDs that other test files also modify
Examples of safe reuse:
- Using
test@capgo.appto create a NEW app specific to your test (user is not modified) - Reading from shared orgs without modifying them
- Creating new channels/bundles under your own dedicated app
When you need isolation, create dedicated seed data:
-- In seed.sql, add dedicated test data for your test file:
INSERT INTO auth.users (id, email, ...) VALUES
('unique-uuid-for-my-test', 'my_feature_test@capgo.app', ...);
INSERT INTO public.apps (app_id, owner_org, ...) VALUES
('com.test.myfeature.app', 'my-test-org-id', ...);Then in your test file, use ONLY these dedicated resources for modifications.
If your test breaks other tests in CI/CD, it is YOUR responsibility to fix it by creating isolated seed data.
- Uses
@antfu/eslint-configwith custom rules - Single quotes, no semicolons
- Vue 3 Composition API preferred
- Ignores: dist, scripts, public, supabase generated files
- Strict mode enabled
- Path aliases:
~/maps tosrc/ - Auto-generated types for Vue components and routes
- Supabase types auto-generated via CLI
- All code comments must be in English, regardless of the chat language.
- Follow Conventional Commits v1.0.0 (https://www.conventionalcommits.org/en/v1.0.0/).
- Use a clear type and scope when helpful (e.g.,
docs: ...,feat(api): ...,fix(frontend): ...).
- Always cover database changes with Postgres-level tests and complement them with end-to-end tests for affected user flows.
- Use the Supabase CLI for every migration and operational task whenever possible; avoid manual changes through the dashboard or direct SQL.
- When a feature requires schema changes, create a single migration file with
the Supabase CLI (
bunx supabase migration new <feature_slug>) and keep editing that file until the feature ships; never edit previously committed migrations. - Updating
supabase/seed.sqlto back new or evolved tests is expected; keep fixtures focused on current behavior while leaving committed migrations unchanged. - A migration that introduces a new table may include seed inserts for that table, but treat that seeding as part of the current feature and do not modify previously committed migrations.
- Investigate failing Supabase tests by reviewing the Docker container logs and any other relevant service logs before retrying.
- Before validating any backend or frontend task, run the project lint/format command to ensure consistent formatting.
- Leave
CHANGELOG.mdentries and theversionfield inpackage.jsonto the release automation; CI/CD updates them during tagged releases. - Do not create new cron jobs it's bad pattern instead update process_all_cron_tasks function in a new migration file to add your job if needed.
- Never use the Supabase admin SDK (with service key) for user-facing APIs. Always use the client SDK with user authentication so RLS policies are enforced. The admin SDK should only be used when accessing data that is not user-accessible or for internal operations (triggers, CRON jobs, etc.). When admin access is unavoidable for a user-facing endpoint, sanitize all user inputs carefully—the SDK is susceptible to PostgREST query injection (not SQL injection, but filter/modifier injection via crafted parameters).
- Prefer claim-based auth lookups for performance: use
supabase.auth.getClaims()(frontend) or auth context from middleware (backend) instead ofgetUser()unless you explicitly need the full user record from the Auth API.
ALWAYS set an empty search path in every PostgreSQL function.
Every function must set search_path = '' and use fully qualified names for all references:
-- CORRECT: Empty search_path with fully qualified names
CREATE OR REPLACE FUNCTION "public"."my_function"()
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
BEGIN
SELECT * FROM "public"."my_table";
-- All table/type references must be fully qualified
END;
$$;
-- WRONG: Missing search_path - vulnerable to attacks
CREATE OR REPLACE FUNCTION public.my_function()
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
BEGIN
SELECT * FROM my_table;
END;
$$;Rule 1: One policy per table per operation.
Never create duplicate policies for the same operation on a table. Multiple policies on the same operation create OR conditions that hurt query performance. Merge all conditions into a single policy:
-- WRONG: Multiple SELECT policies on the same table
CREATE POLICY "policy_1" ON public.my_table FOR SELECT USING (condition_1);
CREATE POLICY "policy_2" ON public.my_table FOR SELECT USING (condition_2);
-- CORRECT: Single merged policy
CREATE POLICY "Allow select on my_table" ON public.my_table
FOR SELECT USING (condition_1 OR condition_2);Rule 2: Call auth.uid() only once using a subquery.
The auth.uid() function should never be called multiple times in a policy. Use a SELECT * subquery pattern to call it once and reference the result:
-- WRONG: Multiple auth.uid() calls - poor performance
CREATE POLICY "my_policy" ON public.my_table
FOR SELECT USING (
user_id = auth.uid()
OR owner_id = auth.uid()
OR created_by = auth.uid()
);
-- CORRECT: Single auth.uid() call with subquery
CREATE POLICY "my_policy" ON public.my_table
FOR SELECT USING (
(SELECT * FROM (SELECT auth.uid() AS uid) AS auth_check
WHERE user_id = auth_check.uid
OR owner_id = auth_check.uid
OR created_by = auth_check.uid)
);
-- ALSO CORRECT: Using a CTE-style approach in the check
CREATE POLICY "my_policy" ON public.my_table
FOR SELECT USING (
EXISTS (
SELECT 1
FROM (SELECT auth.uid() AS uid) AS auth_user
WHERE my_table.user_id = auth_user.uid
OR my_table.owner_id = auth_user.uid
)
);NEVER use get_identity() directly in RLS policies.
ALWAYS use get_identity_org_appid() when app_id exists on the table.
public.get_identity_org_appid(
'{read,upload,write,all}'::public.key_mode[],
owner_org, -- or org_id
app_id
)get_identity_org_allowed() is an ABSOLUTE LAST RESORT. Only use it when:
- The table genuinely has NO app_id column
- There is NO way to join to get an app_id
- You have exhausted all other options
If you find yourself reaching for get_identity_org_allowed(), STOP and ask:
"Is there ANY way to get an app_id here?" If yes, use get_identity_org_appid().
-- CORRECT: Table has app_id - use get_identity_org_appid
CREATE POLICY "Allow org members to select build_requests"
ON public.build_requests
FOR SELECT
TO authenticated, anon
USING (
public.check_min_rights(
'read'::public.user_min_right,
public.get_identity_org_appid(
'{read,upload,write,all}'::public.key_mode[],
owner_org,
app_id
),
owner_org,
app_id,
NULL::BIGINT
)
);
-- CORRECT: Table has no app_id but can JOIN to get it
CREATE POLICY "Allow org members to select daily_build_time"
ON public.daily_build_time
FOR SELECT
TO authenticated, anon
USING (
EXISTS (
SELECT 1 FROM public.apps
WHERE apps.app_id = daily_build_time.app_id
AND public.check_min_rights(
'read'::public.user_min_right,
public.get_identity_org_appid(
'{read,upload,write,all}'::public.key_mode[],
apps.owner_org,
apps.app_id
),
apps.owner_org,
apps.app_id,
NULL::BIGINT
)
)
);
-- LAST RESORT: Table has NO app_id and NO way to get one (e.g., build_logs)
CREATE POLICY "Allow org members to select build_logs"
ON public.build_logs
FOR SELECT
TO authenticated, anon
USING (
public.check_min_rights(
'read'::public.user_min_right,
public.get_identity_org_allowed(
'{read,upload,write,all}'::public.key_mode[],
org_id
),
org_id,
NULL::CHARACTER VARYING,
NULL::BIGINT
)
);Key points:
- Use both
authenticatedandanonroles (anon enables API key auth) - Pass app_id to BOTH
get_identity_org_appid()ANDcheck_min_rights() - Reference apps, channels, app_versions tables for more examples
- The web client is built with Vue.js and Tailwind CSS; lean on utility classes and composition-friendly patterns rather than bespoke CSS.
- Use DaisyUI (
d-prefixed classes) for buttons, inputs, and other interactive primitives to keep behavior and spacing consistent. - Konsta components are reserved for the safe area helpers. Avoid importing
konstaanywhere else in the app. - Capgo's look centers on deep slate bases with the "Extract" azure highlight
(
--color-azure-500: #119eff) and soft radii; mirror the palette fromsrc/styles/style.css(e.g.,--color-primary-500: #515271) when introducing new UI.
- We intentionally route auth email links through
/confirm-signupto avoid mailbox link prefetchers triggering Supabase verification/recovery links. confirm-signup.vueonly allows redirects to the console host and the Supabase host; if you changeVITE_APP_URLorVITE_SUPABASE_URL, update both allow-lists accordingly.
- Cover customer-facing flows with the Playwright MCP suite. Add scenarios under
playwright/e2eand run them locally withbun run test:frontbefore shipping UI changes.
- App ID:
ee.forgr.capacitor_go - Build command:
bun mobile(builds and copies to platforms) - iOS/Android projects in respective platform directories
- Uses Capacitor Updater plugin for OTA updates
Our main database is hosted on Supabase. We use custom replica hosted in Planetscale.
We have 5 read replicas for our main database to ensure high availability and low latency for read operations. These replicas are synchronized with the primary database using logical replication. We have one replica by continent:
- North America (Ohio)
- Europe (Frankfurt)
- Asia (Seoul)
- Australia (Sydney)
- South America (Sao Paulo)
Applications are configured to read from the nearest replica based on the user's
location. This repartition is done by Cloudflare snippets at
cloudflare_workers/snippet/index.js.
When backend code uses the plugin read-path (/updates, /stats, /channel_self), it must only query what exists in the read replicas.
- Logical replication replicates table data, not derived objects like views and SQL functions.
- Treat the read replica (what you see in PlanetScale) as the source of truth for what is queryable from plugin endpoints.
- Do not query credits ledger tables/views from the replica (e.g.
usage_credit_*/usage_credit_balances). If plugin logic needs a “has credits” signal, materialize it into a replicated column/table (example: an org-level boolean flag that is refreshed by primary-side jobs).
Every pull request MUST include the following sections:
- Summary - Brief description of what changed
- Motivation - Why this change is needed
- Business Impact - How this affects Capgo's business, users, or revenue
- Test Plan - Checklist for testing the changes
CRITICAL: ALL sections in a PR created by AI agents MUST be marked with "(AI generated)".
Example:
## Summary (AI generated)
- Fixed the build system RLS policies
## Motivation (AI generated)
The native build system needed consistent RLS patterns...
## Business Impact (AI generated)
This enables revenue growth by providing a working build system...
## Test Plan (AI generated)
- [ ] Verify authenticated users can access build requests
- [ ] Verify API key authentication worksWARNING: Failure to mark AI-generated sections is a violation of transparency requirements. If you do not mark sections as "(AI generated)", you are doing it wrong and this is unacceptable behavior. You will be punished for not being transparent about AI-generated content. ALWAYS mark every section with "(AI generated)".
## Summary (AI generated)
- [Bullet points of changes]
## Motivation (AI generated)
[Why this change is needed]
## Business Impact (AI generated)
[How this affects Capgo - revenue, users, experience, etc.]
## Test Plan (AI generated)
- [ ] [Testing checklist]
Generated with AICRITICAL: All changes to public APIs and plugin interfaces MUST be backward compatible.
Customers take time to update their apps and plugins. Breaking changes cause production issues for users who haven't updated yet. Follow these rules:
- New fields: Can be added freely - old clients will ignore them
- Existing fields: Never remove or change the type/meaning
- New error codes: Fine to add, but don't remove existing ones
- Response format: Must remain compatible with older plugin versions
When behavior must differ between plugin versions, use version detection:
const pluginVersion = body.plugin_version || '0.0.0'
let isNewVersion = false
try {
const parsed = parse(pluginVersion)
isNewVersion = !isDeprecatedPluginVersion(parsed, MIN_V5, MIN_V6, MIN_V7, MIN_V8)
} catch (error) {
// If version parsing fails, assume old version for safety
}
if (isNewVersion) {
// New behavior for updated plugins
} else {
// Legacy behavior for old plugins
}- Adding a new optional response field: Old plugins ignore it, new plugins use it
- Changing error to success with new flag: Return success with
unset: trueinstead of error - old plugins see success, new plugins handle the flag - New endpoint: Doesn't affect existing clients
- Removing a response field that old plugins depend on
- Changing the meaning of an existing field
- Returning different HTTP status codes for the same scenario
- Removing support for old request formats
When in doubt, support both old and new behavior based on plugin version detection.
The deployment happens automatically after GitHub CI/CD on main branch.
You are not allowed to deploy on your own, unless if asked. Same for git you never git push on main branch, add or commit unless asked. You can do it in others branches