Skip to content

Replica-safe credits check for plugins#1592

Merged
riderx merged 7 commits into
mainfrom
riderx/replica-credit-flag
Feb 6, 2026
Merged

Replica-safe credits check for plugins#1592
riderx merged 7 commits into
mainfrom
riderx/replica-credit-flag

Conversation

@riderx
Copy link
Copy Markdown
Member

@riderx riderx commented Feb 6, 2026

Summary (AI generated)

  • Stop plugin read-path queries from using public.usage_credit_balances (not present on read replicas).
  • Add replicated orgs.has_usage_credits flag + daily refresh job to keep replica-safe “has credits” signal.
  • Document the replica data contract for /updates, /stats, /channel_self.

Test plan (AI generated)

  • bun run lint
  • bun run lint:backend

Screenshots (AI generated)

  • N/A

Checklist (AI generated)

  • 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

Generated with AI

Summary by CodeRabbit

  • Documentation

    • Added comprehensive PR guidelines (required Summary, Motivation, Business Impact, Test Plan) with explicit AI-generated content labeling and a PR template.
    • Added API & plugin backward-compatibility guidance and Replica Data Contract rules for safe read-replica queries, deployment, and governance best practices.
  • Infrastructure

    • Introduced a replicated usage-credits flag and associated sync/refresh mechanisms to ensure reliable, replica-safe credit checks.

Copilot AI review requested due to automatic review settings February 6, 2026 21:40
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 6, 2026

Warning

Rate limit exceeded

@riderx has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 12 minutes and 33 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Adds a replica-safe usage-credits flag: a boolean has_usage_credits column on public.orgs, a migration to backfill and keep it synced (functions, trigger, cron task), updates server-side credit-check logic to read the replicated column, and updates AGENTS.md with replica contract and PR guidance.

Changes

Cohort / File(s) Summary
Documentation
AGENTS.md
Adds "Replica Data Contract (CRITICAL)" guidance, mandatory PR sections and AI-content labeling, PR template, API/plugin backward-compatibility rules, deployment/governance notes, and testing/development best practices.
Schema Definition
supabase/functions/_backend/utils/postgres_schema.ts
Adds has_usage_credits boolean column to orgs schema with NOT NULL and DEFAULT false.
Credit Check Logic
supabase/functions/_backend/utils/pg.ts
Replaces join against usage_credit_balances with a replica-safe expression reading has_usage_credits from orgs (uses JSON coercion and COALESCE); adds comments about replica-safety and backward compatibility.
Database Migration & Maintenance
supabase/migrations/20260206213247_org_has_usage_credits_flag.sql
Creates has_usage_credits column, backfills from usage_credit_grants/balances, adds public.refresh_orgs_has_usage_credits() (SECURITY DEFINER), trigger function and trigger to sync on grants changes, and schedules a daily cron task to refresh the flag.
Tests
tests/plugin-credits-flag.test.ts
Adds Vitest that validates plugin plan-gating: asserts 429 when has_usage_credits is false and 200 after enabling it; verifies DB state and replica-safe access patterns.

Sequence Diagram(s)

sequenceDiagram
    participant Plugin as Plugin Endpoint
    participant Replica as Read Replica
    participant Primary as Primary DB
    participant Cron as Scheduler

    Plugin->>Replica: SELECT orgs.has_usage_credits ...
    alt has_usage_credits = true
        Replica-->>Plugin: allow request / proceed
    else has_usage_credits = false
        Replica-->>Plugin: deny or require billing flow (e.g., 429)
    end

    note over Primary,Cron: Writes and syncs happen only on Primary
    Cron->>Primary: CALL public.refresh_orgs_has_usage_credits()
    Primary->>Primary: backfill/compute has_usage_credits from grants
    Primary->>Replica: replication streams updated has_usage_credits
    Primary-->>Primary: trigger on usage_credit_grants -> sync function updates orgs flag
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • feat: credit system v2 #1231 — Introduced usage credit tables/views and the usage_credit_balances surface that this change replaces with a replicated flag.

Suggested labels

enhancement

Poem

🐰 I dug a little column bright,
to tell replicas day from night.
Triggers hop and cron-ticks sing,
now read-only lands know what to bring.
Hooray — synced carrots for every spring!

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description is largely AI-generated with incomplete Test Plan section (showing test output instead of actual testing steps) and missing resolution steps for the required documentation update. Complete the Test Plan section with clear steps to reproduce the changes, and clarify whether the documentation update to https://github.com/Cap-go/website has been made or is pending.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Replica-safe credits check for plugins' accurately summarizes the main change: making the credits check work on read replicas by using a replicated flag instead of querying the credits ledger.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch riderx/replica-credit-flag

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: 81d66a075a

ℹ️ 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 +57 to +61
-- Run daily after credits expiry (03:00:30 UTC) so replicas get a stable replicated flag.
INSERT INTO "public"."cron_tasks" (
"name",
"description",
"task_type",
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 Refresh cadence leaves has_usage_credits stale for a day

The new replica-safe flag is only refreshed by a daily cron task, so any credit purchase or consumption after that run will leave orgs.has_usage_credits stale until the next day. Because buildPlanValidationExpression now relies on this flag for plugin read-path gating, orgs can be incorrectly blocked from updates after topping up (false negatives) or continue to receive updates after exhausting credits (false positives) for up to 24 hours. Consider updating the flag transactionally when credits change or running the refresh much more frequently to keep plan enforcement accurate.

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 updates the plugin read-path plan/credits validation to be safe on read replicas (which don’t replicate views/functions) by introducing a replicated orgs.has_usage_credits flag and refreshing it via a scheduled job, plus documenting the replica data contract for plugin endpoints.

Changes:

  • Add orgs.has_usage_credits replicated boolean + a daily refresh function and cron task registration.
  • Update backend plan validation SQL to rely on the replicated flag (with a backward-compatible fallback for replicas missing the new column).
  • Document replica-safe querying rules for /updates, /stats, /channel_self.

Reviewed changes

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

Show a summary per file
File Description
supabase/migrations/20260206213247_org_has_usage_credits_flag.sql Adds replicated org credits flag + refresh function + cron task entry.
supabase/functions/_backend/utils/postgres_schema.ts Exposes orgs.has_usage_credits in Drizzle schema.
supabase/functions/_backend/utils/pg.ts Switches plan validation from usage_credit_balances to replica-safe org flag (with column-missing fallback).
bun.lock Adds configVersion field to the Bun lockfile.
AGENTS.md Documents replica data contract for plugin read-path endpoints.

Comment on lines +40 to 52
// IMPORTANT: read replicas replicate table data but not views/functions.
// Keep this expression replica-safe by relying on the replicated org flag.
//
// Also keep it backward compatible with replicas that haven't replicated the
// new column yet: we read it via `to_jsonb(row)->>'has_usage_credits'` so the
// query still parses even if the column doesn't exist.
const orgCreditsAlias = alias(schema.orgs, 'org_credits')
const hasCreditsExpression = sql`EXISTS (
SELECT 1
FROM public.usage_credit_balances ucb
WHERE ucb.org_id = ${ownerColumn}
AND COALESCE(ucb.available_credits, 0) > 0
FROM ${orgCreditsAlias}
WHERE ${orgCreditsAlias.id} = ${ownerColumn}
AND COALESCE((to_jsonb(org_credits) ->> 'has_usage_credits')::boolean, false) = true
)`
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

This changes the plan validation logic for plugin endpoints (/updates, /stats, /channel_self) from a live balance view to a replicated flag. There should be a regression test that covers “credits grant enables access” and “credits depleted disables access” (and ideally the “column missing on replica defaults false” fallback) to prevent future changes from accidentally reintroducing replica-only relations or breaking plan gating.

Copilot uses AI. Check for mistakes.
Comment on lines +36 to +40
-- Update orgs that have a row in the balances view.
UPDATE "public"."orgs" AS o
SET "has_usage_credits" = (COALESCE(b."available_credits", 0) > 0)
FROM "public"."usage_credit_balances" AS b
WHERE b."org_id" = o."id";
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

The daily refresh function updates every org with a balance row even when the boolean value doesn’t change. This causes unnecessary writes/WAL and extra replication churn. Add a predicate to only update rows where has_usage_credits differs from the computed value.

Suggested change
-- Update orgs that have a row in the balances view.
UPDATE "public"."orgs" AS o
SET "has_usage_credits" = (COALESCE(b."available_credits", 0) > 0)
FROM "public"."usage_credit_balances" AS b
WHERE b."org_id" = o."id";
-- Update orgs that have a row in the balances view, but only when the flag changes.
UPDATE "public"."orgs" AS o
SET "has_usage_credits" = (COALESCE(b."available_credits", 0) > 0)
FROM "public"."usage_credit_balances" AS b
WHERE b."org_id" = o."id"
AND o."has_usage_credits" IS DISTINCT FROM (COALESCE(b."available_credits", 0) > 0);

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +35
CREATE OR REPLACE FUNCTION "public"."refresh_orgs_has_usage_credits"()
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
BEGIN
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

This SECURITY DEFINER function doesn’t set an explicit owner (many other internal cron functions are ALTERed to OWNER TO postgres). Setting the owner explicitly helps keep privileges consistent across environments and avoids surprises if migrations run under a different role.

Copilot uses AI. Check for mistakes.
Comment on lines +57 to +75
-- Run daily after credits expiry (03:00:30 UTC) so replicas get a stable replicated flag.
INSERT INTO "public"."cron_tasks" (
"name",
"description",
"task_type",
"target",
"run_at_hour",
"run_at_minute",
"run_at_second"
)
VALUES (
'refresh_org_usage_credits_flag',
'Refresh orgs.has_usage_credits from usage credit balances (replicated flag for read replicas)',
'function',
'public.refresh_orgs_has_usage_credits()',
3,
0,
30
)
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

Refreshing orgs.has_usage_credits only once per day can make plan enforcement stale (e.g., org tops up credits but remains blocked until the next run, or consumes credits to 0 but keeps access until the next run). Since this flag is used on the plugin read-path, it should be kept near-real-time via primary-side triggers or by updating the flag as part of credit grant/consumption flows (keeping the daily refresh as a fallback for expiries).

Copilot uses AI. Check for mistakes.
0,
30
)
ON CONFLICT ("name") DO NOTHING;
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

