Skip to content

feat(reconcile): add BillingFeature kind - #2046

Open
rohilsurana wants to merge 6 commits into
mainfrom
feat/pricing-feature-hover-info
Open

rohilsurana wants to merge 6 commits into
mainfrom
feat/pricing-feature-hover-info

Conversation

@rohilsurana

@rohilsurana rohilsurana commented Nov 6, 2026 •

Copy link
Copy Markdown
Member

What

This adds a BillingFeature kind to frontier reconcile and frontier export. Gitops can now manage each billing feature's title and metadata, for example a markdown tooltip.

apiVersion: v1
kind: BillingFeature
spec:
  - name: aoi_size_for_tasking
    title: "Max size for each Tasking AOI: 2,500 km²"
    metadata:
      info: "Imagery is charged separately. Contact [support](mailto:support@example.com)."

Why

Today the BillingProduct kind lists a product's features by name only. A feature's title and metadata could only be set by hand in the DB. The server also ignores title and metadata for features that already exist when a product is updated.

How it works

It follows RFC 0001.

  • Identity: the feature name, matched exactly as the server stores it, including case and spaces. This is the same as the API, where Foo and foo are two features.
  • Field rule: a title left out becomes "". Metadata left out becomes {}.
  • Features are objects: an in-scope server feature missing from the file fails the plan. delete: true is rejected, because there is no API to delete a feature.
  • Name checks: exact duplicate names and blank names fail the plan. A name that differs from a server feature only by case is created as a new feature, like the API does.
  • Short names: server features with names shorter than 3 characters are out of scope, the same as BillingProduct. They are not required in the file and not exported. A file entry with the exact name of one fails the plan.
  • Product links stay with BillingProduct:
    • An update sends back the feature's current product links, so a feature is never unlinked.
    • A create sends no links.
  • Order: in one file, BillingFeature must come before BillingProduct.
  • Validation: every planned request is checked against the API's own rules before anything is applied.
  • Export: writes name, title and metadata, sorted by name. Reconciling an export plans no changes.

Commits

  1. Spec and diff
  2. Reconciler
  3. Export
  4. Kind order rule
  5. CLI registration
  6. Docs, covering the reconcile guide, the CLI reference and an addendum to RFC 0001. The published RFC itself is not changed.

Tests

  • Unit tests:
    • the diff cases
    • product links sent back on update
    • a dry run makes no writes
    • validation
    • the export round trip, including nested metadata
    • short names out of scope, matched by exact name
    • a server feature with unset metadata
    • kind order, checking that no product is written before the order error
  • A local sandbox run:
    • apply, then a rerun with no changes
    • ListFeatures and ListPlans show the new title and metadata
    • product links unchanged
    • a reset, a missing feature, and the wrong kind order all behave as expected

Related

This needs the UpdateFeature change in #2045. Without it, the server ignores an empty title or empty metadata, so a reset would plan but not apply. The two PRs can be reviewed and merged in either order. The CLI and the server are expected to run the same version.

The product link bug found during review is tracked in #2047.

@vercel

vercel Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
frontier Ready Ready Preview Nov 11, 2026 5:51pm UTC

@coderabbitai

coderabbitai Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

Summary

Summary by CodeRabbit

  • New Features
    • Added support for exporting and reconciling billing features, including creating missing features and updating titles and metadata.
    • Reconciliation validates feature names and metadata, supports dry runs, and preserves existing product links when updating features.
    • Billing product documents must appear before billing feature documents. Reconciliation reports errors when features cannot be managed or deleted.
  • Documentation
    • Updated CLI and reconciliation guides with billing feature support, ordering rules, and deletion limitations, and added a billing feature RFC addendum.

Adds billing-feature reconciliation with validation, change detection, API operations, and export. Registers the reconciler and requires billing-feature documents to precede billing-product documents. Updates the CLI reference, reconciliation guide, and RFC addendum.

Changes: Billing feature reconciliation

Layer / File(s) Summary
Feature specification and diff
internal/reconcile/billingfeature.go, internal/reconcile/billingfeature_test.go
Adds billing-feature spec validation and change detection for names, titles, and metadata. The diff reports unlisted server features and sorts additions and updates by name. Tests cover validation, change detection, and operation ordering.
Reconciler API and operations
internal/reconcile/billingfeature_reconciler.go, internal/reconcile/billingfeature_reconciler_test.go
Adds API-backed listing, scope filtering, reconciliation, dry runs, create and update requests, and export. Tests cover API errors, partial reports, request fields, and export behavior.
Registry and document ordering
cmd/reconcile.go, internal/reconcile/reconcile.go, internal/reconcile/reconcile_test.go
Registers the billing-feature reconciler and requires billing-feature documents to precede billing-product documents. A test checks ordering behavior.
Billing feature documentation
cmd/reconcile.go, docs/content/docs/reference/cli.mdx, docs/content/docs/reconcile.mdx, docs/rfcs/*
Documents billing-feature support, desired-state rules, export behavior, deletion limits, and document ordering. The RFC addendum describes feature fields and update behavior.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature

Possibly related PRs

  • raystack/frontier#2045: Adds the UpdateFeature field replacement behavior used by billing-feature reconciliation to apply desired title and metadata values.

Suggested reviewers: rohanchkrabrty

Merge Risk: Moderate · up to 3cdc0

Applying BillingFeature files can overwrite product links that changed concurrently. Clearing a title or metadata only works with a server that includes #2045. The docs do not yet warn about product creation overwriting a feature's links. The feature is usable for the normal flow, but these points should be resolved or explicitly accepted before merge.

Pre-merge checks: Passed 2

Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@coveralls

coveralls commented Nov 6, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 43072087101

Coverage increased (+0.2%) to 56.238%

Details

  • Coverage increased (+0.2%) from the base build.
  • Patch coverage: 24 uncovered changes across 3 files (216 of 240 lines covered, 90.0%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
internal/reconcile/billingfeature_reconciler.go 136 123 90.44%
internal/reconcile/billingfeature.go 96 86 89.58%
cmd/reconcile.go 8 7 87.5%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 42423
Covered Lines: 23858
Line Coverage: 56.24%
Coverage Strength: 19.78 hits per line

💛 - Coveralls

@rohilsurana
rohilsurana force-pushed the feat/pricing-feature-hover-info branch from a43267c to 22fa519 Compare November 6, 2026 09:21
@rohilsurana
rohilsurana marked this pull request as ready for review November 6, 2026 09:21

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 5

Nitpick comments: 2

internal/reconcile/reconcile_test.go: 1

169-172: Trivial · Maintainability & Code Quality · Low value

Assert that no product write occurs before the ordering error.

Use a valid, nonempty product spec and assert that productAPI.created remains empty. The empty spec produces no product writes, so the test can pass if product reconciliation runs before the ordering check.

internal/reconcile/billingfeature_reconciler_test.go: 1

83-85: Trivial · Data Integrity & Integration · Heavy lift

Test reset persistence through the server.

This test checks only the request sent to a fake. The current server retains the old title and metadata when the update sends empty values, so the test cannot detect the failed reset. Add an integration test that reloads both fields after applying the reset and verifies that a second reconciliation plans no changes.

Review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between f38ac11 and 22fa519.

Files selected for processing: 10

  • cmd/reconcile.go
  • docs/content/docs/reconcile.mdx
  • docs/content/docs/reference/cli.mdx
  • docs/rfcs/0001-declarative-reconcile.md
  • internal/reconcile/billingfeature.go
  • internal/reconcile/billingfeature_reconciler.go
  • internal/reconcile/billingfeature_reconciler_test.go
  • internal/reconcile/billingfeature_test.go
  • internal/reconcile/reconcile.go
  • internal/reconcile/reconcile_test.go

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 0 remain after this review.

Comment thread docs/rfcs/0001-declarative-reconcile.md Resolved
Comment on lines +180 to +185
case opUpdate:
req = &frontierv1beta1.UpdateFeatureRequest{Id: op.current.ID, Body: &frontierv1beta1.FeatureRequestBody{
Name: op.current.Name,
Title: op.spec.Title,
Metadata: metadata,
ProductIds: op.current.ProductIDs,

@coderabbitai coderabbitai Bot Nov 6, 2026 •

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.

Major · Data Integrity & Integration · Heavy lift

Supported by static analysis

Script executed:

sed -n '145,200p' internal/reconcile/billingfeature_reconciler.go
sed -n '225,260p' internal/api/v1beta1connect/billing_product.go
sed -n '570,605p' billing/product/service.go
rg -n 'UpsertFeature\(' billing internal/api
sed -n '260,285p' docs/content/docs/reconcile.mdx

Repository: raystack/frontier

Length of output: 8321


Script executed:

for spec in 'billing/product/service.go:105:135' 'billing/product/service.go:600:645' 'billing/plan/service.go:145:172' 'billing/plan/service.go:235:285' 'internal/reconcile/billingfeature_reconciler.go:1:110' 'internal/reconcile/billingfeature.go:1:160' 'cmd/reconcile.go:1:110'; do IFS=: read -r f start end <<< "$spec"; echo "--- $f:$start-$end"; nl -ba "$f" | sed -n "${start},${end}p"; done
rg -n -F -e 'NewBillingFeatureReconciler' -e 'NewFrontierServiceClient' -e 'frontierURL' -e 'frontier_url' -- cmd internal/reconcile

Repository: raystack/frontier

Length of output: 23756


Script executed:

for spec in 'internal/reconcile/billingfeature_reconciler.go:109:164' 'internal/reconcile/billingfeature_reconciler_test.go:14:98' 'internal/store/postgres/billing_feature_repository.go:178:220' 'pkg/metadata/metadata.go:15:35' 'cmd/client.go:1:75'; do IFS=: read -r f start end <<< "$spec"; echo "--- $f:$start-$end"; nl -ba "$f" | sed -n "${start},${end}p"; done
rg -n -F -e 'Host ' -e '"host"' -e 'FRONTIER_HOST' -- cmd/root.go cmd/config.go cmd/client.go

Repository: raystack/frontier

Length of output: 11758


Support and check empty field replacement.

When a title or metadata is omitted, the current server retains the existing value. Reconcile reports success, but later runs plan the same update.

Add replacement support to UpdateFeature without changing partial updates for other UpsertFeature callers. Check the returned feature against the desired values and report an error if it did not converge. Document that resets require a server with replacement support, and test both successful resets and nonconverging responses.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not changing this. The CLI and the server are expected to run the same version. The server side of the reset is in #2045.

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.

Analysis chain

Script executed:

#!/bin/bash
set -o pipefail
echo '--- PR metadata ---'
gh pr view 2045 --repo raystack/frontier --json title,state,mergedAt,baseRefName,headRefName,body,files
echo '--- Relevant diff excerpts ---'
gh pr diff 2045 --repo raystack/frontier | python3 -c '
import sys
s = sys.stdin.read()
for part in s.split("diff --git "):
    if not part.strip():
        continue
    lines = part.splitlines()
    if any(k in part for k in ("UpdateFeature", "UpsertFeature", "ReplaceFeature", "metadata", "Metadata")):
        print("diff --git " + lines[0])
        for i, line in enumerate(lines):
            if any(k in line for k in ("UpdateFeature", "UpsertFeature", "ReplaceFeature", "metadata", "Metadata", "ProductIds", "ProductIDs")):
                print("\n".join(lines[max(1, i-3):min(len(lines), i+5)]))
                print("---")
' | head -c 30000

Length of output: 33942


@rohilsurana Understood. I won’t pursue a CLI response check for a version mismatch.

#2045 is still open and contains the server-side replacement change. Before release, please verify that it is included and that a reset followed by a rerun plans no changes.

You are interacting with an AI system.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed. #2045 must ship before or with this PR. Reset was tested end to end in a local sandbox with both changes: clearing a title and metadata was applied, and a rerun planned no changes.

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.

@rohilsurana Thanks for testing the reset with both changes. The sandbox rerun planned no changes.

Before release, please verify that #2045 is included in the server version shipped with this PR.

You are interacting with an AI system.

Comment thread internal/reconcile/billingfeature.go Resolved
Comment thread internal/reconcile/billingfeature.go Resolved
var kindDependencies = map[string][]string{
KindRole: {KindPermission},
KindRole: {KindPermission},
KindBillingProduct: {KindBillingFeature},

@coderabbitai coderabbitai Bot Nov 6, 2026 •

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.

Major · Data Integrity & Integration · Heavy lift

Supported by static analysis

Script executed:

git diff f38ac119be14d4fda11f874ecfad9b2c4d9b65e8 22fa519476ecf9d61ffe6d9539b00e4bc72d5b20 -- internal/reconcile/reconcile.go
nl -ba internal/reconcile/reconcile.go | sed -n '1,220p'
rg -n -F -e 'CreateProduct(' -e 'UpsertFeature(' -e 'ProductIDs' -- internal cmd | head -250

Repository: raystack/frontier

Length of output: 13787


Script executed:

rg -n -F -e 'func (s *Service) Create(' -e 'func (s *Service) UpsertFeature(' -- billing/product
nl -ba internal/reconcile/billingproduct_reconciler.go | sed -n '1,310p'
nl -ba internal/api/v1beta1connect/billing_product.go | sed -n '1,105p'
nl -ba internal/store/postgres/billing_feature_repository.go | sed -n '50,115p;160,235p'
nl -ba internal/reconcile/billingfeature_reconciler.go | sed -n '65,194p'

Repository: raystack/frontier

Length of output: 29606


Script executed:

nl -ba billing/product/service.go | sed -n '1,220p;530,670p'
nl -ba internal/store/postgres/billing_feature_repository.go | sed -n '99,160p'
nl -ba internal/reconcile/billingproduct.go | sed -n '1,250p'
rg -n -F -e 'UpsertFeature' -e 'ProductIDs' -- billing/product
rg -n -F -e 'features:' -e 'BillingFeature' -- docs/content/docs/reconcile.mdx docs/rfcs/0001-declarative-reconcile.md

Repository: raystack/frontier

Length of output: 32966


Script executed:

nl -ba internal/reconcile/billingproduct.go | sed -n '198,345p'
nl -ba billing/product/service.go | sed -n '230,290p;670,710p'
nl -ba billing/entitlement/service.go | sed -n '1,180p'
nl -ba docs/content/docs/reconcile.mdx | sed -n '245,330p'
nl -ba internal/reconcile/billingproduct_reconciler_test.go | sed -n '275,375p'
git diff f38ac119be14d4fda11f874ecfad9b2c4d9b65e8 22fa519476ecf9d61ffe6d9539b00e4bc72d5b20 -- internal/reconcile/billingproduct.go internal/reconcile/billingproduct_reconciler.go billing/product/service.go internal/store/postgres/billing_feature_repository.go

Repository: raystack/frontier

Length of output: 24592


Preserve existing feature links when creating a product.

If a new product references a feature already linked to another product, creation replaces the feature’s product IDs with the new ID. The earlier product can lose the feature and its subscribers can lose entitlement. Merge the new product ID with the existing links on the create path.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch. This is an older server bug in product.Service.Create, and this PR does not change it. Tracked in #2047.

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.

@rohilsurana Thanks for tracking the existing server bug in #2047. The feature-link risk remains until that bug is fixed.

Would you like a separate follow-up issue, or should #2047 remain the tracker?

You are interacting with an AI system.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

#2047 stays the tracker for this. No separate issue is needed.

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.

@rohilsurana Understood. #2047 remains the tracker. I won’t open a separate issue.

You are interacting with an AI system.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

Review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 22fa519 and 662c0b3.

Files selected for processing: 6

  • docs/content/docs/reconcile.mdx
  • docs/rfcs/0001-declarative-reconcile-addendum-billingfeature.md
  • internal/reconcile/billingfeature.go
  • internal/reconcile/billingfeature_reconciler.go
  • internal/reconcile/billingfeature_test.go
  • internal/reconcile/reconcile_test.go
Files skipped from review as they are similar to previous changes: 1

  • docs/content/docs/reconcile.mdx

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.

Comment on lines +26 to +27
BillingFeature is an object, like BillingProduct and BillingPlan. Every feature on the server
must be in the file, and a missing one fails the plan.

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.

Minor · Functional Correctness · Quick win

Exclude out-of-scope features from the full-list requirement.

Features with names shorter than three characters are omitted from export. If a user follows this requirement and adds one to the file, reconciliation fails. State that only in-scope features must be listed, and that short-name entries are rejected.

Comment thread internal/reconcile/billingfeature.go Outdated
for _, s := range desired {
cur, exists := byName[s.Name]
if !exists {
if other, caseOnly := byLowerName[strings.ToLower(s.Name)]; caseOnly {

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.

We should also catch when two new features in the file have names that differ only by case

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is on purpose for now. The API treats Foo and foo as two features: name is a case-sensitive unique column (migrations/20240106162115_refactor_features.up.sql:20) and lookup is an exact match (billing_feature_repository.go:125). Reconcile matches the API, so two new names that differ only by case are both created. We can tighten this later if the API starts treating names case-insensitively.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

Review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 662c0b3 and ae2ab01.

Files selected for processing: 4

  • docs/content/docs/reconcile.mdx
  • docs/rfcs/0001-declarative-reconcile-addendum-billingfeature.md
  • internal/reconcile/billingfeature.go
  • internal/reconcile/billingfeature_test.go

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.

Comment thread docs/content/docs/reconcile.mdx Outdated
Comment on lines +274 to +275
The server stores every number as a decimal, so prefer strings for text like tooltips and
links. A value the API cannot hold, like a date or a nested map with a number as a key, fails

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.

Minor · Data Integrity & Integration · Quick win

Document numeric precision loss.

Metadata numbers use float64. For example, 9007199254740993 becomes 9007199254740992, and the diff can treat them as equal. (raw.githubusercontent.com) Warn users to quote values that require exact precision.

Comment on lines +121 to +122
if s.Title != cur.Title {
changes = append(changes, "title")

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.

Major · Data Integrity & Integration · Quick win

Integrate the server reset behavior before enabling empty resets.

When an existing feature has a title or metadata, omitting those fields now plans an update. The reconciler sends the empty values through UpdateFeature. However, billing/product/service.go still uses nonempty guards in UpsertFeature, so the server retains the stored values. Apply reports success, and the next reconcile plans the same update again. (raw.githubusercontent.com)

Integrate the stated #2045 dependency before releasing this behavior. Verify that applying an empty title and metadata produces a no-change subsequent plan.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

Review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between ae2ab01 and 3cdc01d.

Files selected for processing: 4

  • docs/content/docs/reconcile.mdx
  • docs/rfcs/0001-declarative-reconcile-addendum-billingfeature.md
  • internal/reconcile/billingfeature_reconciler.go
  • internal/reconcile/billingfeature_reconciler_test.go

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 0 remain after this review.

Rule 2 needs every default to be reachable through the API. Before this change, `UpdateFeature`
kept the old title and metadata when the request sent them empty. Now `UpdateFeature` writes the
title, metadata and product links exactly as sent, and finds the feature by name. Other paths
that save features, like creating a product or a plan, keep their old merge behavior.

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.

Major · Data Integrity & Integration · Quick win

Document the product-link overwrite.

Product creation does not merge existing product links. If a new product uses an existing feature, UpsertFeature replaces that feature’s links with the new product’s link. Customers on the other products can lose the feature. (raw.githubusercontent.com)

Until issue #2047 is fixed, correct this claim and add a warning to the reconciliation guide.

Name: op.current.Name,
Title: op.spec.Title,
Metadata: metadata,
ProductIds: op.current.ProductIDs,

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.

Major · Data Integrity & Integration · Heavy lift

Prevent feature updates from overwriting newer product links.

If another caller changes a feature’s product links after fetchCurrent runs, Line 183 sends the old links with the title or metadata update. The server replaces the stored links with that list. A title edit can therefore undo a product-link change and affect entitlements. Update only the fields managed by BillingFeature, or require a version check before replacing links. (raw.githubusercontent.com)

This branch was successfully deployed

1 active deployment
Preview — 3cdc01d4 Deployed Nov 11, 2026 by vercel[bot]
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.

4 participants