## What
Removes the Next.js compatibility shim layer that the ElysiaJS backend
migration (#1630) introduced, replacing it with standard Web APIs and
de-aliased local runtime helpers. Addresses N2D4's review note on #1630:
> i think we should create a followup PR which cleans these up and uses
the elysia methods directly — so we don't need a nextjs compat layer
forever
Stacked on `migrate-backend-to-elysiajs`.
## Changes
**`next/server` → standard Web APIs (eliminated)**
- `NextRequest` → `Request`, `NextResponse.json()` → `Response.json()`,
`new NextResponse(...)` → `new Response(...)`, `req.nextUrl` → `new
URL(req.url)`
- Hot path (`smart-route-handler`) computes `const requestUrl = new
URL(req.url)` once
- Deleted `lib/next-compat/server.tsx`; rewrote
`server/next-request-shim.ts` → `server/backend-request.ts`
(`createBackendRequest`, returns a plain `Request`)
**`next/headers` + `next/navigation` → `lib/runtime/` (de-aliased)**
- `git mv`'d the real runtime helpers (`headers`, `navigation`,
`request-context`) out of `lib/next-compat/` into `lib/runtime/`,
repointing all consumers to `@/lib/runtime/*`
- The cookie/header/redirect mechanism (AsyncLocalStorage + thrown
redirect errors, driven by `app.ts`) is unchanged — it was only *named*
after Next, never actually Next
**Config + cleanup**
- Dropped the `next/*` path aliases from `tsconfig.json`,
`vitest.config.ts`, `tsdown.config.ts` (removed `nextCompatPlugin`, the
alias map, and the `next` bundling special-case)
- Deleted `fetch.d.ts` and removed the no-op `next: { revalidate }`
fetch option in `changelog/route.tsx` (caching never worked outside the
Next runtime)
- Converted the unreferenced `proxy.tsx` so it still compiles (it's dead
code superseded by `server/middleware.ts` — deletion candidate, left out
of this PR)
No `next/*` imports remain in the backend.
## Verification
- `tsc --noEmit` — passes (exit 0)
- `eslint` on all changed files — clean
## Notes / not in scope
- This removes the **`next/*` façade**. It keeps the underlying
ALS-based request-context mechanism rather than threading Elysia's
native context (`set.cookie`, `set.redirect`) into every handler —
that's a much larger change touching handler signatures across the whole
API surface.
- One internal redirect digest string is still `"NEXT_REDIRECT"`
(matched in both `navigation.tsx` and `app.ts`); renamable but cosmetic.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Remove the `next/*` compatibility layer and switch the backend to
standard Web APIs. This simplifies handlers and config while keeping the
ALS request context and redirect behavior unchanged.
- **Refactors**
- Replaced `NextRequest`/`NextResponse` and `req.nextUrl` with
`Request`/`Response` and `new URL(req.url)` across handlers, proxies,
health/unsubscribe routes, and IDP endpoints.
- Moved runtime helpers to `@/lib/runtime/*` (`headers`, `navigation`,
`request-context`) and updated imports.
- Added `server/backend-request.ts` to build backend `Request`s with
merged headers; removed `server/next-request-shim.ts` and
`lib/next-compat/server.tsx`.
- Updated proxy rewrite to set `x-middleware-rewrite`; IDP routes now
return `Response` directly with 307/308 mapping.
- Removed the `next:{revalidate}` fetch option; deleted
`lib/next-compat/fetch.d.ts`.
- Dropped `next/*` aliases from `tsconfig`, `tsdown`, and `vitest`;
route registry/types now use `Request`.
- **Migration**
- Import from `@/lib/runtime/headers` and `@/lib/runtime/navigation`
instead of `next/headers` and `next/navigation`.
- Route handlers should accept `Request` and use `new URL(req.url)` for
URL parsing.
<sup>Written for commit b4a534ff36.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1652?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: mantra <mantra@stack-auth.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Bilal Godil <bg2002@gmail.com>
DB migration compat / Back-compat — Current branch migrations with ${{ needs.check-migrations-changed.outputs.base_branch }} branch code (push) Has been cancelled
DB migration compat / Forward-compat — Current branch code with ${{ needs.check-migrations-changed.outputs.base_branch }} branch migrations (push) Has been cancelled
## Summary
New "CLI Auth" app registered in `apps-config.ts` (alpha, parent:
authentication) and `apps-frontend.tsx` (TerminalWindowIcon, `/cli-auth`
route).
**Backend** — `GET /internal/cli-auth`:
- Queries `CliAuthAttempt` (last 50) with computed status from
`usedAt`/`refreshToken`/`expiresAt`
- Joins claimed `refreshToken` values against `ProjectUserRefreshToken`
to find active CLI sessions + user info via `ProjectUser`
- Returns `{ summary, recent_attempts, active_cli_users }`
**Dashboard** — `/cli-auth/page-client.tsx`:
- Fetches via `hexclaveAppInternalsSymbol` →
`sendRequest("/internal/cli-auth", {}, "admin")`
- Renders KPI cards (total/completed/expired/active), active sessions
list with last-active time, and recent attempts with status badges
- Expired sessions collapsed by default under `<details>`
Link to Devin session:
https://app.devin.ai/sessions/0868d1452a024b9da36b9d6a45044ff3
Requested by: @N2D4
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds an alpha CLI Auth dashboard to track CLI login attempts and active
refresh tokens, powered by a hidden admin-only endpoint.
- **New Features**
- Registers `cli-auth` under Authentication; adds `/cli-auth` route with
TerminalWindowIcon and docs link.
- Backend `GET /internal/cli-auth`: summary stats (incl. used_attempts),
last 50 attempts with computed status, and active CLI users by joining
CLI-issued refresh tokens (bounded).
- Dashboard fetches via admin request and shows KPIs, active sessions
(last-active/expiry), and recent attempts; expired sessions are
collapsed.
- **Bug Fixes**
- Use per-project admin app (`useAdminApp`) to avoid
ADMIN_AUTHENTICATION_REQUIRED and ensure correct tenancy scoping.
- Resolve primary emails for active sessions via `ContactChannel` join
to prevent 500s.
- Fix badges by using `DesignBadge` label prop and set expired to red;
add default cases in status switches.
- Bound token lookup to last 200 CLI-issued tokens and use a separate
COUNT(*) for accurate `active_tokens`.
- Align loading skeleton grid with the KPI layout.
- Docs: add `cli-auth` icon.
<sup>Written for commit 292859e921.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1739?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added a **CLI Auth** dashboard page with metrics for active sessions,
an expandable expired session list, and recent login attempts.
* Added a hidden internal analytics endpoint powering the dashboard
(summary, recent attempts, and active users).
* Registered **CLI Auth** in the app catalog/navigation (alpha) and
added its icon to the docs UI.
* **Bug Fixes**
* Improved request/response validation, loading/error states, and
avoided state updates after unmount.
* **Documentation**
* Updated docs indexing settings and added a redirect for a related
guide.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
DB migration compat / Back-compat — Current branch migrations with ${{ needs.check-migrations-changed.outputs.base_branch }} branch code (push) Has been cancelled
DB migration compat / Forward-compat — Current branch code with ${{ needs.check-migrations-changed.outputs.base_branch }} branch migrations (push) Has been cancelled
### Context
The scroll box virtualization on the data grid component was growing
monotonically with number of rows, leading to OOM.
### Summary of Changes
Bound the number of rows in the scroll box
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Prevent OOM by bounding unbounded infinite-scroll DataGrid instances so
virtualization can window rows. When the grid is infinite and has no
height, we cap it and observe against its own scroll container;
paginated grids are unchanged.
- **Bug Fixes**
- Apply a default `maxHeight: calc(100dvh - 16rem)` when
`paginationMode="infinite"` and no `fillHeight`/`maxHeight` is provided.
- Use the grid’s scroll container as the IntersectionObserver root in
this case; sticky header respects the effective max height.
- Add tests for default maxHeight, observer root, and to ensure
paginated grids do not get a forced maxHeight.
<sup>Written for commit e203b643cf.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1735?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
<!--
Make sure you've read the CONTRIBUTING.md guidelines:
https://github.com/hexclave/hexclave/blob/dev/CONTRIBUTING.md
-->
## Summary
This PR fixes the Users dashboard delete-dialog click behavior and makes
user mutations refresh the Users page without requiring a manual browser
reload.
The main user-facing changes are:
1. Opening the delete dialog from a Users table row no longer lets
dialog clicks fall through into row navigation.
2. Creating, deleting, or updating a user from the Users page now
refreshes the table, total-user count, and KPI cards automatically.
3. Users table profile links avoid expensive profile prefetching, so
refreshes no longer fan out into many unnecessary per-user profile
requests.
---
## Delete Dialog Click Behavior
The Users page has two delete-user entry points:
- the user profile action menu
- the Users table row action menu
Before this PR, the profile-page flow behaved correctly, but the
table-row flow could accidentally trigger row navigation while the
delete dialog was open. For example, clicking dialog content, footer
whitespace, the confirmation label, or the overlay could navigate to the
user's profile instead of simply interacting with or closing the dialog.
This PR fixes that by treating the row action area as non-row-click
territory and stopping click/double-click propagation from the action
surface.
Behavior after this change:
- Clicking outside the dialog closes it and returns to the page that
opened it.
- Clicking inside the dialog no longer navigates through the underlying
row.
- The confirmation label still toggles the required delete
acknowledgement.
- The explicit user identifier inside the dialog is the only navigation
target to that user's profile.
- The delete confirmation copy is shorter and uses a non-empty display
fallback:
- display name
- primary email
- user ID
---
## Users Page Refresh
Previously, creating or deleting a user through the Users page changed
backend state, but the table and KPI surfaces could stay stale until the
user manually refreshed the page or clicked the reload button.
This PR adds an explicit Users-page mutation refresh path:
- `UserDialog` can notify the page after create/edit succeeds.
- `DeleteUserDialog` can notify the page after delete succeeds.
- `UserTable` exposes its existing `useDataSource().reload()` function
to the page.
- The Users page refreshes the table and the metrics/count surfaces
after successful user mutations.
- The row action menu also refreshes after removing 2FA, so the row no
longer keeps showing a stale 2FA action after the update succeeds.
---
## Refresh Performance
The existing manual reload path used the broad `_refreshUsers()` SDK
invalidation, which refreshes more than this page needs. In local
testing, that broad path could combine with profile prefetching and
trigger many per-user requests for profile details, contact channels,
and OAuth providers.
This PR narrows the automatic Users-page refresh:
- table rows reload through the table data source
- total-user counts refresh through the user-count metrics endpoint
- KPI cards refresh through the metrics endpoint
- table/profile links can opt out of dashboard `UrlPrefetcher` with
`prefetch={false}`
- dense Users table profile links use `prefetch={false}`
- the delete-dialog profile link also opts out of prefetching
This keeps the generic `DataGrid` component unchanged. The Users page
owns the fact that a user mutation happened, and the table only exposes
the reload function it already has.
---
## KPI / Navigation Consistency
The Users page now keeps a page-local metrics snapshot for the total
count and KPI cards. It refreshes that snapshot after mutations and on
page restore, so navigating into a user profile and then returning with
browser back does not leave the KPI cards showing old counts.
Passive page-load/page-restore metrics refreshes use non-alert async
handling, so a background metrics failure is reported normally instead
of showing a blocking browser alert. User-triggered refreshes and
mutation-triggered refreshes still use the dashboard's alert-backed
async handling.
---
## Screenshots of the Delete User Dialog
Before
<img width="524" height="302" alt="old-user-delete-dialog"
src="https://github.com/user-attachments/assets/d75f25d3-e462-4ae3-ac3b-d133f6ddbbf6"
/>
After
<img width="520" height="281" alt="new-user-delete-dialog"
src="https://github.com/user-attachments/assets/4783f61e-76b6-45c5-8952-0f76fa48eba1"
/>
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes delete dialog click-through and makes the Users page auto-refresh
the table and metrics without flicker. KPIs and total users render
instantly from a snapshot and refresh in the background for smoother
navigation.
- **Bug Fixes**
- Block row navigation from all row-action and delete-dialog clicks; add
a non-prefetching profile link in `DeleteUserDialog` that closes the
dialog on click. Dialog now accepts `profileHref`, fires `onDeleted`,
and URL-encodes `projectId`/`user.id`; `redirectTo` still works.
- Safer links: `Link` supports `prefetch={false}` and disables internal
prefetch; applied to profile links in the table and dialogs.
- **Refactors**
- Instant metrics: preload a snapshot via
`fetchMetricsOrThrow`/`fetchMetricsUserCountsOrThrow` and pass it to
Total Users and KPI cards; auto-refresh on tab restore and after user
mutations.
- Targeted reloads: `UserTable` exposes `onReloadChange`; page, dialogs,
and 2FA removal call `onUserMutated` to reload rows and refresh metrics.
The refresh button uses the same path.
- Internals: add `sendRequest` and schema-validated metrics fetchers
with backward-compatible defaults.
<sup>Written for commit 5e5f641bbe.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1684?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* User pages now display KPI metrics and total-user counts immediately
when available, with improved loading/skeleton behavior.
* User actions (create/update/delete/2FA changes) now automatically
refresh the table and KPI data.
* Delete confirmation now includes a direct link to the user profile.
* **Bug Fixes**
* Improved navigation after user deletion using safer, encoded redirect
paths.
* Reduced duplicate error reporting for repeated metrics-loading
failures.
* Disabled unintended prefetching on user profile links for more
predictable navigation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Fixes the session replay player making the page laggy/unresponsive
during playback. Four compounding main-thread bottlenecks:
1. **Mini-tab re-seek every tick (main freeze cause).** Every 200ms TICK
emitted `sync_mini_tabs`, which calls `replayer.pause(offset)` on every
non-active tab. In rrweb v1 that's a full synchronous seek (rebuild from
last FullSnapshot + fast-forward all events), easily 100ms–1s+ of
blocking work per tab, 5×/sec. Now throttled to
`MINI_TAB_SYNC_INTERVAL_MS` (2s) in the state machine, reset on SEEK so
seeks re-sync promptly, and the executor skips tabs that aren't actually
rendered as mini thumbnails.
2. **Full-page re-render every 200ms.** Every dispatch force-rendered
the whole page component. Added `areStatesRenderEquivalent()` —
dispatches that only change playback bookkeeping fields
(`currentGlobalTimeMsForUi`, etc.) skip the React re-render; mini-tab
visibility is refreshed at the throttled sync cadence instead.
3. **Timeline re-rendered at 60fps.** The transport bar rAF loop used
`setState(currentTime)`, re-rendering the whole bar including up to 2000
marker divs every frame. Current time / progress are now written
directly to DOM refs, and the markers lane is extracted into a memoized
`TimelineMarkersLane`.
4. **Synchronous `addEvent` floods.** Background chunk processing now
yields to the event loop between chunks so feeding large batches into
live replayers doesn't block playback.
Added machine tests for the sync throttle, seek re-sync, and render
equivalence (117 passing).
Link to Devin session:
https://app.devin.ai/sessions/7d53fb70217d4ebaa77b9dbac2ba1f54
Requested by: @Developing-Gamer
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes session replay playback freezing by removing main-thread
bottlenecks in the player and timeline. Playback stays smooth and
responsive; background loads finish faster in hidden tabs.
- **Bug Fixes**
- Throttled mini-tab syncing to `MINI_TAB_SYNC_INTERVAL_MS` (2s);
re-syncs on the next tick after seeks and when buffering resumes
(regardless of page age); skips non-rendered tabs to avoid `rrweb` full
seeks; refreshes mini-tab visibility on the throttled cadence.
- Skipped React re-renders for playback bookkeeping via
`areStatesRenderEquivalent`.
- Stopped 60fps timeline re-renders by writing time/progress to DOM refs
and memoizing the markers lane.
- Yield between event chunks only when the tab is visible; skip yields
in hidden tabs to avoid clamped timers and speed up background loads.
- Added tests for sync throttling, post-seek re-sync (immune to page
age), and render-equivalence.
<sup>Written for commit 4abc80c5f7.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1727?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Performance / UX**
* Session replay playback during rapid timeline changes is smoother,
with fewer unnecessary UI refreshes.
* Timeline progress/time updates are more immediate, and event markers
render more efficiently.
* **Bug Fixes**
* Mini-tab syncing is now throttled while playing and correctly re-syncs
after seeking to keep thumbnails accurate.
* Event/chunk loading now yields between batches to reduce main-thread
blocking.
* **Tests**
* Expanded session replay mini-tab sync tests to cover throttling and
post-seek resynchronization.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: armaan <armaan@stack-auth.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
<!--
Make sure you've read the CONTRIBUTING.md guidelines:
https://github.com/hexclave/hexclave/blob/dev/CONTRIBUTING.md
-->
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Restores full interactivity in the hosted auth preview across onboarding
and project settings, replaces Radix tabs with lightweight
`HostedPreviewTabs` with ARIA and keyboard support, and enables
Enter‑to‑submit in the create‑project dialog. Also keeps onboarding
progress dots centered on a fixed rail.
- **New Features**
- Replaced Radix tabs with custom `HostedPreviewTabs` (animated
indicator, ARIA roles/ids for tabs and panels, Arrow keys/Home/End
navigation).
- Hosted-style inputs and links are now interactive; typing and tab
switching work.
- Wrapped the create-project dialog in a form with a shared submit
handler; Enter in the name field submits, the primary button is
`type="submit"`, and IME composition is guarded.
- **Bug Fixes**
- Removed `pointer-events-none`, `inert`, and overlay divs that blocked
the auth preview in onboarding and the project auth-methods page.
- Centered onboarding progress dots with a fixed-width rail
(`w-[150px]`) and a left-offset back arrow; dots keep accessible labels
and `aria-current="step"`.
<sup>Written for commit 7a7f1d27e0.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1721?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Enhanced the “Create Project” dialog with native form submit behavior
and Enter-key handling, including safer concurrent creation and clear
post-create navigation.
* Refreshed hosted authentication previews to support fully interactive
tab switching.
* **Bug Fixes**
* Improved onboarding bottom step navigation layout for more consistent
back-button and progress positioning.
* Removed interaction-blocking styling/attributes in hosted preview
screens so users can click and type normally.
* **Tests**
* Added UI-focused assertions to verify interactive preview behavior and
form submission semantics.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: armaan <armaan@stack-auth.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
## Summary
Threads `arePlanLimitsEnforced()` from the backend through the
plan-usage API response so the dashboard can hide all usage limit
banners when `HEXCLAVE_DISABLE_PLAN_LIMITS=true`.
Backend adds `are_plan_limits_enforced` to `planUsageResponseSchema`
(`.optional().default(true)` for backward compat with older backends) →
SDK surfaces it as `PlanUsage.arePlanLimitsEnforced` with `?? true`
runtime fallback (since `getPlanUsage()` returns raw JSON without yup
validation) → banner components early-return `null` when
`!arePlanLimitsEnforced`.
For the projects page (outside admin-app context, no project selected),
a server action reads the env var directly via
`getEnvVariable(\"STACK_DISABLE_PLAN_LIMITS\", \"false\")`.
Link to Devin session:
https://app.devin.ai/sessions/09ca53f13c294c8e98c7a7227a52217d
Requested by: @Developing-Gamer
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added support for a “plan limits enforced” flag to control messaging
across usage and billing-related screens.
* Projects and team invitation capacity checks now respect the same
enforcement toggle (including admin-seat invitation blocking).
* **Bug Fixes**
* Limit banners and “plan limit exceeded” alerts now only render when
enforcement is enabled (and overage/threshold conditions are met).
* Updated usage payload handling to default to enforcement enabled when
the flag is missing; added/updated tests for both enabled and disabled
scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: armaan <armaan@stack-auth.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Adds a way for dashboard users to create a **manual item quantity
change** — positive or negative — for a single customer and a single
item, directly from the customer payments view.
**Base:** `dev` → **Head:** `feat/manual-item-quantity-change` · 4
files, +219/−10
## Demo
Full flow on a user detail page: open the Payments tab, hit the ± button
on an item balance row, apply **+5** with a description, then **−3** —
balance and transaction history update live.