The cron_tasks registration uses ON CONFLICT DO NOTHING. If the task already exists (e.g., in a long-lived/staging DB), schedule/target changes in this migration won’t apply. Prefer ON CONFLICT (name) DO UPDATE to keep the task definition in sync.

Suggested change
ON CONFLICT ("name") DO NOTHING;
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";

Copilot uses AI. Check for mistakes.
Comment thread supabase/functions/_backend/utils/pg.ts Outdated
AND COALESCE(ucb.available_credits, 0) > 0
FROM ${orgCreditsAlias}
WHERE ${orgCreditsAlias.id} = ${ownerColumn}
AND COALESCE((to_jsonb(org_credits) ->> 'has_usage_credits')::boolean, false) = true
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

The SQL uses a hard-coded table alias identifier in to_jsonb(org_credits). This is fragile (a refactor of the alias string would silently break the query) and is easy to mismatch with the actual alias emitted by Drizzle. Prefer building the row reference from the same alias variable used in the FROM clause so the identifier stays consistent.

Suggested change
AND COALESCE((to_jsonb(org_credits) ->> 'has_usage_credits')::boolean, false) = true
AND COALESCE((to_jsonb(${orgCreditsAlias}) ->> 'has_usage_credits')::boolean, false) = true

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: 1

🤖 Fix all issues with AI agents
In `@tests/plugin-credits-flag.test.ts`:
- Line 28: The Supabase-generated TypeScript types are out of date so the new
orgs column has_usage_credits is not present; regenerate the DB types and commit
them. Run the Supabase type generator (e.g. `supabase gen types typescript
--project-id ...` or your project’s configured command), update the committed
types file(s) referenced by your codebase (the generated Database types imported
by tests such as in tests/plugin-credits-flag.test.ts), then re-run the tests to
ensure references to has_usage_credits (including other occurrences around lines
46-50) type-check correctly.
🧹 Nitpick comments (4)
supabase/migrations/20260206213247_org_has_usage_credits_flag.sql (2)

14-34: Redundant second backfill UPDATE.

The first UPDATE (lines 14–24) already sets has_usage_credits to the result of EXISTS(...), which covers both the true and false cases for every row where the current value differs. The second UPDATE (lines 27–34) is therefore a no-op — it only targets rows that are not false and have no grants, but those rows were already set to false by the first statement.

Remove the redundant second UPDATE
 UPDATE "public"."orgs" AS o
 SET "has_usage_credits" = EXISTS (
   SELECT 1
   FROM "public"."usage_credit_grants" AS g
   WHERE g."org_id" = o."id"
 )
 WHERE o."has_usage_credits" IS DISTINCT FROM EXISTS (
   SELECT 1
   FROM "public"."usage_credit_grants" AS g
   WHERE g."org_id" = o."id"
 );
 
--- Ensure orgs without any grants are false (and avoid needless writes).
-UPDATE "public"."orgs" AS o
-SET "has_usage_credits" = false
-WHERE NOT EXISTS (
-  SELECT 1
-  FROM "public"."usage_credit_grants" AS g
-  WHERE g."org_id" = o."id"
-)
-AND o."has_usage_credits" IS DISTINCT FROM false;

72-104: Minor: trigger function lacks explicit REVOKE/GRANT like the refresh function.

The refresh function has explicit REVOKE ALL ... FROM PUBLIC + GRANT ... TO service_role, but the trigger function (sync_org_has_usage_credits_from_grants) does not. Since it's invoked only via the trigger (owned by postgres), this isn't a security hole, but adding matching REVOKE/GRANT would be consistent.

tests/plugin-credits-flag.test.ts (2)

38-38: Use it.concurrent() for parallel test execution.

Per coding guidelines, tests should use it.concurrent() instead of it().

Fix
-  it('allows /updates when has_usage_credits is true (replica-safe)', async () => {
+  it.concurrent('allows /updates when has_usage_credits is true (replica-safe)', async () => {

As per coding guidelines: tests/**/*.{ts,test.ts}: "Design tests for parallel execution; use it.concurrent() instead of it() to run tests in parallel within the same file."


31-36: Manual cleanup in afterAll may leave orphaned data on test failure.

If resetAppData throws, the subsequent deletes won't run. Consider wrapping each cleanup step independently or using try/catch to ensure all cleanup is attempted. Also note that resetAppData already handles errors internally (doesn't throw), but the subsequent manual deletes could fail and prevent later ones from executing.

Comment thread tests/plugin-credits-flag.test.ts Outdated
.eq('customer_id', stripeCustomerId)

// Ensure default state is "no credits flag".
await supabase.from('orgs').update({ has_usage_credits: false }).eq('id', orgId)
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

Pipeline failure: has_usage_credits not in generated Supabase types.

The CI is failing with TS2353: 'has_usage_credits' does not exist in type. The Supabase-generated Database types need to be regenerated after the new migration so the orgs table type includes the new column. Run supabase gen types typescript (or equivalent) and commit the updated type definitions.

Also applies to: 46-50

🧰 Tools
🪛 GitHub Actions: Run tests

[error] 28-28: vue-tsc --noEmit: TS2353: Object literal may only specify known properties, and 'has_usage_credits' does not exist in type '{ created_at?: string | null | undefined; created_by?: string | undefined; customer_id?: string | null | undefined; email_preferences?: Json | undefined; enforce_encrypted_bundles?: boolean | undefined; ... 13 more ...; use_new_rbac?: boolean | undefined; }'.

🤖 Prompt for AI Agents
In `@tests/plugin-credits-flag.test.ts` at line 28, The Supabase-generated
TypeScript types are out of date so the new orgs column has_usage_credits is not
present; regenerate the DB types and commit them. Run the Supabase type
generator (e.g. `supabase gen types typescript --project-id ...` or your
project’s configured command), update the committed types file(s) referenced by
your codebase (the generated Database types imported by tests such as in
tests/plugin-credits-flag.test.ts), then re-run the tests to ensure references
to has_usage_credits (including other occurrences around lines 46-50) type-check
correctly.

@riderx riderx merged commit f492b2d into main Feb 6, 2026
9 checks passed
@riderx riderx deleted the riderx/replica-credit-flag branch February 6, 2026 23:02
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Feb 6, 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