Skip to content

feat(accounts): add feature flags with negative-balance opt-in - #1817

Merged
pjb157 merged 7 commits into
mainfrom
peter/credit-check-bypass
Sep 18, 2026
Merged

pjb157 merged 7 commits into
mainfrom
peter/credit-check-bypass

Conversation

@pjb157

@pjb157 pjb157 commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Accounts normally lose paid-model access at zero balance and batch admission below zero. Add a reusable user_feature_flags table and use ALLOW_NEGATIVE_BALANCE to let opted-in billing accounts accrue debt while usage continues to be charged.

The database enforces one row per account and flag. Missing or disabled rows mean disabled. A typed Rust lookup and SQL user_has_feature function share the same account-scoped behavior, including denying flags on soft-deleted accounts. Insert, update, and delete notifications refresh onwards eligibility for regular and composite models. Bulk onwards reloads use inline EXISTS lookups so PostgreSQL can reuse an enabled-account set instead of invoking a SQL function for each model/key pair; single-account admission checks retain the shared helper.

Organization-owned keys use the organization's flag; personal keys use their owner's flag. Batch admission checks the active account, uploaded file owner, and resolved execution-key billing account independently, including capped personal keys used in organization context. Deployment-key lookups exclude soft-deleted keys in every access branch. Model permissions, key deletion, and explicit key spending caps remain enforced. Flags are managed directly in the database, with enable/disable SQL documented in the onwards sync guide.

This is the first consumer of the shared table. Existing ZDR, invoicing, and auto-join storage is unchanged; config seeding and authenticated-user flag enrichment are outside this change. The migration is additive relative to the released schema and replaces the unreleased boolean-column migration in this PR.

Validation

  • Full CI on the latest commit passed, including security, Open Responses, and pooled-database end-to-end checks.

  • Rust library suite: 3,298 passed, 4 ignored, 0 failed across all workspace crates.

  • Rust clippy with all features, formatting, and SQLx prepared-query validation passed.

  • Fresh-schema migration and direct PostgreSQL checks passed for flag defaults, uniqueness, timestamps, account isolation, soft deletion, and regular/composite routing toggles.

  • Regression coverage includes capped personal execution keys with organization context, revoked-key deployment lookup, organization versus personal billing, personal file submission from organization context, flag insertion/update/deletion notifications, usage accounting, and existing access/spending restrictions.

  • Independent code review completed; findings addressed.

Add a database-only allow_negative_balance policy on user and organization
billing accounts, defaulting to false. Apply the policy to regular and
composite model key eligibility, batch admission, and insufficient-credit
error enrichment while retaining usage accounting and key spending caps.

Notify onwards when the policy changes, document its operation, refresh
SQLx query metadata, and cover eligibility, accounting, organization scope,
and access restrictions with regression tests.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 18, 2026

Copy link
Copy Markdown

Deploying control-layer with  Cloudflare Pages  Cloudflare Pages

Latest commit: a31f7e4
Status: ✅  Deploy successful!
Preview URL: https://6c16725e.control-layer.pages.dev
Branch Preview URL: https://peter-credit-check-bypass.control-layer.pages.dev

View logs

@pjb157
pjb157 marked this pull request as ready for review September 18, 2026 08:34
Copilot AI lite review requested due to automatic review settings September 18, 2026 08:34

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 13 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="dwctl/src/db/handlers/credits.rs">

<violation number="1" location="dwctl/src/db/handlers/credits.rs:460">
P3: In `get_balance_for_admission`, the flag and the balance are read with two separate queries, so a flag flip committed between them yields a stale decision for one admission check. Since this method gates batch admission, onwards eligibility, and error enrichment, fold both reads into a single statement so the flag and balance come from one snapshot.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread dwctl/src/db/handlers/api_keys.rs Outdated
Comment thread dwctl/src/db/handlers/credits.rs Outdated
/// permits debt. This policy never changes the balance used for accounting.
#[instrument(skip(self), fields(user_id = %abbrev_uuid(&user_id)), err)]
pub async fn get_balance_for_admission(&mut self, user_id: UserId) -> Result<Option<Decimal>> {
let allow_negative_balance = sqlx::query_scalar!(

@cubic-dev-ai cubic-dev-ai Bot Sep 18, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: In get_balance_for_admission, the flag and the balance are read with two separate queries, so a flag flip committed between them yields a stale decision for one admission check. Since this method gates batch admission, onwards eligibility, and error enrichment, fold both reads into a single statement so the flag and balance come from one snapshot.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At dwctl/src/db/handlers/credits.rs, line 460:

<comment>In `get_balance_for_admission`, the flag and the balance are read with two separate queries, so a flag flip committed between them yields a stale decision for one admission check. Since this method gates batch admission, onwards eligibility, and error enrichment, fold both reads into a single statement so the flag and balance come from one snapshot.</comment>

<file context>
@@ -453,6 +453,24 @@ impl<'c> Credits<'c> {
+    /// permits debt. This policy never changes the balance used for accounting.
+    #[instrument(skip(self), fields(user_id = %abbrev_uuid(&user_id)), err)]
+    pub async fn get_balance_for_admission(&mut self, user_id: UserId) -> Result<Option<Decimal>> {
+        let allow_negative_balance = sqlx::query_scalar!(
+            "SELECT allow_negative_balance FROM users WHERE id = $1 AND is_deleted = false",
+            user_id
</file context>
Fix with cubic

Copilot AI left a comment

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.

🟡 Changes recommended

Deleted-key filtering is still needed in the deployment lookup exemption branches.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds a database-only account opt-in allowing negative balances while preserving usage accounting, permissions, and spending caps.

Changes:

  • Applies the opt-in to model eligibility, deployment lookup, batch admission, and error enrichment.
  • Adds migration, reload notification, documentation, SQLx metadata, and regression tests.
  • Review note (nit, 3 votes): deployment lookup needs deleted-key predicates in the exemption branches.
File summaries
File Summary
dwctl/src/sync/onwards_config/tests.rs Tests eligibility toggling and restrictions.
dwctl/src/sync/onwards_config/mod.rs Updates regular and composite model eligibility.
dwctl/src/error_enrichment.rs Adjusts insufficient-credit error enrichment.
dwctl/src/db/handlers/credits.rs Adds balance-admission policy handling and tests.
dwctl/src/db/handlers/api_keys.rs Updates deployment-key eligibility.
dwctl/src/api/handlers/batches.rs Allows opted-in negative-balance batches.
dwctl/migrations/147_allow_negative_balance.sql Adds the account flag and reload trigger.
docs/onwards-sync-notification-system.md Documents operator configuration.
.sqlx/query-d5be88341f10e2b20ca435b2ec6ffa0831e96183fec04f4ba1ed600fbade86f0.json Updates SQLx query metadata.
.sqlx/query-d04d6a6b66259e75a9b18118eb139031d9c66398f447d148abb5e42c255d4394.json Adds admission-query metadata.
.sqlx/query-3161d003a16d0634a6d1b6e69460eeb7f6a07d29a042cb8e77f98512fc139eb8.json Updates composite-query metadata.
.sqlx/query-2704727661d6058708a8f3297121b230fbd8377a3845c1e9f0afdeb183855eab.json Updates deployment-query metadata.
.sqlx/query-0fdbda77ac8a89b72d3c1c218b7013442942604e7a172d7024fa1e708ebc0ba1.json Updates regular-query metadata.
Review details

Files not reviewed (3)

  • .sqlx/query-3161d003a16d0634a6d1b6e69460eeb7f6a07d29a042cb8e77f98512fc139eb8.json: Generated file
  • .sqlx/query-d04d6a6b66259e75a9b18118eb139031d9c66398f447d148abb5e42c255d4394.json: Generated file
  • .sqlx/query-d5be88341f10e2b20ca435b2ec6ffa0831e96183fec04f4ba1ed600fbade86f0.json: Generated file
  • Files reviewed: 10/13 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread dwctl/src/db/handlers/api_keys.rs Outdated
Comment on lines +963 to +969
WHERE dg.deployment_id = $1
AND (
ak.user_id = $2 -- System user always has access
OR EXISTS (
SELECT 1 FROM users u
WHERE u.id = ak.user_id AND u.allow_negative_balance AND NOT u.is_deleted
)
@pjb157
pjb157 marked this pull request as draft September 18, 2026 09:02
Replace the unreleased negative-balance column with account-scoped feature storage and a shared SQL and typed Rust lookup. Notify routing consumers when flags are inserted, updated, or removed while preserving usage accounting and key restrictions.

Check batch file-owner admission independently of organization context and cover flag lifecycle, account isolation, and organization-owned admission with regression tests.
@pjb157 pjb157 changed the title feat(billing): allow account-level negative balance opt-in feat(accounts): add feature flags with negative-balance opt-in Sep 18, 2026
@pjb157
pjb157 marked this pull request as ready for review September 18, 2026 09:48

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 15 files

Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.

Fix all with cubic | Re-trigger cubic

Comment thread dwctl/src/api/handlers/batches.rs
Comment thread docs/onwards-sync-notification-system.md Outdated
Comment thread docs/onwards-sync-notification-system.md Outdated
Expose the enabled-account lookup to the PostgreSQL planner in both regular and composite routing queries so it can reuse a hashed set instead of evaluating a SQL function per model and API key. Preserve the outer deleted-user guard and existing balance, permission, and spending-cap checks. Regenerate SQLx query metadata and update the query-shape regression guard.
…keys

Check the resolved batch execution key owner independently from the active organization and file owner, preventing an organization debt opt-in from covering a capped personal key. Use the execution creditor for verification status. Exclude soft-deleted API keys from all deployment lookup branches and their bulk access queries. Add regressions for cross-account admission and revoked-key lookup, regenerate SQLx metadata, and clarify soft deletion and zero-balance batch behavior in the feature flag documentation.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 7 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Fix all with cubic | Re-trigger cubic

Comment thread dwctl/src/api/handlers/batches.rs Outdated
Read verification status from the active account used by the outstanding-volume limiter. Keep the resolved execution account balance check independent so a cross-account key cannot inherit a negative-balance opt-in without changing existing quota semantics.
Integrate the analytics checkpoint changes from main with the account feature flag and admission fixes so required checks validate the current merge base.
@pjb157
pjb157 enabled auto-merge (squash) September 18, 2026 16:28
@pjb157
pjb157 merged commit dc3bb30 into main Sep 18, 2026
27 checks passed
pjb157 added a commit that referenced this pull request Sep 18, 2026
🤖 I have created a release *beep* *boop*
---


##
[12.1.0](v12.0.1...v12.1.0)
(2026-09-18)


### Features

* **accounts:** add feature flags with negative-balance opt-in
([#1817](#1817))
([dc3bb30](dc3bb30))


### Bug Fixes

* **analytics:** checkpoint records before enrichment
([#1789](#1789))
([7ed4000](7ed4000))
* **fusillade:** fence reclaimed request ownership
([#1804](#1804))
([14a53f9](14a53f9))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
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.

3 participants