Skip to content

Docs SPA migration, portal theming, onboarding, nav toolbars, and backend hardening#23

Merged
leoisadev1 merged 8 commits into
mainfrom
posthog-code/posthog-managed-reverse-proxy
Jun 14, 2026
Merged

Docs SPA migration, portal theming, onboarding, nav toolbars, and backend hardening#23
leoisadev1 merged 8 commits into
mainfrom
posthog-code/posthog-managed-reverse-proxy

Conversation

@leoisadev1

@leoisadev1 leoisadev1 commented Jun 13, 2026

Copy link
Copy Markdown
Member

Overview

Batches several independent workstreams on top of the original managed reverse-proxy change (this is the accumulated feature branch for production).

Docs site → TanStack Start SPA

  • Migrate apps/fumadocs from the Next.js App Router to a TanStack Start SPA (Vite + Nitro for server-only routes: search, sitemap, llms, schemas).
  • Remove the Next.js app/, PostCSS, and proxy.ts; add file-based routes under src/routes/.
  • New docs content for Changelog, Feedback, and Roadmap.

Portal theming

  • Four built-in presets (Amend, Cobalt, T3 Chat, Amber) plus a custom shadcn/tweakcn CSS paste mode with live preview and token-count feedback.
  • Custom CSS is parsed through a strict APPLIED_TOKENS allowlist and applied via React inline style props only (no dangerouslySetInnerHTML); block comments and @media/@supports/@container wrappers are stripped before matching :root/.dark.
  • customThemeCss capped at 50k chars server-side.

Dashboard onboarding + navigation

  • New-user onboarding flow + per-project getting-started checklist (resets per project).
  • Per-page sub-nav moved into top toolbars (dashboard, changelog, feedback, roadmap).
  • Centralized Hugeicons wrapper (@/lib/icons); shared useDisclosureTransition motion hook.

Home redesign

  • Rebuilt hero (static asset) and sections; removed the particle hero and memory/product sections; added a connect section.

Backend security hardening

  • requiresApiToken is now fail-secure in production even when AMEND_API_TOKEN is absent; new requiresGetApiToken guards sensitive GET endpoints.
  • HTTP actions call internal trusted* mutation variants (separating API-token auth from dashboard-session auth).
  • verifyGitHubWebhookSignature rejects unsigned requests in production. New security-regressions.test.ts locks these in.

PostHog managed reverse proxy (original change)

  • Browser SDK points at https://a.amend.sh to dodge ad-blocker drops; analyticsPath() strips query/hash from pageview URLs.

⚠️ Deployment note

verifyGitHubWebhookSignature is now fail-secure: a production workspace with GitHub connected but no GITHUB_WEBHOOK_SECRET will start rejecting inbound webhooks (401) after deploy. Provision GITHUB_WEBHOOK_SECRET in the production Convex environment before/with this deploy.

Validation

bun run check passes locally: lint, format, types, repo hygiene, source size, 109 tests. Reviewed across 6 local Greptile passes; all actionable findings fixed.


Created with PostHog Code

Point the PostHog browser SDK at the first-party managed reverse proxy
(a.amend.sh) instead of us.i.posthog.com so events are not blocked by ad
blockers. Set ui_host so the toolbar still resolves to us.posthog.com, and
drop VITE_POSTHOG_HOST from the env examples since the proxy host is now the
hardcoded default.

Generated-By: PostHog Code
Task-Id: 4a587277-acc5-4751-85c3-3ebdaf4355b2
@greptile-apps

greptile-apps Bot commented Jun 13, 2026

Copy link
Copy Markdown

Greptile Summary

This is a large accumulation PR covering five workstreams: a full TanStack Start SPA migration for the docs app (removing Next.js App Router), portal theming with four presets and a sandboxed custom CSS paste mode, dashboard onboarding, a home page redesign, and—most critically—backend security hardening plus the PostHog managed reverse proxy.

  • Security hardening (the highest-risk area): requiresApiToken and a new requiresGetApiToken are now fail-secure in production when AMEND_API_TOKEN is absent; verifyGitHubWebhookSignature rejects unsigned requests outside local dev; HTTP actions now call internalMutation trusted* variants, correctly separating API-token auth from dashboard-session auth. All behavior is pinned by new regression tests.
  • Portal theming: Custom CSS is parsed through a strict APPLIED_TOKENS allowlist and applied exclusively via React inline style props — no dangerouslySetInnerHTML. The 50k-char server-side cap prevents unbounded storage. The approach is safe.
  • Docs migration: All Next.js App Router files are removed and replaced with TanStack Start file-based routes; server-only surfaces (search, sitemap, llms, schemas) are retained.

Confidence Score: 5/5

Safe to merge with the deployment prerequisite actioned first: provision GITHUB_WEBHOOK_SECRET before deploying to avoid webhooks being rejected.

The security hardening changes are correct, well-tested, and well-structured. The trusted/public mutation split cleanly separates the two auth paths. Portal theming is sandboxed via an allowlist and React inline styles only. The two observations raised are both style-level: a missing useMemo in the settings panel and a silent no-op for var() references in custom CSS.

No files require special attention beyond the deployment prerequisite (GITHUB_WEBHOOK_SECRET) called out in the PR description.

Important Files Changed

Filename Overview
packages/backend/convex/httpRuntimeAuth.ts Core security hardening: requiresApiToken now fail-secure in production, new requiresGetApiToken for sensitive GETs, requiresOwnerApiToken gates on SITE_URL to allow token-free local dev
packages/backend/convex/signatures.ts verifyGitHubWebhookSignature now rejects unsigned requests in production via allowUnsigned option; correctly gates on isLocalAuthSiteUrl
packages/backend/convex/amend.ts Adds internalMutation variants (trustedXxx) for all HTTP-action-callable mutations, separating API-token auth from dashboard-session auth
apps/web/src/lib/portal-themes.ts New portal theming library: four presets, APPLIED_TOKENS allowlist, parseThemeCss parser strips comments and at-rule wrappers, portalThemeStyleVars applies tokens as React inline styles
apps/web/src/components/settings-workspace-portal-panel.tsx Portal theme settings UI with preset swatches, custom CSS textarea, and live preview; parseThemeCss is called in the render body without useMemo
scripts/security-regressions.test.ts New regression test suite locking in fail-secure auth behavior; covers GET token requirement, local dev bypass, production write auth, and analytics path stripping
packages/backend/convex/httpRestGet.ts Adds requiresGetApiToken guard for sensitive GET resources (agent-runs, settings, projects, etc.) before dispatching to resource handlers
packages/backend/convex/amendSourceIngest.ts Splits ingestSourceEventHandler into public (requires dashboard user) and trustedIngestSourceEventHandler (for HTTP actions with API token), correct separation of auth boundaries
packages/backend/convex/schemaWorkspaceCoreTables.ts Adds customThemeCss, themeAppearance, and themePreset to portalSettings schema; all optional, backward-compatible
apps/web/src/components/settings-workspace-controller-state.ts Adds theme form state (themePreset, themeAppearance, customThemeCss) with proper initialization from workspace and useEffect sync; dependencies correctly tracked
packages/backend/convex/amendWorkspaceSettingsMutationHandlers.ts 50k char cap on customThemeCss enforced server-side; themePreset stored as unvalidated string (relies on client/preset lookup for resolution)

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[HTTP Action\nrestGet / restPost] --> B{requiresGetApiToken\nor requiresApiToken?}
    B -- "No (public resource)" --> C[Execute handler directly]
    B -- "Yes (protected resource)" --> D[verifyApiToken]
    D -- "AMEND_API_TOKEN absent\n+ non-local SITE_URL" --> E["401\nMissing token config"]
    D -- "Token present\nbut wrong" --> F["401\nInvalid token"]
    D -- "Token OK" --> G{Which action?}
    G -- "source-events / changelog\n/ roadmap / deliveries..." --> H[ctx.runMutation\ninternal.amend.trustedXxx\ninternalMutation - no re-auth]
    G -- "public mutation path" --> I[api.amend.xxx\nmutation - requires dashboard user]

    J[GitHub Webhook] --> K[verifyGitHubSignature]
    K -- "No secret\n+ production SITE_URL" --> L["401\nMissing webhook secret"]
    K -- "No secret\n+ local SITE_URL" --> M[allowUnsigned = true\npass through]
    K -- "Secret present" --> N[HMAC verify]
    N -- "OK" --> H
Loading

Reviews (3): Last reviewed commit: "fix(fumadocs): declare tailwind-merge de..." | Re-trigger Greptile

Comment on lines 100 to 103
posthog.capture("$pageview", {
$current_url: window.location.href,
path,
$current_url: analyticsPath(window.location.href),
path: safePath,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 partner $current_url now receives a bare pathname (e.g. /dashboard) instead of a full absolute URL. analyticsPath returns only url.pathname, stripping the origin. PostHog uses $current_url for URL-based insights, funnels, and session replay; a relative path breaks all URL-pattern filtering in the dashboard. The path custom property carries the stripped pathname already, so $current_url should still include the origin. Score: 4/5 confident this is a real regression.

Suggested change
posthog.capture("$pageview", {
$current_url: window.location.href,
path,
$current_url: analyticsPath(window.location.href),
path: safePath,
});
posthog.capture("$pageview", {
$current_url: `${window.location.origin}${safePath}`,
path: safePath,
});

Fix in Claude Code

Comment on lines 55 to +56
api_host: optionalClientEnv("VITE_POSTHOG_HOST") ?? defaultPostHogHost,
ui_host: "https://us.posthog.com",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 partner api_host still reads VITE_POSTHOG_HOST from env (allowing overrides), but ui_host is always hardcoded to https://us.posthog.com. If a developer or CI environment sets VITE_POSTHOG_HOST to an EU-region proxy, the toolbar and PostHog app links will still route to the US region, causing broken toolbar sessions or mismatched project lookups. Consider deriving ui_host from the region implied by VITE_POSTHOG_HOST or documenting that only US-region is supported. Score: 2/5 — unlikely to affect most users but a real inconsistency for EU-region overrides.

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

Fix in Claude Code

Comment thread apps/web/src/lib/posthog.ts
…updates

Large feature batch landing on top of the managed reverse-proxy work.

Docs (apps/fumadocs)
- Migrate the docs site from the Next.js App Router to a TanStack Start SPA
  (Vite + router.tsx + file-based routes); drop the Next.js app/, postcss,
  and proxy.ts; add changelog/feedback/roadmap content.

Web app (apps/web)
- Onboarding: welcome/showcase panels, onboarding flow, and a dashboard
  onboarding checklist + model.
- Navigation: move per-page sub-nav into top toolbars (dashboard, changelog,
  feedback, roadmap).
- Icons: centralize on a Hugeicons wrapper (@/lib/icons) + brand icons.
- Portal theming: shadcn-token portal themes with a settings picker; portal rail.
- Motion: useDisclosureTransition hook + motion.css (t-* classes), MOTION.md.
- Home: rebuild hero (static hero-loop asset) and sections; remove the
  particle hero and the memory/product sections; add a connect section.
- Remove the proactivation surface; keep its design doc under docs/.

Backend (packages/backend)
- Update amend mutation handlers (content, custom domain, delivery, source
  ingest), workspace settings, REST endpoints, signatures, and schema tables.

Tooling
- Exclude generated route trees from oxfmt; refresh deps/lockfile, AGENTS.md,
  README, and env examples.

bun run check passes: lint, format, types, repo hygiene, source size, 102 tests.

Generated-By: PostHog Code
Task-Id: db14a484-b04e-45e2-82a7-a88618b6dd30
Greptile P2: the module-level DECLARATION_RE carries the /g flag, so exec()
mutates lastIndex across calls. Iterate with String.prototype.matchAll so the
parser holds no shared mutable state.

Generated-By: PostHog Code
Task-Id: db14a484-b04e-45e2-82a7-a88618b6dd30
…m CSS

Greptile pass 2:

- P1: the portal settings sync effect depended on the whole `workspace`
  object, which gets a new reference on every Convex mutation. Any unrelated
  workspace write re-ran the effect and clobbered in-progress portal edits.
  Narrow the dep array to the specific portalSettings fields the effect reads.
- P2: cap persisted `customThemeCss` at 50k chars in the update handler so a
  workspace owner can't store unbounded multi-megabyte documents.

Generated-By: PostHog Code
Task-Id: db14a484-b04e-45e2-82a7-a88618b6dd30
…ations

Greptile pass 3 (custom CSS parser):

- Strip /* ... */ block comments before token matching so a commented-out
  declaration can't shadow the live one via iteration order.
- Strip @media wrappers before extracting :root/.dark so a
  prefers-color-scheme-scoped :root can't be mistaken for the base block,
  regardless of whether it appears before or after it. @layer wrappers are
  left intact since shadcn exports nest the real tokens inside them.

Adds scripts/portal-themes.test.ts covering the happy path, the allowlist,
and both edge cases.

Generated-By: PostHog Code
Task-Id: db14a484-b04e-45e2-82a7-a88618b6dd30
…ath fallback

Greptile pass 4 (quality observations):

- Parse custom portal CSS once in the settings panel and pass the result into
  resolvePortalTheme (new optional arg) instead of re-parsing on every
  keystroke.
- Generalize the at-rule strip from @media to the conditional group rules
  (@media / @supports / @container) so a feature-query-scoped :root can't
  shadow the base block either; @layer stays intact.
- analyticsPath: use `|| "/"` so an empty path (e.g. a bare "?query") actually
  falls back to "/"; the previous `?? "/"` was unreachable.

Generated-By: PostHog Code
Task-Id: db14a484-b04e-45e2-82a7-a88618b6dd30
… open mount

Greptile pass 5:

- P1: the onboarding checklist read its dismissed flag from localStorage in a
  useState initializer that only runs on mount, so switching projects (no
  remount) carried the previous project's dismissed state and hid the new
  project's checklist. Add key={activeProject.id} at the call site so the
  per-project state resets on switch.
- P2: useDisclosureTransition reset to the pre-state in an effect even when it
  mounted already-open, flashing through two frames. Track the previous `open`
  and replay the entrance only on a real closed→open transition; open-on-mount
  now stays in the open phase. Behaviour is unchanged for the current
  (closed-on-mount) callers.

Generated-By: PostHog Code
Task-Id: db14a484-b04e-45e2-82a7-a88618b6dd30
@leoisadev1 leoisadev1 changed the title Route PostHog through managed reverse proxy (a.amend.sh) Docs SPA migration, portal theming, onboarding, nav toolbars, and backend hardening Jun 14, 2026
apps/fumadocs/src/lib/cn.ts imports tailwind-merge directly, but it was only
declared in @amend/ui. It resolved locally via hoisting, so check-types passed,
but a clean `bun install --frozen-lockfile` in CI nested it under @amend/ui
only and fumadocs' tsc failed with TS2307. Declare it on fumadocs (same
^3.6.0 as @amend/ui so they dedupe) so the frozen install resolves it.

Generated-By: PostHog Code
Task-Id: db14a484-b04e-45e2-82a7-a88618b6dd30
@leoisadev1 leoisadev1 merged commit 696f4de into main Jun 14, 2026
3 of 4 checks passed
@leoisadev1 leoisadev1 deleted the posthog-code/posthog-managed-reverse-proxy branch June 14, 2026 03:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant