Skip to content

feat(dashboard-api): implement upsertProjectLimits - #3438

Merged
ben-fornefeld merged 5 commits into
mainfrom
management-upsert-project-limits
Jul 29, 2026
Merged

feat(dashboard-api): implement upsertProjectLimits#3438
ben-fornefeld merged 5 commits into
mainfrom
management-upsert-project-limits

Conversation

@ben-fornefeld

@ben-fornefeld ben-fornefeld commented Jul 29, 2026

Copy link
Copy Markdown
Member

The first /v1/management handler to leave 501, now that project_limits (#3429) gives it somewhere to write.

What it does

Stores a project's effective limits exactly as sent. This side does no arithmetic — the caller owns plans and add-ons and has already resolved them. That's the point of the push, and it's what lets team_limits prefer this row over the tier it would otherwise compute from.

Contract: two missing dimensions

tiers gained default_free_disk_size_mb and max_disk_size_mb, and the view exposes both — but AdminControlPlaneProjectLimits carried only seven fields, so an override could never have set the other two.

Both added, required like the rest: a partial override has no meaning against a table whose columns are all NOT NULL.

This is a breaking contract change for the caller, and deliberately so — belt's exhaustruct rule is scoped to these generated types precisely to make a new field a build failure rather than a silently-zero payload.

Errors are distinguished, not collapsed

Failure Status Why
Foreign key violation 404 The team is the row's only FK, so it means the project is unknown here — not that the payload was bad
Check violation 400 The caller sent a combination the column set refuses. Currently free disk above the ceiling, which the request schema can't express
Anything else 500 Ours

Adds dberrors.IsCheckViolation alongside the existing helpers.

Cache invalidation, and a bug it uncovered

The team cache carries limits, so without invalidating it the write is invisible until the entry expires.

Review found InvalidateTeamCache was itself incomplete: it cleared team-<id> and the API-key hashes but not the per-member entries ValidateAuthProviderTeam populates — which hold a full Team including limits, and gate sandbox creation on the browser-session path. It now sweeps all three key kinds via a new GetTeamMemberIDs.

That was already a bug for the other caller: admin_kill_team_sandboxes invalidates after blocking a team and left the same entries stale.

The handler logs rather than fails: the row is already committed, and returning an error would invite a retry that can't improve on a stale cache.

Tests

Driven through the handler, asserting through team_limits rather than the table — so the two are proven wired together rather than assumed. Idempotency is covered because the caller retries.

The 404, the 400 and the cache invalidation are each mutation-verified: removing the FK mapping, the check mapping, or the invalidation call fails exactly the intended test.

Verification

packages/db and dashboard-api build, vet, test and lint clean. Codegen idempotent.

Also: finishing the management rename

#3361 moved these routes from /admin/v1 to /v1/management but touched only the path keys. The surface has carried three names for one concept ever since:

Layer Was Now
Paths /v1/management/* unchanged
Schemas AdminControlPlane* Management*
operationIds upsertProject managementUpsertProject
Handler methods UpsertProject ManagementUpsertProject
Tag workspace-admin control-plane-management
Handler files one workspace_admin.go one management_*.go per operation
Scheme AdminJWTAuth unchanged

AdminJWTAuth stays — it names a credential, not the surface, and sits beside AdminApiKeyAuth, which is a different scheme entirely. That adjacency is also why Admin on the schemas had become actively misleading rather than merely stale.

Folded in here rather than deferred because six more handlers are about to be written against these types, and the caller's call sites are about to multiply. It only gets more expensive.

The operationId prefix matters on a shared ServerInterface: without it these seven sit in the same namespace as the dashboard's own operations, and UpsertProject says nothing about which surface it answers on.

Breaks the caller on its next spec sync — seven method names and four type names in controlplanes/management.go. All mechanical, all compile failures, same coordination as the two required fields above.

Handler files are now one per operation, matching the rest of the directory (admin_users_delete.go, template_get.go, teams_list.go) rather than the single scaffolded file the seven arrived in. A PR implementing one handler touches one file, and the surface is legible from ls. sendNotImplemented moved to utils_management.go and loses a caller with each handler that lands.


Also drops six int64 conversions in packages/auth/pkg/types/teams.go that became redundant when #3429 widened TeamLimit. They were failing lint on main — after that change I checked db and dashboard-api but not the package that consumes the type.

The first of the /v1/management handlers to leave 501, now that
project_limits gives it somewhere to write.

Values arrive absolute and are stored as given. This side performs no
arithmetic: the caller owns plans and add-ons and has already resolved
them, which is the whole point of the push and what lets team_limits
prefer this row over the tier it would otherwise compute from.

Adds the two limit dimensions the contract was missing. tiers gained
default_free_disk_size_mb and max_disk_size_mb, and the view exposes
both, but AdminControlPlaneProjectLimits carried only seven fields --
so an override could never have set the other two. Required, like the
rest, because a partial override has no meaning against a table whose
columns are all NOT NULL.

Three failures are distinguished rather than collapsed into 500. A
foreign key violation is the only way the row can fail on identity, so
it means the project is unknown here and answers 404. A check violation
means the caller sent a combination the column set refuses -- currently
free disk above the ceiling, which the request schema cannot express --
and answers 400. Everything else is ours.

The team cache carries limits, so the write is invisible until the entry
expires without invalidating it. The existing admin user-delete handler
omits that call; this one does not. It logs rather than fails, because
the row is already committed and an error would only invite a retry that
cannot improve on a stale cache.

Tests drive the handler and assert through team_limits rather than the
table, so the two are proven wired together. Idempotency is covered
because the caller retries. The 404, the 400 and the cache invalidation
are each mutation verified.
@cursor

cursor Bot commented Jul 29, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes affect enforced sandbox limits and auth cache freshness across API key and session paths; the limits contract and generated client names are breaking for the caller.

Overview
This PR implements PUT /v1/management/projects/{teamID}/limits, the first management route that persists data instead of returning 501. The handler stores caller-supplied absolute limits in project_limits (including newly required default_free_disk_size_mb and max_disk_size_mb), maps FK failures to 404 and CHECK violations to 400, and calls InvalidateTeamCache after a successful write so cached team limits update immediately. Auth InvalidateTeamCache is extended to drop per-member browser-session cache entries via GetTeamMemberIDs, not only team-id and API-key hashes. The OpenAPI surface is aligned under Management* types and management* operationIds (breaking for the control-plane client on spec sync); other management operations remain stubs. Handler layout is split one file per operation with shared sendNotImplemented.

Reviewed by Cursor Bugbot for commit feaa416. Bugbot is set up for automated code reviews on this repo. Configure here.

@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

❌ 6 Tests Failed:

Tests completed Failed Passed Skipped
3585 6 3579 7
View the top 1 failed test(s) by shortest run time
github.com/e2b-dev/infra/tests/integration/internal/tests/envd::TestCommandKillNextApp
Stack Traces | 278s run time
=== RUN   TestCommandKillNextApp
=== PAUSE TestCommandKillNextApp
=== CONT  TestCommandKillNextApp
    process_test.go:30: Build failed: {<nil> An internal error occurred. Please try again or contact support with the build ID. <nil>}
--- FAIL: TestCommandKillNextApp (278.03s)
View the full list of 5 ❄️ flaky test(s)
github.com/e2b-dev/infra/tests/integration/internal/tests/envd::TestListDir

Flake rate in main: 21.68% (Passed 112 times, Failed 31 times)

Stack Traces | 0.53s run time
=== RUN   TestListDir
=== PAUSE TestListDir
=== CONT  TestListDir
--- FAIL: TestListDir (0.53s)
Executing command cat in sandbox ip2lge3gxxfpoe6jkeu4c (user: root)
github.com/e2b-dev/infra/tests/integration/internal/tests/envd::TestListDir/depth_0_lists_only_root_directory

Flake rate in main: 25.93% (Passed 20 times, Failed 7 times)

Stack Traces | 0.02s run time
=== RUN   TestListDir/depth_0_lists_only_root_directory
=== PAUSE TestListDir/depth_0_lists_only_root_directory
=== CONT  TestListDir/depth_0_lists_only_root_directory
    filesystem_test.go:96: 
        	Error Trace:	.../tests/envd/filesystem_test.go:96
        	Error:      	Received unexpected error:
        	            	unavailable: 502 Bad Gateway
        	Test:       	TestListDir/depth_0_lists_only_root_directory
--- FAIL: TestListDir/depth_0_lists_only_root_directory (0.02s)
github.com/e2b-dev/infra/tests/integration/internal/tests/envd::TestListDir/depth_1_lists_root_directory

Flake rate in main: 17.27% (Passed 91 times, Failed 19 times)

Stack Traces | 0.01s run time
=== RUN   TestListDir/depth_1_lists_root_directory
=== PAUSE TestListDir/depth_1_lists_root_directory
=== CONT  TestListDir/depth_1_lists_root_directory
    filesystem_test.go:96: 
        	Error Trace:	.../tests/envd/filesystem_test.go:96
        	Error:      	Received unexpected error:
        	            	unavailable: 502 Bad Gateway
        	Test:       	TestListDir/depth_1_lists_root_directory
--- FAIL: TestListDir/depth_1_lists_root_directory (0.01s)
github.com/e2b-dev/infra/tests/integration/internal/tests/envd::TestListDir/depth_2_lists_first_level_of_subdirectories_(in_this_case_the_root_directory)

Flake rate in main: 50.00% (Passed 1 times, Failed 1 times)

Stack Traces | 0.01s run time
=== RUN   TestListDir/depth_2_lists_first_level_of_subdirectories_(in_this_case_the_root_directory)
=== PAUSE TestListDir/depth_2_lists_first_level_of_subdirectories_(in_this_case_the_root_directory)
=== CONT  TestListDir/depth_2_lists_first_level_of_subdirectories_(in_this_case_the_root_directory)
    filesystem_test.go:96: 
        	Error Trace:	.../tests/envd/filesystem_test.go:96
        	Error:      	Received unexpected error:
        	            	unavailable: 502 Bad Gateway
        	Test:       	TestListDir/depth_2_lists_first_level_of_subdirectories_(in_this_case_the_root_directory)
--- FAIL: TestListDir/depth_2_lists_first_level_of_subdirectories_(in_this_case_the_root_directory) (0.01s)
github.com/e2b-dev/infra/tests/integration/internal/tests/envd::TestListDir/depth_3_lists_all_directories_and_files

Flake rate in main: 19.40% (Passed 54 times, Failed 13 times)

Stack Traces | 0.01s run time
=== RUN   TestListDir/depth_3_lists_all_directories_and_files
=== PAUSE TestListDir/depth_3_lists_all_directories_and_files
=== CONT  TestListDir/depth_3_lists_all_directories_and_files
    filesystem_test.go:96: 
        	Error Trace:	.../tests/envd/filesystem_test.go:96
        	Error:      	Received unexpected error:
        	            	unavailable: 502 Bad Gateway
        	Test:       	TestListDir/depth_3_lists_all_directories_and_files
--- FAIL: TestListDir/depth_3_lists_all_directories_and_files (0.01s)

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Comment thread spec/openapi-dashboard.yml
#3361 moved these routes from /admin/v1 to /v1/management but touched
only the path keys, so the surface has carried three names for one
concept ever since: /v1/management paths, AdminControlPlane* schemas,
and a workspace-admin tag. Searching for the management contract finds
none of the types that define it.

Renames the seven schemas to Management* and the tag to management.
AdminJWTAuth stays: it names a credential rather than the surface, and
sits beside AdminApiKeyAuth, which is a different scheme entirely --
which is also why leaving Admin on the schemas had become actively
misleading rather than merely stale.

Folded in here rather than deferred because six more handlers are about
to be written against these types, and the caller's call sites are about
to multiply. The rename only gets more expensive.

Breaks the caller on its next spec sync: four references in
controlplanes/management.go. Loud and mechanical.
operationIds gain a management prefix, so the generated handler methods
read ManagementUpsertProject rather than UpsertProject. Worth doing on a
shared ServerInterface: without it these seven sit in the same namespace
as the dashboard's own operations, and UpsertProject says nothing about
which surface it answers on.

Tag becomes control-plane-management, which is what these operations
actually are -- the workspace-admin name predates the routes moving off
/admin/v1 and described the caller rather than the surface.

Handler files renamed to match. Leaving workspace_admin.go next to a
management tag, management paths and Management* schemas would have kept
a fourth name for the one concept the previous commit set out to unify.

Every reference the caller holds now changes on its next sync: seven
method names and four type names in controlplanes/management.go. All
mechanical, all compile failures.
The seven were scaffolded into a single management.go, which is not how
the rest of this directory is organised -- admin_users_delete.go,
template_get.go, teams_list.go are all one operation each. Splitting
them means a PR implementing one handler touches one file, and the
surface is legible from a directory listing.

Named resource-first to match the existing files, so the project
operations sort together rather than by verb.

sendNotImplemented moves to utils_management.go, following the utils_
convention already here. It loses a caller with every handler that lands
and disappears with the last one.

management_test.go becomes management_contract_test.go: it never tested
management.go, it pins the wire shapes the caller depends on.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: da55e1e3c1

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

InvalidateTeamCache cleared team-<id> and the API key hashes, but not
the per-member entries ValidateAuthProviderTeam populates. Those hold a
full Team including limits, so the browser session path -- bearer token
plus X-Team-ID, which gates sandbox creation -- kept enforcing the old
limits until the entry expired.

That made the invalidation this PR added only partly effective, and it
was already only partly effective for the other caller: killing a team's
sandboxes after blocking it left the same entries stale.

Adds GetTeamMemberIDs and sweeps the member keys alongside the rest, so
"invalidate this team" means all three key kinds rather than two of
them. The docstring now lists them, because the failure from missing one
is stale auth on some paths and not others, which is harder to find than
no invalidation at all.

The query is covered against a team with members and a team without.
Sweeping itself is not, for the same reason the API key hash loop is
not: packages/auth has no Redis harness, and standing one up is out of
proportion to a loop that mirrors the one above it.

Also drops six int64 conversions in types/teams.go that became redundant
when #3429 widened TeamLimit. Those were failing lint on main -- I had
checked db and dashboard-api after that change but not the package that
consumes the type.
@ben-fornefeld
ben-fornefeld merged commit ec1ed29 into main Jul 29, 2026
51 checks passed
@ben-fornefeld
ben-fornefeld deleted the management-upsert-project-limits branch July 29, 2026 10:32
charlie-e2b added a commit that referenced this pull request Jul 30, 2026
🤖 I have created a release *beep* *boop*
---


## 0.0.1 (2026-07-30)


### Features

* add workspace admin API foundations
([#3314](#3314))
([0f72030](0f72030))
* **api:** LD-gated ClickHouse read switcher
([#3061](#3061))
([29e74ca](29e74ca))
* **api:** soft-delete build layers in DB on user delete
([#3121](#3121))
([ee88776](ee88776))
* **auth:** support admin token team auth
([#2934](#2934))
([5496666](5496666))
* **auth:** verifiers on one axis, and a reusable authenticator
constructor ([#3423](#3423))
([923b99b](923b99b))
* **dashboard-api:** add internal admin route for deleting a user
([#2986](#2986))
([ecc1291](ecc1291))
* **dashboard-api:** add internal team creation
([#2824](#2824))
([375051b](375051b))
* **dashboard-api:** add OIDC admin user bootstrap endpoint
([#2841](#2841))
([6a7a59e](6a7a59e))
* **dashboard-api:** add Ory user profile provider and auth middleware
fix ([#2840](#2840))
([30d40d2](30d40d2))
* **dashboard-api:** add template tags handlers
([#2885](#2885))
([bf52a4b](bf52a4b))
* **dashboard-api:** batch member sync route, and unenumerate
project_type ([#3427](#3427))
([cc16acf](cc16acf))
* **dashboard-api:** expose auth profile admin routes
([#2743](#2743))
([b673a10](b673a10))
* **dashboard-api:** flag sandboxes past data retention
([#3102](#3102))
([9b162bf](9b162bf))
* **dashboard-api:** implement upsertProjectLimits
([#3438](#3438))
([ec1ed29](ec1ed29))
* **dashboard-api:** include build resources in /builds response
([#3009](#3009))
([bf49c32](bf49c32))
* **dashboard-api:** map Ory SSO organizations to E2B teams
([#3094](#3094))
([dbd098f](dbd098f))
* **dashboard-api:** populate Ory identity external_id on admin
bootstrap ([#3062](#3062))
([6c51232](6c51232))
* **dashboard-api:** project upsert, member sync and user purge
([#3442](#3442))
([f997c39](f997c39))
* **dashboard-api:** templates list pagination
([#2904](#2904))
([6882463](6882463))
* **db:** add project_limits, an override the limits owner can write
([#3429](#3429))
([021c2a4](021c2a4))
* improve templates list sorting
([#2983](#2983))
([51ad7ff](51ad7ff))
* **otel:** instrument auth service HTTP client with otelhttp
([#2722](#2722))
([69b085d](69b085d))
* per-team events TTL limit (tier + addons)
([#3181](#3181))
([f76b2cb](f76b2cb))


### Bug Fixes

* added api and orch
([#3454](#3454))
([fda5e45](fda5e45))
* **api:** copy auth/internal into api and dashboard-api image builds
([#3323](#3323))
([bda1fee](bda1fee))
* **api:** invalidate auth cache on API key deletion
([#3324](#3324))
([8b02910](8b02910))
* correct 3 CVES ([#3218](#3218))
([076823b](076823b))
* **dashboard-api:** avoid repeated Ory bootstrap provisioning
([#2940](#2940))
([da5ce59](da5ce59))
* **dashboard-api:** drop removed read-replica accessor in provisioning
tests ([#3340](#3340))
([6addc91](6addc91))
* **dashboard-api:** pass signup metadata to billing provisioning
([#2978](#2978))
([d0ea5b4](d0ea5b4))
* **dashboard-api:** set Ory external_id only after the bootstrap commit
([#3133](#3133))
([00ad04b](00ad04b))
* push client-proxy, dashboard-api, and docker-reverse-proxy image…
([#2953](#2953))
([1d930ee](1d930ee))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: e2b-release-please[bot] <298072688+e2b-release-please[bot]@users.noreply.github.com>
Co-authored-by: Charlie Wyse <charlie.wyse@e2b.dev>
charlie-e2b pushed a commit that referenced this pull request Jul 31, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.1.0](dashboard-api-v0.0.1...dashboard-api-v0.1.0)
(2026-07-31)


### Features

* add workspace admin API foundations
([#3314](#3314))
([0f72030](0f72030))
* **api:** LD-gated ClickHouse read switcher
([#3061](#3061))
([29e74ca](29e74ca))
* **api:** soft-delete build layers in DB on user delete
([#3121](#3121))
([ee88776](ee88776))
* **auth:** support admin token team auth
([#2934](#2934))
([5496666](5496666))
* **auth:** verifiers on one axis, and a reusable authenticator
constructor ([#3423](#3423))
([923b99b](923b99b))
* **dashboard-api:** add internal admin route for deleting a user
([#2986](#2986))
([ecc1291](ecc1291))
* **dashboard-api:** add internal team creation
([#2824](#2824))
([375051b](375051b))
* **dashboard-api:** add OIDC admin user bootstrap endpoint
([#2841](#2841))
([6a7a59e](6a7a59e))
* **dashboard-api:** add Ory user profile provider and auth middleware
fix ([#2840](#2840))
([30d40d2](30d40d2))
* **dashboard-api:** add template tags handlers
([#2885](#2885))
([bf52a4b](bf52a4b))
* **dashboard-api:** batch member sync route, and unenumerate
project_type ([#3427](#3427))
([cc16acf](cc16acf))
* **dashboard-api:** expose auth profile admin routes
([#2743](#2743))
([b673a10](b673a10))
* **dashboard-api:** flag sandboxes past data retention
([#3102](#3102))
([9b162bf](9b162bf))
* **dashboard-api:** implement upsertProjectLimits
([#3438](#3438))
([ec1ed29](ec1ed29))
* **dashboard-api:** include build resources in /builds response
([#3009](#3009))
([bf49c32](bf49c32))
* **dashboard-api:** map Ory SSO organizations to E2B teams
([#3094](#3094))
([dbd098f](dbd098f))
* **dashboard-api:** populate Ory identity external_id on admin
bootstrap ([#3062](#3062))
([6c51232](6c51232))
* **dashboard-api:** project upsert, member sync and user purge
([#3442](#3442))
([f997c39](f997c39))
* **dashboard-api:** templates list pagination
([#2904](#2904))
([6882463](6882463))
* **db:** add project_limits, an override the limits owner can write
([#3429](#3429))
([021c2a4](021c2a4))
* improve templates list sorting
([#2983](#2983))
([51ad7ff](51ad7ff))
* **otel:** instrument auth service HTTP client with otelhttp
([#2722](#2722))
([69b085d](69b085d))
* per-team events TTL limit (tier + addons)
([#3181](#3181))
([f76b2cb](f76b2cb))


### Bug Fixes

* added api and orch
([#3454](#3454))
([fda5e45](fda5e45))
* **api:** copy auth/internal into api and dashboard-api image builds
([#3323](#3323))
([bda1fee](bda1fee))
* **api:** invalidate auth cache on API key deletion
([#3324](#3324))
([8b02910](8b02910))
* correct 3 CVES ([#3218](#3218))
([076823b](076823b))
* creating whitespace to test publish
([#3476](#3476))
([5158cc9](5158cc9))
* **dashboard-api:** avoid repeated Ory bootstrap provisioning
([#2940](#2940))
([da5ce59](da5ce59))
* **dashboard-api:** drop removed read-replica accessor in provisioning
tests ([#3340](#3340))
([6addc91](6addc91))
* **dashboard-api:** pass signup metadata to billing provisioning
([#2978](#2978))
([d0ea5b4](d0ea5b4))
* **dashboard-api:** set Ory external_id only after the bootstrap commit
([#3133](#3133))
([00ad04b](00ad04b))
* push client-proxy, dashboard-api, and docker-reverse-proxy image…
([#2953](#2953))
([1d930ee](1d930ee))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: e2b-release-please[bot] <298072688+e2b-release-please[bot]@users.noreply.github.com>
jakubno pushed a commit that referenced this pull request Aug 3, 2026
The first `/v1/management` handler to leave 501, now that
`project_limits` (#3429) gives it somewhere to write.

## What it does

Stores a project's effective limits exactly as sent. This side does **no
arithmetic** — the caller owns plans and add-ons and has already
resolved them. That's the point of the push, and it's what lets
`team_limits` prefer this row over the tier it would otherwise compute
from.

## Contract: two missing dimensions

`tiers` gained `default_free_disk_size_mb` and `max_disk_size_mb`, and
the view exposes both — but `AdminControlPlaneProjectLimits` carried
only seven fields, so an override could never have set the other two.

Both added, **required** like the rest: a partial override has no
meaning against a table whose columns are all `NOT NULL`.

This is a breaking contract change for the caller, and deliberately so —
belt's `exhaustruct` rule is scoped to these generated types precisely
to make a new field a build failure rather than a silently-zero payload.

## Errors are distinguished, not collapsed

| Failure | Status | Why |
|---|---|---|
| Foreign key violation | **404** | The team is the row's only FK, so it
means the project is unknown here — not that the payload was bad |
| Check violation | **400** | The caller sent a combination the column
set refuses. Currently free disk above the ceiling, which the request
schema can't express |
| Anything else | 500 | Ours |

Adds `dberrors.IsCheckViolation` alongside the existing helpers.

## Cache invalidation, and a bug it uncovered

The team cache carries limits, so **without invalidating it the write is
invisible until the entry expires.**

Review found `InvalidateTeamCache` was itself incomplete: it cleared
`team-<id>` and the API-key hashes but not the per-member entries
`ValidateAuthProviderTeam` populates — which hold a full `Team`
including limits, and gate sandbox creation on the browser-session path.
It now sweeps all three key kinds via a new `GetTeamMemberIDs`.

That was **already a bug** for the other caller:
`admin_kill_team_sandboxes` invalidates after blocking a team and left
the same entries stale.

The handler logs rather than fails: the row is already committed, and
returning an error would invite a retry that can't improve on a stale
cache.

## Tests

Driven through the handler, asserting through `team_limits` rather than
the table — so the two are proven wired together rather than assumed.
Idempotency is covered because the caller retries.

The 404, the 400 and the cache invalidation are each mutation-verified:
removing the FK mapping, the check mapping, or the invalidation call
fails exactly the intended test.

## Verification

`packages/db` and `dashboard-api` build, vet, test and lint clean.
Codegen idempotent.

## Also: finishing the management rename

#3361 moved these routes from `/admin/v1` to `/v1/management` but
touched only the path keys. The surface has carried three names for one
concept ever since:

| Layer | Was | Now |
|---|---|---|
| Paths | `/v1/management/*` | unchanged |
| Schemas | `AdminControlPlane*` | **`Management*`** |
| operationIds | `upsertProject` | **`managementUpsertProject`** |
| Handler methods | `UpsertProject` | **`ManagementUpsertProject`** |
| Tag | `workspace-admin` | **`control-plane-management`** |
| Handler files | one `workspace_admin.go` | **one `management_*.go` per
operation** |
| Scheme | `AdminJWTAuth` | unchanged |

`AdminJWTAuth` stays — it names a credential, not the surface, and sits
beside `AdminApiKeyAuth`, which is a different scheme entirely. That
adjacency is also why `Admin` on the schemas had become actively
misleading rather than merely stale.

Folded in here rather than deferred because six more handlers are about
to be written against these types, and the caller's call sites are about
to multiply. It only gets more expensive.

The operationId prefix matters on a shared `ServerInterface`: without it
these seven sit in the same namespace as the dashboard's own operations,
and `UpsertProject` says nothing about which surface it answers on.

**Breaks the caller on its next spec sync** — seven method names and
four type names in `controlplanes/management.go`. All mechanical, all
compile failures, same coordination as the two required fields above.

Handler files are now one per operation, matching the rest of the
directory (`admin_users_delete.go`, `template_get.go`, `teams_list.go`)
rather than the single scaffolded file the seven arrived in. A PR
implementing one handler touches one file, and the surface is legible
from `ls`. `sendNotImplemented` moved to `utils_management.go` and loses
a caller with each handler that lands.

---

Also drops six `int64` conversions in `packages/auth/pkg/types/teams.go`
that became redundant when #3429 widened `TeamLimit`. They were
**failing lint on main** — after that change I checked `db` and
`dashboard-api` but not the package that consumes the type.
jakubno pushed a commit that referenced this pull request Aug 3, 2026
🤖 I have created a release *beep* *boop*
---


## 0.0.1 (2026-07-30)


### Features

* add workspace admin API foundations
([#3314](#3314))
([0f72030](0f72030))
* **api:** LD-gated ClickHouse read switcher
([#3061](#3061))
([29e74ca](29e74ca))
* **api:** soft-delete build layers in DB on user delete
([#3121](#3121))
([ee88776](ee88776))
* **auth:** support admin token team auth
([#2934](#2934))
([5496666](5496666))
* **auth:** verifiers on one axis, and a reusable authenticator
constructor ([#3423](#3423))
([f68e713](f68e713))
* **dashboard-api:** add internal admin route for deleting a user
([#2986](#2986))
([ecc1291](ecc1291))
* **dashboard-api:** add internal team creation
([#2824](#2824))
([375051b](375051b))
* **dashboard-api:** add OIDC admin user bootstrap endpoint
([#2841](#2841))
([6a7a59e](6a7a59e))
* **dashboard-api:** add Ory user profile provider and auth middleware
fix ([#2840](#2840))
([30d40d2](30d40d2))
* **dashboard-api:** add template tags handlers
([#2885](#2885))
([bf52a4b](bf52a4b))
* **dashboard-api:** batch member sync route, and unenumerate
project_type ([#3427](#3427))
([6d8dc38](6d8dc38))
* **dashboard-api:** expose auth profile admin routes
([#2743](#2743))
([b673a10](b673a10))
* **dashboard-api:** flag sandboxes past data retention
([#3102](#3102))
([9b162bf](9b162bf))
* **dashboard-api:** implement upsertProjectLimits
([#3438](#3438))
([f4ee390](f4ee390))
* **dashboard-api:** include build resources in /builds response
([#3009](#3009))
([bf49c32](bf49c32))
* **dashboard-api:** map Ory SSO organizations to E2B teams
([#3094](#3094))
([dbd098f](dbd098f))
* **dashboard-api:** populate Ory identity external_id on admin
bootstrap ([#3062](#3062))
([6c51232](6c51232))
* **dashboard-api:** project upsert, member sync and user purge
([#3442](#3442))
([8c90702](8c90702))
* **dashboard-api:** templates list pagination
([#2904](#2904))
([6882463](6882463))
* **db:** add project_limits, an override the limits owner can write
([#3429](#3429))
([5ab6259](5ab6259))
* improve templates list sorting
([#2983](#2983))
([51ad7ff](51ad7ff))
* **otel:** instrument auth service HTTP client with otelhttp
([#2722](#2722))
([69b085d](69b085d))
* per-team events TTL limit (tier + addons)
([#3181](#3181))
([f76b2cb](f76b2cb))


### Bug Fixes

* added api and orch
([#3454](#3454))
([d56e0a8](d56e0a8))
* **api:** copy auth/internal into api and dashboard-api image builds
([#3323](#3323))
([bda1fee](bda1fee))
* **api:** invalidate auth cache on API key deletion
([#3324](#3324))
([8b02910](8b02910))
* correct 3 CVES ([#3218](#3218))
([076823b](076823b))
* **dashboard-api:** avoid repeated Ory bootstrap provisioning
([#2940](#2940))
([da5ce59](da5ce59))
* **dashboard-api:** drop removed read-replica accessor in provisioning
tests ([#3340](#3340))
([6addc91](6addc91))
* **dashboard-api:** pass signup metadata to billing provisioning
([#2978](#2978))
([d0ea5b4](d0ea5b4))
* **dashboard-api:** set Ory external_id only after the bootstrap commit
([#3133](#3133))
([00ad04b](00ad04b))
* push client-proxy, dashboard-api, and docker-reverse-proxy image…
([#2953](#2953))
([1d930ee](1d930ee))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: e2b-release-please[bot] <298072688+e2b-release-please[bot]@users.noreply.github.com>
Co-authored-by: Charlie Wyse <charlie.wyse@e2b.dev>
jakubno pushed a commit that referenced this pull request Aug 3, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.1.0](dashboard-api-v0.0.1...dashboard-api-v0.1.0)
(2026-07-31)


### Features

* add workspace admin API foundations
([#3314](#3314))
([0f72030](0f72030))
* **api:** LD-gated ClickHouse read switcher
([#3061](#3061))
([29e74ca](29e74ca))
* **api:** soft-delete build layers in DB on user delete
([#3121](#3121))
([ee88776](ee88776))
* **auth:** support admin token team auth
([#2934](#2934))
([5496666](5496666))
* **auth:** verifiers on one axis, and a reusable authenticator
constructor ([#3423](#3423))
([f68e713](f68e713))
* **dashboard-api:** add internal admin route for deleting a user
([#2986](#2986))
([ecc1291](ecc1291))
* **dashboard-api:** add internal team creation
([#2824](#2824))
([375051b](375051b))
* **dashboard-api:** add OIDC admin user bootstrap endpoint
([#2841](#2841))
([6a7a59e](6a7a59e))
* **dashboard-api:** add Ory user profile provider and auth middleware
fix ([#2840](#2840))
([30d40d2](30d40d2))
* **dashboard-api:** add template tags handlers
([#2885](#2885))
([bf52a4b](bf52a4b))
* **dashboard-api:** batch member sync route, and unenumerate
project_type ([#3427](#3427))
([6d8dc38](6d8dc38))
* **dashboard-api:** expose auth profile admin routes
([#2743](#2743))
([b673a10](b673a10))
* **dashboard-api:** flag sandboxes past data retention
([#3102](#3102))
([9b162bf](9b162bf))
* **dashboard-api:** implement upsertProjectLimits
([#3438](#3438))
([f4ee390](f4ee390))
* **dashboard-api:** include build resources in /builds response
([#3009](#3009))
([bf49c32](bf49c32))
* **dashboard-api:** map Ory SSO organizations to E2B teams
([#3094](#3094))
([dbd098f](dbd098f))
* **dashboard-api:** populate Ory identity external_id on admin
bootstrap ([#3062](#3062))
([6c51232](6c51232))
* **dashboard-api:** project upsert, member sync and user purge
([#3442](#3442))
([8c90702](8c90702))
* **dashboard-api:** templates list pagination
([#2904](#2904))
([6882463](6882463))
* **db:** add project_limits, an override the limits owner can write
([#3429](#3429))
([5ab6259](5ab6259))
* improve templates list sorting
([#2983](#2983))
([51ad7ff](51ad7ff))
* **otel:** instrument auth service HTTP client with otelhttp
([#2722](#2722))
([69b085d](69b085d))
* per-team events TTL limit (tier + addons)
([#3181](#3181))
([f76b2cb](f76b2cb))


### Bug Fixes

* added api and orch
([#3454](#3454))
([d56e0a8](d56e0a8))
* **api:** copy auth/internal into api and dashboard-api image builds
([#3323](#3323))
([bda1fee](bda1fee))
* **api:** invalidate auth cache on API key deletion
([#3324](#3324))
([8b02910](8b02910))
* correct 3 CVES ([#3218](#3218))
([076823b](076823b))
* creating whitespace to test publish
([#3476](#3476))
([6b4177f](6b4177f))
* **dashboard-api:** avoid repeated Ory bootstrap provisioning
([#2940](#2940))
([da5ce59](da5ce59))
* **dashboard-api:** drop removed read-replica accessor in provisioning
tests ([#3340](#3340))
([6addc91](6addc91))
* **dashboard-api:** pass signup metadata to billing provisioning
([#2978](#2978))
([d0ea5b4](d0ea5b4))
* **dashboard-api:** set Ory external_id only after the bootstrap commit
([#3133](#3133))
([00ad04b](00ad04b))
* push client-proxy, dashboard-api, and docker-reverse-proxy image…
([#2953](#2953))
([1d930ee](1d930ee))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: e2b-release-please[bot] <298072688+e2b-release-please[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants