Skip to content

feat(api): give every project its own sponsorship budget, keyed by environment - #2580

Closed
multipletwigs wants to merge 8 commits into
chore/sponsorship-budget-job-entrypointfrom
chore/sponsorship-budget-drop-organization-scope
Closed

multipletwigs wants to merge 8 commits into
chore/sponsorship-budget-job-entrypointfrom
chore/sponsorship-budget-drop-organization-scope

Conversation

@multipletwigs

@multipletwigs multipletwigs commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #2579. Gives every project its own sponsorship budget row and keys budgets by SDP environment (sandbox, production) instead of network names. Migration 0184 drops the organization scope and the NULL project default.

  • Breaking (sdp:migration-compat: breaking), in one step on purpose. SDP has zero users, mainnet sponsorship has never run, and the prod DB is reset before launch. So we accept a few minutes of refused sponsorship while 0184 runs (feat(api): budget sponsorship per project and run the operator CLI as a job #2579's budget queries fail closed) and the loss of pre-launch org/project revision history, instead of a four-PR expand/contract. Merge only once feat(api): budget sponsorship per project and run the operator CLI as a job #2579 is live in prod; never roll back past it. After launch, renames go expand/contract.
  • Every project gets its row from a trigger on projects, at starting limits per environment, in the same transaction as the project. Tenants can't write budgets: RLS allows that insert only from inside the trigger.
  • A project without its row refuses only itself (sdp_api_sponsorship_project_policy_missing); the breaker stays closed. status lists projectsWithoutPolicy and exits 2.
  • Backfill carries each project's override, else its environment's default.

Final structure

Table Key Rows
sponsorship_budget_policies environment 2: cap on all projects together, plus breaker/kill switch
sponsorship_budget_project_policies project_id (FK) 1 per project (2 per org)
…_revisions (one per policy table) Append-only history
sponsorship_budget_reservations per sponsored tx environment, org, project, reserved/actual lamports

How a sponsorship is checked

  1. Request scoped to an active project; environment from the cluster scope.
  2. Load the environment's global row and the project's row. Missing global trips the breaker; missing project refuses that project.
  3. Either disabled: refused.
  4. Worst case (network fee + Kora max_allowed_lamports) must fit both per-transaction limits.
  5. Reservation written to Postgres; Redis atomically checks hourly/daily counters for global and project:<id>. Over: RATE_LIMITED, released.
  6. Kora signs; SDP sends.
  7. Reconciler (every minute, reservations ≥2 min old) settles actual lamports from chain: released if provably never landed, charged_unknown if unprovable.

Verification: typecheck 28/28, biome clean. Migrated a fresh DB and the local dev DB (55/55 projects got rows, override carried, 2 global rows). CLI end to end: preview, apply, stale-version refusal, kill, resume, missing-row detection. Trigger and RLS probed as a non-bypass role. Transfer admission not exercised locally (local API is self_hosted, which skips budgets); covered by the service suite. Vitest not run locally.

…hema

Delete organization-scope policies and their revisions, narrow the scope_type
CHECK to global and project, and drop the organization clause from the tenant
read policy. Breaking: deploy only after the two-scope API is live.
@vercel

vercel Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
sdp-docs Ready Ready Preview Oct 27, 2026 2:22pm UTC
sdp-web Ready Ready Preview Oct 27, 2026 2:22pm UTC

Request Review

@greptile-apps

greptile-apps Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Critical impact] The PR appears safe to merge under the documented pre-launch deployment plan.

Summary

Gives each project its own sponsorship budget and keys global budgets by SDP environment.

  • Each project gets its own sponsorship budget, with shared limits keyed by environment.
  • Sponsorship reservations and counters follow the SDP environment.
  • Operators manage global and project budgets by environment.

Previous decisions remain accepted:

  • multipletwigs accepted the reservation ID change because there are no live mainnet reservations and the database will be reset before launch.
  • multipletwigs rejected archive fields for operational policy and append-only history tables; Greptile withdrew that finding.
  • multipletwigs accepted deploying the breaking migration with its code because the platform is pre-launch and the compatibility check is optional; Greptile withdrew that finding.
  • multipletwigs accepted deleting pre-launch budget history because it has no audit value and the database reset would remove it anyway; Greptile withdrew that finding.

Reviews (5) · Last reviewed commit: "test(api): read 0184 bigint columns as t..." · Reviewed by Greptile

Comment thread apps/sdp-api/src/db/repositories/sponsorship-budget.repository.ts Outdated
@multipletwigs multipletwigs changed the title feat(api): drop the organization sponsorship budget scope from the schema feat(api): give every project its own sponsorship budget, keyed by environment Oct 27, 2026
@multipletwigs
multipletwigs added this pull request to stack #2581 October 27, 2026 12:18
Comment thread apps/sdp-api/src/services/sponsorship-budget.service.ts
CREATE DOMAIN sdp_lua_safe_lamports AS BIGINT
CHECK (VALUE BETWEEN 0 AND 9007199254740991);

CREATE TABLE sponsorship_budget_project_policies (

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.

P2 Required archive fields are missing

Both new tables omit the required status column and its CHECK. This violates “Soft delete only” in the API section of docs/contributing/code-standards.md, which requires those fields on new tables.

Add them to sponsorship_budget_project_policies and sponsorship_budget_project_policy_revisions before merging.

File Used: docs/contributing/code-standards.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Not adding these; the soft-delete rule targets resource tables, and neither new table is one. sponsorship_budget_project_policies is one row per project that lives and dies with the project, and its enabled flag is the switch. sponsorship_budget_project_policy_revisions is an append-only log (a trigger denies UPDATE and DELETE), so a row has nothing to be archived from. This matches the existing sponsorship_budget_policies, sponsorship_budget_policy_revisions and sponsorship_reconciliation_state tables, none of which has a status column.

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.

You’re right; I over-applied the soft-delete convention. These are operational policy and append-only history tables, not API resources, and their lifecycle is already handled by the project relationship and enabled flag. The existing sponsorship policy/revision tables provide direct precedent. I’m withdrawing the finding.

Should I remember this distinction for future reviews: require status = 'archived' on resource tables, but not on operational policy or append-only history tables?

@multipletwigs

Copy link
Copy Markdown
Collaborator Author

@greptileai please re-review: 0184 now marks its locks as blocking, with the reason, and the breaking-migration finding is answered in its thread.

…vironment

Brings the branch onto the current base and applies the restructure: one
global policy per SDP environment, one policy per project written by a trigger
on project insert, budgets/reservations/Redis keyed by sandbox|production,
status reporting projects without a policy, and tests for all of it.
A recreated project policy restarts at version 1; keyed by project, its Redis
control entry kept the old row's higher version and forward-only syncs never
caught up, so admission would refuse the project as a stale policy. Keyed by
the row id, a new row gets a new entry.
SDP has no users, no mainnet sponsorship has run, and the production database is reset before launch, so a brief sponsorship refusal and the loss of pre-launch revision history beat a four-step expand/contract. Not a precedent after launch.
…l int8 parser

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QhoeJoQRZxzdNUMVTZXeoU
@multipletwigs

Copy link
Copy Markdown
Collaborator Author

backoffice solves this

This branch was successfully deployed

2 active deployments
Preview – sdp-docs — a193611c Deployed Oct 27, 2026 by vercel[bot]
Preview – sdp-web — a193611c Deployed Oct 27, 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.

1 participant