<sub>WebM version:
[trimmed.webm](https://gist.githubusercontent.com/BilalG1/f55a8c9055cd0626320fb3153371fa97/raw/trimmed.webm)</sub>
## Screenshots
| Adjust dialog (positive) | Adjust dialog (negative) |
| --- | --- |
| 
| 
|
| After applying changes | Entry point on item balance rows |
| --- | --- |
| 
| 
|
## What's new
- **`ItemQuantityChangeDialog`**
(`apps/dashboard/src/components/payments/item-quantity-change-dialog.tsx`)
- Signed integer quantity input (`10` adds, `-5` subtracts), validated
as a non-zero whole number, with a live `New balance: X → Y` preview.
- Optional description, stored on the transaction.
- Submits via `adminApp.createItemQuantityChange` (which passes
`allow_negative: true`, so balances may go negative — consistent with
the existing admin API semantics).
- **Entry point** — a ± icon button on every row of the *Item balances*
card in `customer-payments-section.tsx`. Since that card is shared, the
feature works on the **user detail page**, **team detail page**, and the
payments **Customers page**, i.e. all three customer types (`user` /
`team` / `custom`).
- **SDK cache fix** (`packages/template`, SDKs regenerated) —
`createItemQuantityChange` previously left the item and transaction
caches stale, so a dashboard caller would keep seeing the old quantity.
It now refreshes the affected item cache (new protected
`_refreshItemCache` helper on the server app impl, mirroring what
`item.increaseQuantity` already did) and invalidates the transactions
cache. This is what makes the balance and transaction history update
immediately after applying a change.
## Notes for reviewers
- The dialog intentionally does not expose `expiresAt` (the API supports
it) — kept the surface minimal for the first pass.
- Transaction history already knew how to render
`manual-item-quantity-change` transactions (`api_credits (+25)` detail
rows), so no changes were needed there.
- The per-row dialog receives `currentQuantity` from the same `useItem`
hook that renders the row, so the preview can never disagree with the
displayed balance.
## Test plan
- [x] `pnpm typecheck` and `pnpm lint` green
- [x] Manual e2e against the seeded Demo Project (production builds of
dashboard + backend): created a user-scoped `api_credits` item, applied
`+25`, `−10`, `+5`, `−3` on a user — balance updated live (25 → 15 → 20
→ 17), each change produced an *Item quantity change* transaction,
success/error toasts behave, invalid input (`0`, non-integers)
disables/blocks Apply
- [x] Verified the adjust button renders for user-scoped items on the
user page (team/customers pages use the identical shared component)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added an item quantity adjustment dialog for customer payments, with
live preview and “Apply change” support for manual whole-number deltas
(non-zero), plus success/error toasts.
* Payment item rows now include quick-action controls to open the
adjustment dialog directly.
* **Bug Fixes**
* Quantity updates now immediately refresh the affected item details and
keep related transaction data in sync after changes.
* Improved validation to reject invalid quantity inputs and provide
clearer inline feedback during submission.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->