BETAPro is not billed during beta. Lock in the price and we will honor it at launch.Freeze this price

Changelog

Stay up to date with the latest changes in our product.

May 21, 2026

Plan 024 Phases 0–3 (production hardening)

Security and hardening

  • BYOK API key removed from Inngest event payload: scan/requested and platform-scan/requested no longer ship the OPENROUTER_API_KEY through the durable event store. Worker now fetches the key fresh via resolveOpenRouterKey(organizationId) inside step.run. Stops the key from sitting in Inngest's UI, logs, or retry payloads.
  • Fake RAG retired: retrieveContext (which embedMany'd a query embedding and then did a no-op exists() subquery) renamed to getRecentMentions and now honestly returns chronologically-recent rows. System prompt updated to say "RECENT BRAND MENTIONS (chronological, not ranked)".
  • Bulk-scan failure recovery: missing .catch reattached to runScanInBackground in bulk-execute.ts so a single failing worker no longer leaves a scan_session row stuck in pending forever.
  • Typed environment validation: new @repo/env package built on @t3-oss/env-nextjs. All process.env.* reads now flow through a Zod-validated schema (emptyStringAsUndefined: true, most fields .optional() to avoid breaking provider-gated deployments). Wired into apps/saas/config.ts and apps/marketing/config.ts.
  • Server-side password schema enforcement: Better Auth's before hook now runs passwordSchema.safeParse() against /sign-up/email, /change-password, /reset-password, /set-password. A forged request that skips the React form is rejected with a BAD_REQUEST instead of writing a weak hash to disk.
  • Impersonation banner: when session.impersonatedBy is set, a sticky red banner with a one-click stop button is rendered on every authenticated page via <ImpersonationBanner /> mounted in the authenticated layout.
  • Content-Security-Policy with per-request nonce on both proxies (apps/saas/proxy.ts + apps/marketing/proxy.ts). crypto.getRandomValues → 16-byte base64 nonce per request, propagated to layouts via x-nonce header. Default Report-Only (set CSP_REPORT_ONLY=false to enforce) so violations are observable before they break the app.
  • AuditEvent table + `@repo/audit-log`: append-only audit_event schema (actor, org, type, target, JSON metadata, IP, UA, createdAt) with indexes on actor/org/type/createdAt. recordAudit() swallows write failures (compliance prefers a missing log line to a missed login). Better Auth after hook records sign-in/sign-up, password reset/change, admin impersonate/ban/role-change/delete, two-factor enable/disable, email change. Migration 0014_shiny_prima.sql.

Performance and DX

  • `reportWebVitals` wired: <WebVitals /> mounted in root layout uses next/web-vitals. Logs to console in dev; in production beacons to NEXT_PUBLIC_WEB_VITALS_URL (no-op if unset).
  • Bundle analyzer behind `ANALYZE=true`: @next/bundle-analyzer wrapping next.config.ts. Set ANALYZE=true pnpm --filter saas build to inspect chunk sizes.

Conformance and quality

  • Fumadocs version pinned correctly: apps/docs/fumadocs-mdx set to 15.0.7 (latest 15.x; v16 doesn't exist) so the withMDX typegen + postinstall regenerate the .source/server.ts correctly.
  • `proxy.ts` export style aligned: marketing's default export switched to a named export async function proxy to match the SaaS app + agents.md "avoid default exports" guidance.
  • `types` glob fixes in three more packages (@repo/logs, @repo/storage, @repo/utils) — malformed "./**/.ts" patterns replaced with "./index.ts".
  • `type` → `interface` for the prop type alias on the @repo/ui components that needed it (button, badge, chart).
  • Floating-promise sweep: five queryClient.invalidateQueries(...) call sites that were warning under typescript/no-floating-promises now properly prefixed with void.

May 21, 2026

Features and additions

  • AI alias suggester with Tavily grounding: The "Suggest with AI" button next to the aliases field on the brand add/edit forms now runs tracking.suggestAliases, which combines a Tavily web-search of the brand (Wikipedia, About page, news, Crunchbase) with a gpt-4o call on OpenRouter. The model is shown the search snippets as grounding context and instructed to ground every alias in that text — fixes the prior failure mode where niche B2B brands were getting generic Anglo defaults like "Cognigy Inc" instead of the actual legal entity "Cognigy GmbH". Bare + www. variants of the brand's primary domain are prepended server-side so they always appear. Popover groups suggestions by type, surfaces a confidence pill + one-sentence reason per row, pre-selects high-confidence picks, and includes a "Grounded in" panel listing the source URLs for verification. TAVILY_API_KEY is optional — without it the suggester falls back to LLM-only knowledge. Costs ~\$0.01/call (\$0.005 Tavily + \$0.005 gpt-4o) and is capped at 20/min per user per org.
  • "Your brand" inline form: Orgs created via the org dropdown (not the onboarding wizard) previously hit a dead-end on /brands — the "Your brand" section showed "finish onboarding to add one" with no path forward. The form is now rendered inline when the section is empty, with the same name + expandable details (website + AI-suggested aliases) used for adding competitors.
  • Default project on every new organization: Better Auth's organizationHooks.afterCreateOrganization now provisions a default project row, and the [organizationSlug] route layout lazily backfills one for orphan orgs created before this hook existed. /topics no longer dead-ends for org-dropdown signups. The "project" data-model abstraction stays invisible in the UI — one project per org, never surfaced in nav or settings. The multi-project selector in TopicsView remains for the eventual agency-style use case.
  • Notion-style URL handles: Workspace URLs are now /<slug>-<cuid2>/dashboard (e.g. /get-vocal-kf3p9b4z2g7q1d5x8w7v3yhq/dashboard) instead of the prior FCFS globally-unique slug pattern. The trailing CUID is canonical; the slug is decoration generated from the org name on every render. Two orgs named "Acme" coexist as /acme-<id1>/... and /acme-<id2>/... with zero collision possible — no FCFS squatting, no trademark exposure. URLs are bookmarkable and survive renames (changing the org name regenerates the slug part but the trailing ID never moves, so old links keep resolving). Legacy bare-slug URLs (/get-vocal/dashboard) still resolve via a slug-fallback and 301-upgrade to the canonical handle on first hit. Active workspace is now URL-driven; Better Auth's session-based activeOrganizationId is used only as the redirect default when landing on /.
  • Loading state everywhere: Every leaf route gets a loading.tsx skeleton that mirrors the page's real layout for zero reflow when content swaps in (dashboard, brands, queries, topics, scans, insights, all detail pages, all settings routes, plus org-scoped + account-scoped settings). The two LLM-driven routes (/insights and /topics/[id]/insights) get an additional banner-with-phases treatment: a spinner with a rotating phase string ("Reading scan history" → "Aggregating brand mentions" → "Comparing against tracked competitors" → "Detecting trend shifts" → "Drafting executive summary" → "Final pass"), a live m:ss elapsed counter, and a subtitle explaining the ~30s cold-start cost. Phases are honest descriptions of pipeline stages, not invented progress.
  • Error boundaries on every layer that can fail: error.tsx files at the org route, the insights route, the topic-insights route, and a last-resort catch in (main). All driven by a shared RouteError component with retry, dev-mode error dump, and a digest reference for support. LLM-driven routes have branded copy pointing at Settings → API keys.
  • Custom 404: not-found.tsx at the [organizationSlug] layer renders a friendly recovery panel inside the app shell — notFound() calls no longer fall through to Next's chromeless default.
  • Single-scrollbar shell: Replaced the framed-scroll pattern (which produced two scrollbars on tall pages — body's min-h-screen + main's overflow-y-auto) with natural body scroll + fixed sidebar. Matches every normal-feeling SaaS shell.

Security and hardening

  • Security headers + `poweredByHeader: false` on both apps/saas/next.config.ts and apps/marketing/next.config.ts: HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, plus COOP/CORP on the SaaS app.
  • Better Auth `freshAge` restored from 0 to 60×15 (15 min). Without this, any old session could perform sensitive ops (deleteUser, changePassword, passkey.add, twoFactor.enable). Sensitive routes should catch SESSION_NOT_FRESH and prompt the user to re-auth.
  • Better Auth `rateLimit` config wired: 10/min baseline; tighter caps on /sign-in/email, /sign-up/email (5/min), /magic-link/send, /forget-password (3/min), /two-factor/send-otp (5/min). In-memory by default; swap to a Redis storage adapter when scaling multi-region.
  • App-layer rate limiting on three abuse surfaces: ai.stream (30/min/user), tracking.suggestAliases (20/min/(user, org)), organizations.generateSlug (600/min global). Token-bucket helper at @repo/api/lib/rate-limit.ts with 4 unit tests.
  • `/ai/stream` now requires `organizationId` + membership verification: previously any authenticated user could spam the endpoint against the platform's shared model key — direct route to draining the OpenAI/OpenRouter budget. AiChat updated to pass the active org.
  • Cron auth = `crypto.timingSafeEqual`: all 4 /api/cron/* endpoints (nightly-topic-insights-submit, process-completed-batches, scheduled-scans, weekly-digest-submit) switched from string-equality Bearer token compare to timing-safe via a shared helper at apps/saas/app/api/cron/_lib/auth.ts.
  • Defense-in-depth auth proxy at apps/saas/proxy.ts (Next.js 16 renamed middleware → proxy). Edge-safe cookie-presence check on non-public paths; redirects to /login with redirectTo preserved. Layout-level check stays the authoritative auth path; the proxy is the belt-and-suspenders layer that stops a future misplaced route group from bypassing auth.
  • Image-proxy path-traversal hardening: apps/saas/app/image-proxy/[...path]/route.ts validates filenames against ^[a-zA-Z0-9._-]+\.(png|jpe?g|webp|gif|avif)$ and an explicit bucket allowlist with a type-guard. Rejects ../, nul bytes, non-image extensions.
  • S3 signed upload accepts caller-supplied `contentType` validated against a per-bucket allowlist (ALLOWED_UPLOAD_CONTENT_TYPES). Was hard-coded to image/jpeg, letting clients upload HTML/SVG/PDF under a jpeg label — stored-XSS vector on public-read buckets.
  • OpenAPI Scalar docs gated at packages/api/orpc/handler.ts: only mounted in non-prod or when ENABLE_API_DOCS=true. The full API surface was previously discoverable at /api/docs on a public URL in prod.
  • Toast on org-invitation accept/decline errors: OrganizationInvitationModal was silently swallowing the error with a TODO comment — now surfaces via toastError with a localized fallback message.

Performance

  • `disableCookieCache: true` removed from default `getSession()`. Used to force a DB hit on every authenticated render (5–10× DB load multiplier on logged-in pages). Cookie cache is integrity-signed by Better Auth (5–10 min TTL) — safe for the default path. New getFreshSession() opt-in variant for the rare flows that need live DB state (admin role transitions, billing).
  • N+1 `db.insert(...)` calls batched in execute-scan.ts: brandMention fallback rows and source rows are now inserted via single batched values([...]) calls instead of one-row-per-iteration loops. Hot scans with many brands matched go from N round-trips to 1.
  • `fetch` to OpenRouter wrapped in `AbortController` + 8s timeout in true-up-cost.ts. Could previously hang indefinitely if their API stalled, blocking scan completion.
  • 29 redundant `revalidate = 0` declarations removed across saas + marketing routes. Implied by force-dynamic or cookies() / headers() reads.

Database

  • Migration tree reconciliation: 0011_foamy_pyro.sql + meta/0011_snapshot.json + _journal.json entry generated to record the topic_insight_snapshot → analytics_snapshot consolidation (the table was applied to dev earlier via the manual apply-migration-0011.mjs script per plan 022, but drizzle's codegen tree was never updated). The generated SQL was hand-edited to add IF NOT EXISTS / IF EXISTS guards so it's safe to apply against either fresh DBs or DBs that already have the table.
  • `backfill-drizzle-migrations.mjs` script: reads the journal, computes sha256 of each migration's SQL, and inserts one row per entry into __drizzle_migrations with the correct created_at. Solves the case where the DB has been populated via direct schema push or manual apply scripts before drizzle's tracking table existed and drizzle-kit migrate hangs trying to apply migrations from scratch against a populated schema.

Hygiene

  • `@repo/auth`, `@repo/ai`, `@repo/database`, `@repo/payments` `package.json` `types` fields corrected from the malformed ./**/.tsx (or ./**/.ts on payments) glob to ./index.ts. Inert in practice (consumers go through main) but worth fixing — the typo originated upstream in the Supastarter template and is propagated to every fork.
  • Removed unused `@scalar/hono-api-reference` dependency from @repo/api.
  • Renamed `apps/saas/modules/admin/component/` → `components/` to match the rest of the repo's directory convention.
  • Deleted dead `apps/saas/modules/organizations/components/OrganizationStart.tsx` (Supastarter template leftover with no imports — the org root redirects to /dashboard now).
  • Deleted dead `apps/saas/modules/lib/sidebar-context.tsx` (identical duplicate of the @shared/lib/ version, no imports).
  • `Pagination.tsx`: fixed PaginatioProps typo → PaginationProps, converted type → interface, arrow → function, removed redundant displayName.
  • `oauth-providers.tsx`: IconProps type → interface.
  • Marketing ContactForm + NewsletterSection `console.log` calls in the unimplemented submit-stubs gated behind NODE_ENV !== "production" so demo payloads don't leak in prod logs.
  • `metadataBase` wired in both root layouts from NEXT_PUBLIC_SAAS_URL / NEXT_PUBLIC_MARKETING_URL (fixes the OG/Twitter image resolution build warning).
  • SaaS `robots.ts` switched to `Disallow: /` — it's an authenticated product with no public content to index.
  • `LoginForm` password-toggle button got an aria-label (auth.login.showPassword / hidePassword i18n strings).
  • `as any` casts narrowed: LoginForm.tsx:92 via an in check on data.twoFactorRedirect; ActiveOrganizationProvider.tsx:69 typed against Session.
  • GitHub Actions workflow concurrency cancellation added to validate-prs.yml so rapid PR updates don't stack runs.

Deprecated routes removed

  • `/api/cron/weekly-digest` route deleted. Was superseded by the batch lane (weekly-digest-submit + process-completed-batches) shipped in plan 022; marker said "stays around for one release cycle" and that cycle ended.

May 13, 2026

Fixes and improvements

  • Dependency minimum release age: A 1-day minimum release age is now enforced at two levels to reduce supply chain attack exposure. Dependabot is configured with cooldown: default-days: 1 so upgrade PRs are not opened immediately for freshly published versions. pnpm-workspace.yaml sets settings.minimumReleaseAge: 1440 (minutes) so pnpm v11+ will also refuse to install any package version younger than one day, including transitive dependencies. Together these ensure a community-detection window before newly published — potentially compromised — versions reach the project.

May 9, 2026

Fixes and improvements

  • Two-factor authentication schema: Added the missing Better Auth verified flag to the TwoFactor Prisma model, generated Prisma Zod schema, and PostgreSQL, MySQL, and SQLite Drizzle schemas so two-factor enrollment state is represented consistently across database adapters.

May 6, 2026

Fixes and improvements

  • Account security settings: Passkeys can now be renamed from the passkey list, and the rename dialog opens automatically after creating a new passkey. The passkey list shows user-defined names without the device type prefix and falls back to “Unnamed passkey” for legacy passkeys without a saved name. The two-factor authentication block remains visible when a password has not been configured and now explains that a password is required before two-factor authentication can be enabled.

2026-04-24 v3.3.2

2026-04-24 v3.3.2

Fixes and improvements

  • Organization general settings: Organization name field now syncs when client data loads; success and error toasts use dedicated organizations.settings i18n keys. After renaming, the organization list query is refetched, the active organization is refreshed, and the name form resets to the saved value. The organization switcher no longer briefly shows “Personal account” when opening account settings with an active organization (active-org query keeps previous data across route key changes).

2026-04-20 v3.3.1

2026-04-20 v3.3.1

Fixes and improvements

  • Drizzle notifications and schema: Notification persistence (preferences, insert support, listing rows, unread counts, mark read) is implemented in @repo/database for both Prisma and Drizzle, so the Drizzle scaffold no longer mixes in Prisma-style db calls. The Drizzle schema barrel (drizzle/schema/index.ts) re-exports the PostgreSQL schema (aligned with the Drizzle client) and exposes NotificationType / NotificationTarget for type-safe consumers.
  • `user.lastActiveOrganizationId` in Drizzle: Added on PostgreSQL, MySQL, and SQLite user tables so Drizzle schemas match the Prisma user model and auth hooks that read this field.
  • Organization lookups (Drizzle): findFirst-based helpers now normalize missing rows to null, matching Prisma findUnique behavior for tests and callers.
  • `@repo/notifications`: Dropped the thin list, mark-read, and preferences modules; the package index re-exports the shared notification query helpers from @repo/database next to create/welcome/resolve-link.
  • Notifications procedures: List and unread-count handlers use the database row helpers from @repo/notifications / @repo/database and apply resolveNotificationLink when shaping list responses.
  • Notification center: Removed interval-based refetching of notifications from the notification center UI.

2026-03-30 v3.3.0

2026-03-30 v3.3.0

Added

  • Notification entity: New Notification model in Prisma and Drizzle (PostgreSQL, MySQL, SQLite) with user association and read/unread state.
  • `@repo/notifications`: Shared module for notification definitions (catalog), creating and listing notifications, marking as read, per-user preferences, and a welcome notification helper.
  • Notifications oRPC: Procedures to list notifications, get unread count, mark one or all as read, and read/update notification preferences.
  • Notification Center: Navbar UI to view notifications and mark them read.
  • Notification preferences: Account settings page and form for per-channel preferences; server-only notification logic is kept out of the client bundle for the preferences form.
  • Auth: Database hook after user creation creates a welcome in-app notification via @repo/notifications.
  • `Notification` email template and template wiring; saas and mail translation keys for notifications in English, German, Spanish, and French.
  • Popover and Switch components exported from @repo/ui for notification UI patterns.

Changed

  • Account and organization settings: Removed nested settings/layout.tsx for account and org routes; settings sub-pages (general, billing, security, members, etc.) are updated to match the flatter structure. New Notifications route under account settings.
  • NavBar: Reworked layout and behavior (including notification entry points); Tailwind theme (tooling/tailwind/theme.css) and related component tweaks for consistency.

2026-03-24 v3.2.0

2026-03-24 v3.2.0

Testing

  • Vitest setup: Added Vitest configuration (vitest.config.ts) to apps/saas, apps/marketing, and packages/api so unit tests can be run with pnpm test in each workspace package.
  • Unit tests: Added initial unit test suites covering base-url helpers in both apps, content utilities in the marketing app, and organization membership logic, slug generation, and oRPC procedure wiring in the API package.
  • CI integration: Added a unit test job to the GitHub Actions workflow so all unit tests run on every pull request; the Turbo test task no longer depends on build.

2026-03-24 v3.1.1

2026-03-24 v3.1.1

Fixes and improvements

  • Checkout return after payment: After Stripe checkout, users are redirected to /checkout-return, which polls listPurchases until an active plan appears (avoiding a race with webhook processing). The pricing table passes organizationId in the return URL when applicable. If confirmation does not arrive within the timeout, users are sent to /choose-plan. Added checkoutReturn copy in English, German, Spanish, and French.

2026-03-23 v3.1.0

2026-03-23 v3.1.0

Tooling

  • Lint and format stack: Replaced Biome with Oxlint and Oxfmt for faster linting and formatting across the monorepo.
  • Workspace layout: Consolidated Oxlint/Oxfmt dependencies at the repository root (pnpm catalog) and removed redundant per-package Biome configs; lockfile and many source files were updated to match the new rules and formatter output.

2026-03-18 v3.0.3

2026-03-18 v3.0.3

Added

  • Persist last active organization: A new lastActiveOrganizationId field is stored on the user record whenever the active organization changes. On next sign-in, the session is automatically restored to that organization via a better-auth databaseHook, so users no longer land on a default/empty organization after logging back in.

Page 4 of 8