Commit Graph

563 Commits

Author SHA1 Message Date
Konsti Wohlwend
d5ed20863a
Enable dev tool Dashboard tab via development-environment probe instead of env var (#1749) 2026-07-09 20:23:07 -07:00
github-actions[bot]
105f03fb94 chore: update package versions 2026-07-06 23:34:27 +00:00
github-actions[bot]
16912e7d33 chore: update package versions 2026-07-06 05:53:33 +00:00
github-actions[bot]
0344efb844 chore: update package versions 2026-07-06 05:51:13 +00:00
github-actions[bot]
675e8608af chore: update package versions 2026-07-06 04:45:14 +00:00
github-actions[bot]
1fe5150740 chore: update package versions 2026-07-06 04:29:19 +00:00
Armaan Jain
d78405d7e7
Gate usage limit banners behind HEXCLAVE_DISABLE_PLAN_LIMITS (#1728)
## 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>
2026-07-05 21:17:21 -07:00
github-actions[bot]
56dd0f357b chore: update package versions 2026-07-06 03:17:30 +00:00
BilalG1
4988312133
feat(dashboard): manual item quantity adjustments from customer payments view (#1731)
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.

![Manual item quantity change
flow](https://gist.githubusercontent.com/BilalG1/f55a8c9055cd0626320fb3153371fa97/raw/manual-item-quantity-change-flow.gif)

<sub>WebM version:
[trimmed.webm](https://gist.githubusercontent.com/BilalG1/f55a8c9055cd0626320fb3153371fa97/raw/trimmed.webm)</sub>

## Screenshots

| Adjust dialog (positive) | Adjust dialog (negative) |
| --- | --- |
| ![Positive change with live
preview](https://gist.githubusercontent.com/BilalG1/f55a8c9055cd0626320fb3153371fa97/raw/07-adjust-dialog-positive.png)
| ![Negative
change](https://gist.githubusercontent.com/BilalG1/f55a8c9055cd0626320fb3153371fa97/raw/09-adjust-dialog-negative.png)
|

| After applying changes | Entry point on item balance rows |
| --- | --- |
| ![Balance + transaction history
updated](https://gist.githubusercontent.com/BilalG1/f55a8c9055cd0626320fb3153371fa97/raw/10-after-negative-change.png)
| ![Item balances
card](https://gist.githubusercontent.com/BilalG1/f55a8c9055cd0626320fb3153371fa97/raw/06-user-payments-item-balances.png)
|

## 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 -->
2026-07-03 18:12:42 -07:00
github-actions[bot]
976aaffd56 chore: update package versions 2026-07-02 17:12:34 +00:00
github-actions[bot]
484850e8d2 chore: update package versions
Some checks failed
all-good: Did all the other checks pass? / all-good (push) Has been cancelled
Ensure Prisma migrations are in sync with the schema / check_prisma_migrations (22.x) (push) Has been cancelled
Publish RDE dashboard release / publish-dashboard (push) Has been cancelled
Docker Server Build and Push / Docker Build and Push Server (push) Has been cancelled
Docker Server Build and Run / docker (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (mock, 22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (prod, 22.x) (push) Has been cancelled
Runs E2E API Tests with custom port prefix / build (22.x) (push) Has been cancelled
Runs E2E Fallback Tests / E2E Fallback Tests (Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Lint & build / lint_and_build (24) (push) Has been cancelled
Publish npm packages / publish (push) Has been cancelled
Publish Swift SDK to prerelease repo / publish (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
2026-07-02 16:45:16 +00:00
aadesh18
d5d2f27a96
Move queryAnalytics to server app (#1687)
Moves `queryAnalytics` from the admin app to the server app so backend
code can call it without needing an over-privileged admin key. All
existing dashboard pages that ran queries internally are updated to use
`useServerApp()` instead of `useAdminApp()`.

## What changed

- **Backend**: New `POST /api/v1/analytics/query` and `POST
/api/v1/analytics/query/timing` endpoints on the **server** app
(previously admin-only)
- **Dashboard**: `queryAnalytics` calls in Queries, Query Analytics,
Session Replays, and Sign-up Rules pages switched from `adminApp` →
`serverApp`
- **Docs**: Added SDK + REST API usage example under the Queries section
of the Analytics guide
- **REST API reference**: New `Run analytics query` and `Get analytics
query timing` endpoints visible under Server API > Analytics

## Docs — Analytics › Queries section

New paragraph + code snippet showing
`hexclaveServerApp.queryAnalytics()` from backend code.

| | Light | Dark |
|---|---|---|
| **Before** |
![](https://gist.githubusercontent.com/aadesh18/e7950872b14557cfac823a99cb90ea9f/raw/analytics-before-light-queries.png)
|
![](https://gist.githubusercontent.com/aadesh18/e7950872b14557cfac823a99cb90ea9f/raw/analytics-before-dark-queries.png)
|
| **After** |
![](https://gist.githubusercontent.com/aadesh18/e7950872b14557cfac823a99cb90ea9f/raw/analytics-after-light-queries.png)
|
![](https://gist.githubusercontent.com/aadesh18/e7950872b14557cfac823a99cb90ea9f/raw/analytics-after-dark-queries.png)
|

<details>
<summary>Full-page overview (both themes)</summary>

| Light | Dark |
|---|---|
|
![](https://gist.githubusercontent.com/aadesh18/e7950872b14557cfac823a99cb90ea9f/raw/analytics-after-light.png)
|
![](https://gist.githubusercontent.com/aadesh18/e7950872b14557cfac823a99cb90ea9f/raw/analytics-after-dark.png)
|

</details>

## REST API reference — new Server API endpoints

`POST /analytics/query` and `POST /analytics/query/timing` now appear
under **Server API > Analytics** in the Mintlify reference.

| Light | Dark |
|---|---|
|
![](https://gist.githubusercontent.com/aadesh18/e7950872b14557cfac823a99cb90ea9f/raw/mintlify-run-analytics-query-light.png)
|
![](https://gist.githubusercontent.com/aadesh18/e7950872b14557cfac823a99cb90ea9f/raw/mintlify-run-analytics-query-dark.png)
|


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added public server-access analytics query support, including a query
timing endpoint for performance details.
  * Enriched API documentation for analytics queries and results.
* **Bug Fixes**
* Switched analytics querying across the dashboard and E2E tests to use
the correct public route and server access flow.
* Increased the long-request warning threshold for the analytics query
endpoint.
* **Documentation**
* Updated OpenAPI specs and the analytics guide with REST endpoint
details and examples.
* **Breaking Changes**
* Removed analytics query support from the admin client-side flow; use
the server-access method instead.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-02 09:42:10 -07:00
github-actions[bot]
e883f112fb chore: update package versions 2026-07-01 18:33:02 +00:00
github-actions[bot]
c9612539c7 chore: update package versions 2026-06-30 23:15:23 +00:00
Konsti Wohlwend
d472283283
Use tsdown -W --watch (workspace mode) for dev builds (#1689) 2026-06-30 15:40:08 -07:00
github-actions[bot]
31b32735bf chore: update package versions 2026-06-30 20:44:13 +00:00
BilalG1
2f0d9bc6cb
fix(session-replay): gzip replay batches so large rrweb snapshots aren't dropped (#1675)
## Problem

Session replay batches are uploaded as **raw, uncompressed JSON**. A
single large rrweb full snapshot (>900 KB) is dropped client-side in
`SessionRecorder._flush`, and any batch near the server's 1 MB body
limit is rejected. Meanwhile the **analytics events** path right next
door already gzips on the wire (`encodeAnalyticsBody` + a server-side
gunzip with zip-bomb guards) — replays just never got the same
treatment.

Notably, `dev` was already **red** on the SDK test `sends a single
oversized event alone without dropping it` (the code dropped at 900 KB
while the test expected the event to be sent). This change makes that
test pass.

## What this does

Ports the proven analytics compression pattern to session replays:

- **`packages/shared` (client transport):** generalize
`encodeAnalyticsBody` → `encodeGzipJsonBody` and wire it into
`sendSessionReplayBatch`. Replays now gzip via native
`CompressionStream` (`application/octet-stream`), with the same
fallbacks: keepalive flushes send plain JSON (so the request survives
page teardown), and a missing/throwing `CompressionStream` (Safari <
16.4) degrades to plain JSON.
- **`packages/template` (SDK recorder):** replace the 900 KB hard-drop
with `MAX_SINGLE_EVENT_BYTES = 8 MB`, kept in sync with the server's
decompressed cap. The wire is gzipped downstream, so an oversized single
event is now sent compressed instead of discarded; only an event that
could never fit *decompressed* is dropped.
- **`apps/backend` (route):** add `maybeDecodeBinaryBody` (gunzip with a
1 MB compressed cap + 8 MB `maxOutputLength` zip-bomb guard) via a
`.transform` on the batch route body schema. Plain-JSON bodies pass
through untouched.

## Why gzip / CompressionStream

Benchmarked on a real rrweb capture (Puppeteer, rrweb 1.1.3): a **1.09
MiB full snapshot → 134 KiB gzip (~8×) in ~6.6 ms**. Native
`CompressionStream` adds **0 bundle bytes** and was faster than fflate.
This mirrors what PostHog (gzip, native `CompressionStream` + fflate
fallback) and Sentry (fflate deflate in a worker) both ship — both rely
on compression, not splitting, as the primary mechanism for large
events.

## Tests

- **Client SDK** (`client-interface.test.ts`): gzip+octet-stream on
non-keepalive (round-trip + size assert), plain JSON on keepalive, and
both `CompressionStream`-unavailable fallbacks.
- **SDK recorder** (`session-replay.test.ts`): existing oversized-event
test now passes; new test that a >8 MB event is dropped while a
following normal event still sends.
- **Backend e2e** (`session-replays.test.ts`): gzipped happy path
(`compressed < 1MB < raw`), invalid gzip → 400, oversized compressed →
413, zip-bomb → 400. Verified passing against a live local backend.

## Verification notes

- `@hexclave/shared` + `@hexclave/template` typecheck pass; touched
files lint clean; SDK unit tests pass.
- Two **pre-existing, unrelated** local failures (each confirmed by
stashing this change and re-running on clean `dev`): a backend typecheck
error in `oauth/ssrf-protection.test.ts` (`NODE_ENV` read-only), and 3
session-replay **quota** e2e tests failing in the payments
`setSessionReplayItemQuantity` setup helper.

## Follow-up (out of scope)

The keepalive/unload path still sends uncompressed (64 KB browser
keepalive cap), so a very large final batch on page exit can still be
lost — not addressed here.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Gzip session replay batches so large rrweb snapshots are sent instead of
dropped. Keepalive flushes cap single-event size to avoid 413s on page
unload, and limits now include a 1 KiB envelope margin to match server
caps.

- **Bug Fixes**
- Client (`@hexclave/shared`): generalized `encodeAnalyticsBody` →
`encodeGzipJsonBody`; gzip via native `CompressionStream` for
`/session-replays/batch`; keepalive and missing/throwing
`CompressionStream` fall back to JSON.
- SDK recorder (`@hexclave/template`): normal flushes allow a single
event up to `MAX_SINGLE_EVENT_BYTES = 8 MiB - 1 KiB`; keepalive flushes
cap single-event size at `MAX_BATCH_UNCOMPRESSED_BYTES = 900 KB`; send
oversized events alone and rely on gzip; drop only above those
thresholds; log a distinct 413 warning with the count of buffered events
dropped and stop the loop.
- Backend: gunzip `application/octet-stream` bodies with caps (`1 MB`
compressed, `8 MiB` decompressed); invalid gzip → 400; oversized
compressed → 413; plain JSON passes through.
- Tests: added client encoding tests for session replays; SDK tests for
>8 MiB drop, keepalive drop, and 413 warning; backend e2e for gzip,
invalid gzip, oversized compressed, and zip-bomb. The oversized-event
SDK test now passes.

- **Refactors**
  - Hoisted shared test helpers and trimmed comments.

<sup>Written for commit 855cdb052e.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1675?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**
* Session replay batch uploads can now be automatically gzipped (when
supported) to reduce request size; analytics batch uploads use the same
behavior.
* **Bug Fixes**
* Improved handling of gzipped upload bodies, with stricter validation
and safer size/decompression limits to reject invalid payloads and “zip
bomb” scenarios.
* Updated session replay buffering so oversized events are dropped only
when they exceed decompression constraints, while allowing later events
to upload.
* **Tests**
* Added client, server, and end-to-end coverage for gzip/keepalive
behavior and limit enforcement.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-30 13:13:11 -07:00
github-actions[bot]
9b52f76520 chore: update package versions 2026-06-30 18:27:55 +00:00
BilalG1
81433e5181
feat(emails): arbitrary-recipient send + Emailable on managed domains (#1681)
## Summary

Two related email changes:

### 1. Send to arbitrary email addresses
The `sendEmail` SDK function and `POST /api/v1/emails/send-email` now
accept a third recipient selector, **`emails: string[]`**, mutually
exclusive (XOR) with `user_ids` and `all_users`. These recipients don't
have to belong to a user — they're mapped to the existing internal
`custom-emails` recipient type (no associated user object, can't
unsubscribe, so intended for transactional mail).

- Response `results` items are now `{ user_id?, email? }`; the `emails`
path returns `{ email }`.
- Validation message updated to `Exactly one of user_ids, all_users, or
emails must be provided`.
- Recipient addresses are validated with the shared `emailSchema`.

### 2. Run Emailable on managed custom domains
`LowLevelEmailConfig.type` is widened `'shared' | 'standard'` →
`'shared' | 'managed' | 'standard'`, and `getEmailConfig` returns
`'managed'` for the managed-Resend branch. The Emailable deliverability
check now runs for **shared** and **managed**, and is skipped for
**`standard`** (a customer's own SMTP server or Resend API key).

**Why:** managed domains send through our single Resend *account*
(per-domain scoped keys, but shared account → account-level bounce
penalties are collateral across managed customers), so we own that
reputation. With a custom SMTP server or the customer's own Resend key,
the customer owns their deliverability, so we don't second-guess their
recipients or spend Emailable checks on their volume. The shared
dev-email wrapper remains `shared`-only.

## Tests
- `send-email.test.ts`: added arbitrary-recipient send
(single/multiple), empty-array, and validation cases (both-selectors,
no-selector, invalid email); updated the exactly-one-of snapshot.
**24/24 pass.**
- New `emails/deliverability-gating.test.ts`: shared & managed skip an
undeliverable address (`LIKELY_NOT_DELIVERABLE`), custom SMTP still
sends. **3/3 pass.**

> Note: `email-queue.test.ts` has 2 pre-existing snapshot failures (a
`margin:0rem`→`margin:0` CSS normalization in rendered email HTML) that
are unrelated to this change — confirmed by reproducing them on `dev`
with these changes stashed.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds support for sending emails to arbitrary addresses and enables
Emailable checks for managed domains to protect shared sending
reputation. The API/SDK now accept `emails: string[]` as a recipient
selector, and deliverability checks run on shared and managed sending.

- New Features
- API/SDK: `emails: string[]` recipient selector (XOR with
`user_ids`/`all_users`) in `sendEmail` and POST
/api/v1/emails/send-email. Results return `{ email }` for this path;
addresses validated via `emailSchema`. Recipients map to
transactional-only “custom-emails” (no user, no unsubscribe).
- Deliverability: email config adds `'managed'`; `getEmailConfig`
returns it for managed Resend domains. Emailable check runs for `shared`
and `managed`; skipped for `standard` (custom SMTP or customer Resend
key).

<sup>Written for commit e48cf2ddda.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1681?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**
* Emails can now be sent to arbitrary email addresses, not just existing
users.
* Email sending APIs and client options now accept an email list as a
recipient choice.

* **Bug Fixes**
* Improved recipient validation so exactly one target type must be
chosen.
* Fixed response results to consistently show either a user ID or an
email address.
* Corrected deliverability checks for managed email setups and updated
skip behavior for undeliverable addresses.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-30 11:16:25 -07:00
github-actions[bot]
ea2915faa8 chore: update package versions 2026-06-30 18:04:45 +00:00
github-actions[bot]
d962e9843a chore: update package versions 2026-06-30 17:18:40 +00:00
github-actions[bot]
2c5b2d02cd chore: update package versions 2026-06-30 01:19:04 +00:00
github-actions[bot]
092c27dd0e chore: update package versions 2026-06-27 22:00:12 +00:00
github-actions[bot]
e70e95a3af chore: update package versions 2026-06-27 00:28:49 +00:00
github-actions[bot]
c868ec31bc chore: update package versions 2026-06-26 21:28:46 +00:00
github-actions[bot]
788e7cc87c chore: update package versions 2026-06-26 21:18:23 +00:00
Konstantin Wohlwend
73b8a0c27f Fix redirect URL on devtool indicator 2026-06-26 14:12:47 -07:00
Konstantin Wohlwend
dff454f47f Document init script less 2026-06-26 13:50:48 -07:00
github-actions[bot]
ee48755ef3 chore: update package versions 2026-06-26 20:38:10 +00:00
Konstantin Wohlwend
ee93732e8e Silence HEXCLAVE_BOT_CHALLENGE_SITE_KEY warning 2026-06-26 13:30:54 -07:00
Vedanta-Gawande
164374f6c8
User page email filtering (#1668) 2026-06-26 12:27:24 -07:00
github-actions[bot]
53b0cae480 chore: update package versions 2026-06-26 19:10:16 +00:00
github-actions[bot]
fc6d111e6b chore: update package versions 2026-06-26 18:50:39 +00:00
Konsti Wohlwend
325fb791f6
fix: prevent OAuth callback race condition between startup handler and OAuthCallback component (#1671) 2026-06-26 11:44:28 -07:00
BilalG1
d2a84f5a28
fix: reduce recurring production Sentry errors (Stripe webhooks, email, session replay) (#1667)
## Summary

A cleanup pass over recurring production errors triaged from Sentry
(`stackframe-pw` org). The common thread: expected/edge-case conditions
thrown as `HexclaveAssertionError` / `captureError`, so Sentry filed
them as errors (and, for the Stripe ones, Stripe redelivered
indefinitely). Each is handled at the source or logged at the correct
severity.

| Sentry issue | Fix | Risk |
|---|---|---|
|
[STACK-BACKEND-1F5](https://stackframe-pw.sentry.io/issues/STACK-BACKEND-1F5)
— `Unknown stripe webhook type` (`invoice_payment.paid`, `payout.paid`)
| Add both to `ignoredEvents`. They fell through to the throwing `else`
and Stripe redelivered them. (`payout.failed`/`canceled`/`updated`
intentionally left unhandled for now.) | Trivial |
|
[STACK-SERVER-1ZV](https://stackframe-pw.sentry.io/issues/STACK-SERVER-1ZV)
— session-replay `413 Request body too large` | Measure event size in
UTF-8 bytes (was UTF-16 `.length`, which undercounts multibyte content);
drop a single oversized event with a warning instead of shipping a
doomed request | Low |
|
[STACK-BACKEND-140](https://stackframe-pw.sentry.io/issues/STACK-BACKEND-140)
+
[STACK-BACKEND-1F1](https://stackframe-pw.sentry.io/issues/STACK-BACKEND-1F1)
— `Unknown error while sending (test) email` | Classify refused SMTP
connections (`ECONNREFUSED`, surfaced by nodemailer as `code:
'ESOCKET'`) as a typed `CONNECTION_REFUSED` error with a real
user-facing message, instead of falling through to the `UNKNOWN`
catch-all in both the low-level sender and the send-test-email route.
Marked `canRetry` so the queued-email path reschedules with backoff. |
Low |

## Notes

- **Session replay (1ZV):** edited the `packages/template`
source-of-truth; the generated SDK copies are gitignored and regenerated
by CI (`pnpm -w run generate-sdks`). The `TextEncoder` is hoisted out of
the rrweb emit hot path to avoid per-event allocation.
- **Email classification (140/1F1):** the new `CONNECTION_REFUSED`
errorType is additive — other consumers only read `errorType` for
logging, and the send-test-email route only special-cases `UNKNOWN`, so
the new type cleanly bypasses both assertion captures. `canRetry: true`
is safe because the connection is refused before any SMTP exchange (no
message handed off → no duplicate-delivery risk); transient refusals
recover, and a persistent misconfig still fails after
`MAX_SEND_ATTEMPTS`. The one-shot send-test-email path ignores
`canRetry`, so its immediate feedback is unchanged.

## Investigated but intentionally NOT changed here

These were initially included, then reverted so we keep getting Sentry
signal while the root causes are still under investigation:

-
**[STACK-BACKEND-1GM](https://stackframe-pw.sentry.io/issues/STACK-BACKEND-1GM)**
— `Stripe webhook bad customer id`. A subscription-changed event with no
customer (the observed case was a Stripe-CLI test
`payment_intent.succeeded` against a dev-connected account). Skipping is
likely the right long-term fix, but kept the throw for now to keep
observing. Note: in live mode the same path could fire on legitimate
customerless one-time payments / guest checkouts.
-
**[STACK-BACKEND-1CN](https://stackframe-pw.sentry.io/issues/STACK-BACKEND-1CN)**
— `Recovered N stale outgoing request(s)`. This is a self-healing
recovery notice (0 user impact); the underlying cause is the poller
process dying between the claim `UPDATE` and the delete. Kept at
`captureError` to keep collecting data on how often / why it happens.

## Verification
- `typecheck` clean: `@hexclave/backend`, `@hexclave/template`,
`@hexclave/js`, `@hexclave/react`, `@hexclave/next`,
`@hexclave/tanstack-start`
- `eslint` clean on all touched files
2026-06-25 14:48:49 -07:00
BilalG1
b8e384493d
feat(payments): unified customers table + Create checkout everywhere (#1666)
## Summary

Two related payments/dashboard changes:

1. **Customer page rework** (`/projects/<id>/payments/customers`) —
replaces the old single-customer selector page with **one unified
table** of users, teams, and custom customers, with filtering and
search. Clicking a row opens a customer view that renders the **same
data as the payments tab** on the user/team detail pages, plus a button
to return to the list.
2. **"Create checkout" everywhere** — the existing checkout dialog is
now reachable from every relevant surface (user table, team table,
user/team detail payments tabs, product detail page, product-lines
cards, and the customer detail view), all driven by **one shared
dialog**.

> ℹ️ Screenshots are from a local dev environment (test-mode banner +
dev-tools rail are dev-only chrome).

---

## 1. Customers page rework

A single `DataGrid` lists users + teams + custom customers (custom ones
are derived from transactions, since they aren't otherwise enumerable).
It uses the existing table/filter components: a **Type** filter (All /
Users / Teams / Custom) and quick-search.

![Customers
table](https://gist.githubusercontent.com/BilalG1/20311c5660f8ad04a5b651b1c6bcbc5f/raw/01-customers-table.png)

Filtering by type — e.g. **Custom** customers surfaced from transaction
history:

![Customers filtered to
Custom](https://gist.githubusercontent.com/BilalG1/20311c5660f8ad04a5b651b1c6bcbc5f/raw/02-customers-filter-custom.png)

Clicking a row opens a **read-only customer view** that is identical to
the payments tab on the user/team detail pages (metrics, products &
subscriptions, transaction history, item balances), with a **"Back to
customers"** button and a **Create checkout** button:

![Customer detail
view](https://gist.githubusercontent.com/BilalG1/20311c5660f8ad04a5b651b1c6bcbc5f/raw/03-customer-detail.png)

To guarantee the view is identical, the user/team payments tabs and this
page now share one generic `CustomerPaymentsSection` component keyed by
`(customerType, customerId)`.

---

## 2. Create checkout everywhere (one shared dialog)

The single `CreateCheckoutDialog` now supports:

- **Customer pre-selected** → just choose a product (tables, detail
tabs, customer detail view):

![Dialog with pre-selected
customer](https://gist.githubusercontent.com/BilalG1/20311c5660f8ad04a5b651b1c6bcbc5f/raw/04-dialog-preselected.png)

- **Locked product + customer selector** → launched from a product, the
product is fixed and you pick the customer:

![Dialog launched from a
product](https://gist.githubusercontent.com/BilalG1/20311c5660f8ad04a5b651b1c6bcbc5f/raw/05-dialog-from-product.png)

The customer selector reuses the existing searchable user/team tables
(nested dialog):

![Customer
picker](https://gist.githubusercontent.com/BilalG1/20311c5660f8ad04a5b651b1c6bcbc5f/raw/06-customer-picker.png)

Resulting checkout URL:

![Checkout URL
result](https://gist.githubusercontent.com/BilalG1/20311c5660f8ad04a5b651b1c6bcbc5f/raw/07-checkout-url-result.png)

### Entry points

**Product detail page** — in the ellipsis menu:

![Product detail ellipsis
menu](https://gist.githubusercontent.com/BilalG1/20311c5660f8ad04a5b651b1c6bcbc5f/raw/08-product-ellipsis-menu.png)

**Product-lines** product-card menu:

![Product-lines card
menu](https://gist.githubusercontent.com/BilalG1/20311c5660f8ad04a5b651b1c6bcbc5f/raw/09-productlines-card-menu.png)

**User table** row action (team table is analogous):

![User table action
menu](https://gist.githubusercontent.com/BilalG1/20311c5660f8ad04a5b651b1c6bcbc5f/raw/10-user-table-action.png)

**User detail payments tab** (and **team detail payments tab**) get a
Create-checkout button in the header:

![User payments
tab](https://gist.githubusercontent.com/BilalG1/20311c5660f8ad04a5b651b1c6bcbc5f/raw/11-user-payments-tab.png)

![Team payments
tab](https://gist.githubusercontent.com/BilalG1/20311c5660f8ad04a5b651b1c6bcbc5f/raw/12-team-payments-tab.png)

---

## Implementation notes

- **New SDK method**: `adminApp.createCheckoutUrl({ userId | teamId |
customCustomerId, productId, returnUrl? })` added to the
`@hexclave/template` **server** app (interface + impl). This is what
enables checkout for **custom** customers, which previously had no
checkout path (the old dialog only called
`customer.createCheckoutUrl(...)` on a `ServerUser`/`Team` object).
Regenerate SDKs after pulling (`pnpm -w run generate-sdks`).
- It lives on the server app (not client) deliberately: it targets an
*arbitrary* customer, which is a server/admin capability. The backend
route enforces this — for `client` auth it calls
`ensureClientCanAccessCustomer(...)` ("clients can only create purchase
URLs for their own user or teams they admin"). The client's safe, scoped
path is the existing customer-object method `user.createCheckoutUrl()` /
`team.createCheckoutUrl()`. The new method sits alongside
`grantProduct`/`createItemQuantityChange`/`getItem`, which take the same
`{ userId | teamId | customCustomerId }` shape.
- **New shared components** under
`apps/dashboard/src/components/payments/`:
- `customer-selector.tsx` — `CustomerSelector`, `CustomerTypeSelect`,
`SelectedCustomer`, `customerToMutationOptions` (extracted from the old
customers page).
- `customer-payments-section.tsx` — generic payments view;
`user-payments.tsx` / `team-payments.tsx` are now thin wrappers over it.
- `CreateCheckoutDialog` reworked to take a unified `customer`
descriptor and the new selector / locked-product props.

## Testing

- `pnpm typecheck` and `pnpm lint` pass.
- Manually verified end-to-end against a seeded local project (test
mode): generated real checkout URLs for a **custom** customer and a
**team**, exercised every entry point above, the type filter
(All/Users/Teams/Custom), search, row → detail → back, and the error
states (e.g. "product already granted", "no products for this customer
type").


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a unified Customers browser for payments with filter, quick
search, infinite scrolling, and a customer detail view.
* Enabled “Create checkout” directly from product and customer-related
screens (including list/dropdown actions).
* Added streamlined “customer payments” views (metrics, transactions,
product/subscription info, and item balances where available).
* Introduced a flexible customer picker to support user, team, and
custom customers in checkout flows.
* **Bug Fixes**
* Improved checkout validation, dialog open/close behavior, and
empty-state handling when no customer or products are available.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-25 14:48:27 -07:00
github-actions[bot]
c749cf2b62 chore: update package versions 2026-06-25 19:11:40 +00:00
Konstantin Wohlwend
6883d83ad1 Fix overlay warning 2026-06-24 11:25:58 -07:00
github-actions[bot]
09c9df410a chore: update package versions
Some checks failed
all-good: Did all the other checks pass? / all-good (push) Has been cancelled
Ensure Prisma migrations are in sync with the schema / check_prisma_migrations (22.x) (push) Has been cancelled
DB migration compat / Check if migrations changed (push) Has been cancelled
Docker Server Build and Push / Docker Build and Push Server (push) Has been cancelled
Docker Server Build and Run / docker (push) Has been cancelled
Runs E2E API Tests (Local Emulator) / E2E Tests (Local Emulator, Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (mock, 22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (prod, 22.x) (push) Has been cancelled
Runs E2E API Tests with custom port prefix / build (22.x) (push) Has been cancelled
Runs E2E Fallback Tests / E2E Fallback Tests (Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Lint & build / lint_and_build (24) (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
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
DB migration compat / No migration changes (skipped) (push) Has been cancelled
2026-06-24 01:55:58 +00:00
Konsti Wohlwend
bf265abf88
refactor: move esbuild utils from stack-shared to shared-backend (#1653)
Some checks failed
all-good: Did all the other checks pass? / all-good (push) Has been cancelled
Ensure Prisma migrations are in sync with the schema / check_prisma_migrations (22.x) (push) Has been cancelled
Docker Server Build and Push / Docker Build and Push Server (push) Has been cancelled
Docker Server Build and Run / docker (push) Has been cancelled
Runs E2E API Tests (Local Emulator) / E2E Tests (Local Emulator, Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (mock, 22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (prod, 22.x) (push) Has been cancelled
Runs E2E API Tests with custom port prefix / build (22.x) (push) Has been cancelled
Runs E2E Fallback Tests / E2E Fallback Tests (Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Lint & build / lint_and_build (24) (push) Has been cancelled
Publish npm packages / publish (push) Has been cancelled
Publish Swift SDK to prerelease repo / publish (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
2026-06-23 16:23:04 -07:00
Konsti Wohlwend
a6c46db72c
perf: prefetch email theme previews during earlier onboarding steps (#1655) 2026-06-23 15:46:03 -07:00
Konstantin Wohlwend
012098e1a9 Change Quick Sign Up email address 2026-06-23 13:23:17 -07:00
github-actions[bot]
781dde9a78 chore: update package versions 2026-06-23 20:06:39 +00:00
Konsti Wohlwend
7b0f430975
fix: suppress React 19 script warning in SsrScript (#1648) 2026-06-23 12:16:14 -07:00
Konsti Wohlwend
a2889fc044
fix: chunk SessionRecorder flush to stay under server 1MB body limit (#1647) 2026-06-23 12:04:46 -07:00
github-actions[bot]
cb7ea302c7 chore: update package versions 2026-06-23 18:48:44 +00:00
Konstantin Wohlwend
8820b39b37 Stop using app.urls everywhere 2026-06-23 11:22:44 -07:00
Armaan Jain
0132ec151a
Project onboarding speedup (#1596)
Speeds up project onboarding by consolidating API calls and adding
prefetching:

- Single PATCH endpoint for saving onboarding status + state together
- Background config save on welcome step (with proper retry/error
handling)
- Prefetch email themes and Stripe info for upcoming steps
- Suspense-based skeleton loaders instead of full-page spinners
- Extracted shared types and lightweight step components

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Unified onboarding progress saving for the new project flow,
persisting status plus optional onboarding state in a single update
path.
* **Improvements**
* Onboarding wizard now derives status/state from owned project data and
removes extra internal fetching/loading gates.
* Added skeleton/Suspense loading for email theme and payments steps,
with more reliable “final config”/completion sequencing.
* Admin project data now includes onboarding state; Stripe account info
uses cached retrieval.
* Backend avoids source-of-truth override changes during metadata-only
updates.
* **Tests**
* Updated onboarding wizard tests and added end-to-end coverage for
updating onboarding status/state together.
* **Documentation**
  * Added the “clickmaps” app icon to docs.
<!-- 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>
2026-06-23 10:59:38 -07:00
Armaan Jain
80ba110e78
Usage page in settings (#1595)
## Summary

Adds a Usage page under Project Settings showing the owner team's plan,
billing period, and resource consumption (dashboard admins, auth users,
emails, analytics events, session replays) with progress bars and an
upgrade CTA.

Backend aggregates usage across the team via `sumTenancyUsage`
(parallelized with `Promise.all`) and serves it through `GET
/internal/plan-usage`. Shared types in `@hexclave/shared` define the
contract consumed by the SDK and dashboard.

## Screenshots

![Usage page — light
mode](https://app.devin.ai/api/presigned_proxy?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOiJvcmdfaGwzT2d1STVWMXBYcTUwUCIsInVzZXJfaWQiOm51bGwsImJ1Y2tldF9uYW1lIjoiZGV2aW5hdHRhY2htZW50cyIsImJ1Y2tldF9rZXkiOiJhdHRhY2htZW50c19wcml2YXRlL29yZ19obDNPZ3VJNVYxcFhxNTBQLzE2ZTAyM2NkLTQzZjgtNDkyZS1hNDFkLTVmZjc1ZDg5NTQ3MSIsImlhdCI6MTc4MTU1MzY4OSwiZXhwIjoxNzgyMTU4NDg5LCJmaWxlbmFtZSI6InNjcmVlbnNob3RfZWE1YWU3YTJkNWQwNGM2NmFmYmM4NTY0YjQ2OTMxMDMucG5nIn0.O5H-gvyZ5an3wM7-CRcuyb6uFgg86cSftnAKnWh57VA)

Link to Devin session:
https://app.devin.ai/sessions/1bc3344126a442adb4f29ae373d346be
Requested by: @Developing-Gamer

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added Usage page in project settings displaying comprehensive plan
usage metrics including dashboard seats, authentication users, emails
sent, analytics events, and session replays
* Shows current usage against plan limits with visual progress
indicators
* Displays upgrade recommendations when plan limits are exceeded with
one-click upgrade functionality
  * Added Usage menu item to project settings navigation

<!-- 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>
2026-06-23 10:32:07 -07:00
Armaan Jain
bd511a6255
fix(sdk): bypass urls proxy in internal SDK methods for hosted components (#1641) 2026-06-23 10:30:54 -07:00