From a22d5a49c1c05e7ba5a9c115ab6dafe4d8e6ef38 Mon Sep 17 00:00:00 2001 From: BilalG1 Date: Wed, 22 Apr 2026 17:27:29 -0700 Subject: [PATCH 1/3] Reduce Jaeger memory usage in local dev (#1357) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Jaeger's `all-in-one` image uses an unbounded in-memory span store by default, so on long-running local dev sessions it quietly grows until the Docker VM is under memory pressure. This caps it in two ways: - `MEMORY_MAX_TRACES=50000` — tells Jaeger to evict old traces once the in-memory ring buffer hits 50k. Plenty for local debugging without retaining every trace forever. - `mem_limit: 1g` — hard Docker memory cap as a backstop so a runaway collector can't starve the rest of the compose stack (Postgres, ClickHouse, Svix, etc.). Only touches `docker/dependencies/docker.compose.yaml` — no app code, no prod config. ## Test plan - [ ] `docker compose -f docker/dependencies/docker.compose.yaml up jaeger` starts cleanly - [ ] Jaeger UI reachable at `localhost:8107` - [ ] OTLP endpoint on `localhost:8131` still ingests spans from the API/dashboard - [ ] `docker stats` shows the jaeger container capped at ~1GB under load - [ ] Old traces get evicted once trace count exceeds 50k (verify via UI search) ## Summary by CodeRabbit ## Release Notes * **Chores** * Configured memory constraints and trace buffering optimization for the tracing service to improve resource management and system stability. --- docker/dependencies/docker.compose.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker/dependencies/docker.compose.yaml b/docker/dependencies/docker.compose.yaml index c745fea72..9aafb788f 100644 --- a/docker/dependencies/docker.compose.yaml +++ b/docker/dependencies/docker.compose.yaml @@ -148,9 +148,11 @@ services: image: jaegertracing/all-in-one:latest environment: - COLLECTOR_OTLP_ENABLED=true + - MEMORY_MAX_TRACES=50000 ports: - "${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}07:16686" # Jaeger UI - "${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}31:4318" # OTLP Endpoint + mem_limit: 1g restart: always # ================= svix ================= From 4f198bd55b0c030ce7eccaabcbf161ae107c055a Mon Sep 17 00:00:00 2001 From: BilalG1 Date: Wed, 22 Apr 2026 17:27:37 -0700 Subject: [PATCH 2/3] Fix dashboard UI bugs: webhook detail crash and http domain silent https upgrade (#1362) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes two dashboard UI bugs surfaced while auditing the project area for large user-visible issues: 1. **Webhook detail page completely broken** — the page shows a blank screen because the SvixProvider token was being set to the string `"[object Object]"`. 2. **Editing a trusted domain with an `http://` base URL silently upgrades it to `https://`** — saving the edit dialog without changing anything changes the protocol, breaking callbacks to the original host. Both are corrected with minimal, targeted changes in the dashboard app. No API, schema, or shared package changes are required. --- ## Bug 1 — Webhook detail page crashes because `svixToken + ''` yields `"[object Object]"` ### Where `apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/webhooks/[endpointId]/page-client.tsx` ### Root cause `stackAdminApp.useSvixToken()` returns an object of shape `{ token: string, url: string | null }` (see `packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts`). The page was doing: ```ts const svixToken = stackAdminApp.useSvixToken(); const [updateCounter, setUpdateCounter] = useState(0); // This is a hack to make sure svix hooks update when content changes const svixTokenUpdated = useMemo(() => { return svixToken + ''; }, [svixToken, updateCounter]); // … ``` `svixToken + ''` coerces the object to the string `"[object Object]"`, which is then passed to `` as the auth token. Every nested Svix hook (`useEndpoint`, `useEndpointSecret`, `useEndpointMessageAttempts`) authenticates with that bogus token, gets a `401 {"code":"authentication_failed","detail":"Invalid token"}` from Svix, and `getSvixResult` (`apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/webhooks/utils.tsx`) throws, crashing the page. Additional notes while in there: - `setUpdateCounter` was declared but never called anywhere, so the surrounding `useMemo`/`useState` was dead weight as well as broken. Removing it removes the dead code too. - The neighbouring list page (`webhooks/page-client.tsx`) already uses the correct shape (`svixToken.token`, `svixToken.url`), which is why the list page rendered correctly while the detail page didn't. ### Fix Pass `svixToken.token` directly to `` and drop the unused counter/memo. ```ts export default function PageClient(props: { endpointId: string }) { const stackAdminApp = useAdminApp(); const svixToken = stackAdminApp.useSvixToken(); return ( ); } ``` ### Reproduction (before fix) 1. Enable the Webhooks app on a project. 2. Create an endpoint with any URL. 3. Open the row's action menu and click **View Details**. 4. The page renders blank (Svix hooks throw 401 Invalid token; the error boundary unmounts the detail tree). URL, Description, Verification Secret, and Events History never appear. ### Before / After | Before | After | | --- | --- | | ![Webhook detail blank before fix](https://gist.githubusercontent.com/BilalG1/f31b7631cb914ea8fd0113b97d26319e/raw/bug1-webhook-detail-before.png) | ![Webhook detail renders after fix](https://gist.githubusercontent.com/BilalG1/f31b7631cb914ea8fd0113b97d26319e/raw/bug1-webhook-detail-after.png) | --- ## Bug 2 — Editing an `http://` trusted domain silently upgrades it to `https://` ### Where `apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/domains/page-client.tsx` ### Root cause In `EditDialog`, the form's `defaultValues` always set `insecureHttp: false`, regardless of the protocol of the domain being edited: ```ts defaultValues={{ addWww: props.type === 'create', domain: props.type === 'update' ? props.defaultDomain.replace(/^https?:\/\//, "") : undefined, handlerPath: props.type === 'update' ? props.defaultHandlerPath : "/handler", insecureHttp: false, // ← ignores the existing protocol }} ``` The `domain` field strips `http(s)://` for display but the protocol itself is only tracked through the `insecureHttp` switch, which lives inside the collapsed-by-default **Advanced** accordion. On submit: ```ts const protocol = values.insecureHttp ? 'http://' : 'https://'; const baseUrl = protocol + values.domain; ``` So an `http://myapp.test` entry reopens with `insecureHttp: false`, the Advanced section stays collapsed, the user sees nothing wrong, and hitting **Save** (even with zero visible changes) writes `https://myapp.test` back to config. Existing redirects from SSO / email verification flows that depend on the original `http://` host stop working. ### Fix Derive `insecureHttp` from the existing `defaultDomain` when editing: ```ts insecureHttp: props.type === 'update' ? props.defaultDomain.startsWith('http://') : false, ``` This makes the switch in the Advanced panel pre-check itself correctly and the submit path emits the preserved protocol. ### Reproduction (before fix) 1. Go to **Project Settings → Trusted Domains**. 2. Add a new domain, expand **Advanced**, toggle **Use HTTP instead of HTTPS** on, enter `myapp.test`, click **Create**. The list now shows `http://myapp.test`. 3. Click the row's **⋯ → Edit**, then **Save** without changing anything. 4. Observe the list now shows `https://myapp.test`. ### Before / After **Domain list after an edit+save:** | Before (http silently became https) | After (http preserved) | | --- | --- | | ![Domain list before](https://gist.githubusercontent.com/BilalG1/f31b7631cb914ea8fd0113b97d26319e/raw/bug4-domain-list-before.png) | ![Domain list after](https://gist.githubusercontent.com/BilalG1/f31b7631cb914ea8fd0113b97d26319e/raw/bug4-domain-list-after.png) | In the "before" screenshot, `http://myapp.test` was edited with no changes and silently became `https://myapp.test`. `http://www.myapp.test` (not edited) stayed `http://`, confirming the bug is triggered only through the edit-save path. **Edit dialog (Advanced expanded):** | Before (HTTP switch always off) | After (reflects stored protocol) | | --- | --- | | ![Edit dialog before](https://gist.githubusercontent.com/BilalG1/f31b7631cb914ea8fd0113b97d26319e/raw/bug4-edit-dialog-before.png) | ![Edit dialog after](https://gist.githubusercontent.com/BilalG1/f31b7631cb914ea8fd0113b97d26319e/raw/bug4-edit-dialog-after.png) | The "after" dialog also shows the protocol prefix label flip from `https://` to `http://` next to the input — a second visual cue that the user is editing an HTTP domain. --- ## Scope / out of scope In scope here: - The two fixes above, plus a small amount of dead-code cleanup adjacent to the first fix (the unused `updateCounter` / `useMemo` hack). Intentionally **not** included (tracked separately from the same audit — see internal notes): - Cursor pagination cache wipe across Users/Teams/Transactions tables (`data-table/common/cursor-pagination.tsx`) - Email Outbox "Scheduled At" input being reset on every keystroke and rendered in the wrong timezone (`email-outbox/page-client.tsx`) - Latent empty-group handling in the sign-up rule builder (validator + CEL emitter), which is real in code but not currently reachable through the editor UI These are broader and deserve their own PRs. ## Test plan - [ ] **Bug 1 (webhook detail):** Enable Webhooks on a project, create an endpoint, open **View Details**. Confirm URL, Description, Verification Secret, and Events History render (no 401s in the console, no blank page). Confirm the Copy button on the verification secret still copies the key. - [ ] **Bug 2 (domain edit preserves http):** Add an `http://` trusted domain. Edit it and save with no changes — list should still show `http://`. Edit again, flip the Advanced switch to HTTPS, save — list should show `https://`. Repeat with the inverse direction (start https, flip to http). - [ ] **Regression sweep:** Webhooks list page, create/delete endpoint, copy signing secret; Trusted Domains add/delete; auth-methods callbacks against an `http://localhost` domain continue to work. - [ ] `pnpm typecheck` passes locally. (`pnpm lint` was also run against the dashboard app and is clean.) ## Summary by CodeRabbit * **Bug Fixes** * Domain editing now correctly initializes and preserves the protocol type (HTTP or HTTPS) based on the existing domain configuration. --- .../projects/[projectId]/domains/page-client.tsx | 2 +- .../[projectId]/webhooks/[endpointId]/page-client.tsx | 11 ++--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/domains/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/domains/page-client.tsx index 2a6269bc2..de60fc93a 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/domains/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/domains/page-client.tsx @@ -113,7 +113,7 @@ function EditDialog(props: { addWww: props.type === 'create', domain: props.type === 'update' ? props.defaultDomain.replace(/^https?:\/\//, "") : undefined, handlerPath: props.type === 'update' ? props.defaultHandlerPath : "/handler", - insecureHttp: false, + insecureHttp: props.type === 'update' ? props.defaultDomain.startsWith('http://') : false, }} onOpenChange={props.onOpenChange} trigger={props.trigger} diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/webhooks/[endpointId]/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/webhooks/[endpointId]/page-client.tsx index 438626bf9..daca97828 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/webhooks/[endpointId]/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/webhooks/[endpointId]/page-client.tsx @@ -4,7 +4,7 @@ import { DesignAlert, DesignBadge, DesignButton, DesignCard, DesignEditableGrid, import { CopyButton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui"; import { getPublicEnvVar } from '@/lib/env'; import { CaretLeftIcon, CaretRightIcon, InfoIcon, KeyIcon, LinkIcon, TextAlignLeftIcon } from "@phosphor-icons/react"; -import { useMemo, useState } from "react"; +import { useMemo } from "react"; import { SvixProvider, useEndpoint, useEndpointMessageAttempts, useEndpointSecret } from "svix-react"; import { AppEnabledGuard } from "../../app-enabled-guard"; import { PageLayout } from "../../page-layout"; @@ -159,18 +159,11 @@ function MessageTable(props: { endpointId: string }) { export default function PageClient(props: { endpointId: string }) { const stackAdminApp = useAdminApp(); const svixToken = stackAdminApp.useSvixToken(); - const [updateCounter, setUpdateCounter] = useState(0); - - // This is a hack to make sure svix hooks update when content changes - const svixTokenUpdated = useMemo(() => { - return svixToken + ''; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [svixToken, updateCounter]); return ( From 0532a18c36cc1c15ffb787df0e2cbec2f1dee439 Mon Sep 17 00:00:00 2001 From: BilalG1 Date: Wed, 22 Apr 2026 17:28:09 -0700 Subject: [PATCH 3/3] fix(dashboard): wrap "Block new purchases" toggle in a Card (#1364) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary The **Block new purchases** toggle on the Payments → Settings page was visually out of place: it rendered as a bare `SettingSwitch` outside the `max-w-3xl` settings column, while every neighboring setting (Stripe Connection, Test Mode, Payment Methods, Platform-Managed Methods) was a full-width `Card`. This PR wraps it in a `Card` that matches the existing `TestModeToggle` pattern so it inherits the same width constraint, border, padding, title/description structure, and state-colored icon badge. **File changed:** [`apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/settings/page-client.tsx`](https://github.com/stack-auth/stack-auth/blob/fix/payments-block-new-purchases-card/apps/dashboard/src/app/(main)/(protected)/projects/%5BprojectId%5D/payments/settings/page-client.tsx) ## What was wrong Two concrete mismatches with the rest of the page: 1. **Wrong container.** The `SettingSwitch` was a direct child of `` rather than the `
` column that wraps the other settings — so it stretched to the full page width instead of the 3xl column and broke the vertical rhythm (no consistent `space-y-6` gap from the card above). 2. **Wrong style primitive.** It used the bare `SettingSwitch` row component instead of a `Card` + `CardHeader`/`CardTitle`/`CardDescription`/`CardContent` structure — so there was no border, no heading hierarchy, and no state-colored icon badge, which every other setting on the page has. ## Fix - Moved the block inside the `space-y-6 max-w-3xl` column so it's constrained and spaced like its siblings. - Replaced the `SettingSwitch` with a `Card` mirroring `TestModeToggle`: - `CardHeader` with `CardTitle` (\"Block New Purchases\") and `CardDescription` (\"Stops new checkouts while keeping existing subscriptions active.\"). - `CardContent` with an icon badge (`ProhibitIcon`) that turns red when blocking is active, plus a short \"Block new purchases\" label and the `Switch`. - Copy is intentionally minimal: one title, one sentence of description, one label next to the switch. No two-state narration. ## Visual comparison ### Pixel diff (changed pixels tinted red over the after image) 4.7% of pixels changed, all concentrated in the bottom of the settings column — everything else is pixel-identical, confirming the fix is scoped. ![pixel diff](https://gist.githubusercontent.com/BilalG1/faacb21aea28bc6acae0f527f232c38c/raw/compare_pixel_diff.png) ### Cropped before/after toggle (zoomed to the changed region) Full-viewport comparisons are noisy when the delta is a single component at the bottom. This one is cropped to the changed bbox so the card fix is the whole frame — 1s before, 1s after, looped. ![crop toggle](https://gist.githubusercontent.com/BilalG1/faacb21aea28bc6acae0f527f232c38c/raw/compare_crop_toggle.gif) ### Wipe reveal (before on the left, after swept in from the left) A vertical red sweeps across the full page, revealing the after state over the before state. Useful for spotting any unintended drift elsewhere on the page (there is none). ![wipe](https://gist.githubusercontent.com/BilalG1/faacb21aea28bc6acae0f527f232c38c/raw/compare_wipe.gif) ## Test plan - [ ] Open `/projects//payments/settings` in the dashboard. - [ ] Verify \"Block New Purchases\" renders as a `Card` with the same width as Stripe Connection / Test Mode / Payment Methods. - [ ] Toggle the switch on — icon badge turns red, config write fires (`payments.blockNewPurchases = true`, `pushable: true`). - [ ] Toggle off — icon returns to muted gray, config write fires with `false`. - [ ] Reload the page and confirm the persisted state matches the toggle. - [ ] `pnpm lint` and `pnpm typecheck` pass. ## Summary by CodeRabbit * **Improvements** * Redesigned the "Block New Purchases" toggle in payment settings with a new card-based interface and visual prohibit indicator for improved clarity and user experience. --- .../payments/settings/page-client.tsx | 56 +++++++++++++++---- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/settings/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/settings/page-client.tsx index 7ed6d1964..37554157f 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/settings/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/settings/page-client.tsx @@ -1,7 +1,9 @@ "use client"; -import { SettingSwitch } from "@/components/settings"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle, Switch, Typography } from "@/components/ui"; import { useUpdateConfig } from "@/lib/config-update"; +import { cn } from "@/lib/utils"; +import { ProhibitIcon } from "@phosphor-icons/react"; import { PageLayout } from "../../page-layout"; import { useAdminApp } from "../../use-admin-app"; import { PaymentMethods } from "./payment-methods"; @@ -14,6 +16,14 @@ export default function PageClient() { const paymentsConfig = project.useConfig().payments; const updateConfig = useUpdateConfig(); + const handleBlockNewPurchasesToggle = async (checked: boolean) => { + await updateConfig({ + adminApp, + configUpdate: { "payments.blockNewPurchases": checked }, + pushable: true, + }); + }; + return ( + + + Block New Purchases + + Stops new checkouts while keeping existing subscriptions active. + + + +
+
+
+ +
+ + Block new purchases + +
+ +
+
+
- void await updateConfig({ - adminApp, - configUpdate: { "payments.blockNewPurchases": checked }, - pushable: true, - })} - hint="Stops new checkouts while keeping existing subscriptions active." - />
); }