Skip to content

feat(api-service, dashboard): Better auth integration DRAFT#9711

Merged
scopsy merged 58 commits intonextfrom
better-auth-integration
Jan 25, 2026
Merged

feat(api-service, dashboard): Better auth integration DRAFT#9711
scopsy merged 58 commits intonextfrom
better-auth-integration

Conversation

@scopsy
Copy link
Contributor

@scopsy scopsy commented Dec 23, 2025

What changed? Why was the change needed?

EE: https://github.com/novuhq/packages-enterprise/pull/381

Screenshots

Expand for optional sections

Related enterprise PR

Special notes for your reviewer

Adds full support for Better Auth as an alternative to Clerk for enterprise authentication. Includes backend provider abstraction, JWT strategy, and organization support, as well as frontend client, provider, and UI components for sign-in, sign-up, and organization management. Updates environment variable handling, CORS, and body parser logic to support Better Auth endpoints. Documentation for integration and implementation is included.
Removed the token exchange step and storage of 'better-auth-token' in the dashboard sign-in and sign-up flows, now relying solely on 'better-auth-session-token'. Updated token retrieval and sign-out logic accordingly. Switched API auth decorator to use isEEAuthEnabled. Added 'passport-custom' dependency to enterprise auth package.
Introduces OrganizationDropdown and UserButton components for improved organization switching and user actions in the dashboard. Updates exports and internal hooks to support infinite organization list loading and switching. Also updates Vite config to alias the new organization dropdown component.
Introduces a new OrganizationCreate component in better-auth, refactors related imports, and updates the OrganizationList to use this new component. Also adds reload methods to user and organization hooks, and updates Vite config to alias the new component path.
Refactored organization-create component to use window.location.href for navigation after organization selection or creation, removing the dependency on useNavigate from react-router-dom. Also updated useClerk's setActive to reload the page after setting the active organization.
Introduces an invitation acceptance flow, including a new route, page, and UI component for handling organization invitations. Updates the sign-up process to support joining via invitation and improves the team members management UI with member listing, invitation management, and removal actions. Refactors exports and routing to support these new features.
Introduces a refreshSession method to the auth context and uses it in the invitation acceptance flow to handle cases where a session token exists but the user is not signed in. Adds detailed agent logging throughout the invitation-accept component for debugging and tracing. Updates navigation in sign-up and invitation-accept to use the router's navigate function instead of window.location.href.
Eliminated all agent log fetch calls from invitation-accept.tsx, replacing them with console logs for debugging. Improved invitation handling in sign-up.tsx by extracting the invitation ID from the redirect URL and ensuring proper session refresh after sign-up. Also removed unnecessary input IDs and minor UI cleanups.
Switched to using 'better-auth/react' for createAuthClient and refactored the authentication context to use the new session hook. Improved organization fetching logic and removed redundant session state management for better clarity and reliability.
Added custom OrganizationSettings and UserProfile components for Better Auth, replacing Clerk-based components where appropriate. Updated organization and team member management logic to support both Clerk and Better Auth providers. Refactored sign-up flow to defer organization creation, and improved organization selection and navigation.
Added a role-permissions mapping for RBAC in `role-permissions.ts` and integrated it into the authentication context. Updated team member and invitation UI to use role enums and display correct labels and styles. Enhanced the Protect component to support permission and role-based access checks. Refactored related logic for consistency and maintainability.
@netlify
Copy link

netlify bot commented Dec 23, 2025

Deploy Preview for dashboard-v2-novu-staging canceled.

Name Link
🔨 Latest commit cab81dd
🔍 Latest deploy log https://app.netlify.com/projects/dashboard-v2-novu-staging/deploys/697612bdea585300080ef5e3

@github-actions
Copy link
Contributor

github-actions bot commented Dec 23, 2025

Hey there and thank you for opening this pull request! 👋

We require pull request titles to follow specific formatting rules and it looks like your proposed title needs to be adjusted.

Your PR title is: feat(api-service, dashboard): Better auth integration DRAFT

Requirements:

  1. Follow the Conventional Commits specification
  2. As a team member, include Linear ticket ID at the end: fixes TICKET-ID or include it in your branch name

Expected format: feat(scope): Add fancy new feature fixes NOV-123

Details:

PR title must end with 'fixes TICKET-ID' (e.g., 'fixes NOV-123') or include ticket ID in branch name

Deleted BETTER_AUTH_FULL_IMPLEMENTATION.md and BETTER_AUTH_INTEGRATION.md documentation files. Updated organization usecases to support Better Auth, removed debug logging from JWT strategy and bootstrap, and adjusted CORS config to always allow wildcard origins when enabled.
@pkg-pr-new
Copy link

pkg-pr-new bot commented Dec 23, 2025

Open in StackBlitz

npm i https://pkg.pr.new/novuhq/novu@9711
npm i https://pkg.pr.new/novuhq/novu/@novu/providers@9711
npm i https://pkg.pr.new/novuhq/novu/@novu/shared@9711

commit: cab81dd

scopsy added 25 commits January 12, 2026 09:30
Update better-auth and sso dependencies to v1.4.10

Upgrades @better-auth/sso and better-auth packages to version 1.4.10 in dashboard and enterprise auth packages. Updates pnpm-lock.yaml to reflect new versions and peer dependencies.

Update .source

Fix organization image handling and fetchNext logic

Corrects the assignment of organization image URLs by using the 'logo' property and ensures 'fetchNext' is always defined as a function in useOrganizationList. Also updates OrganizationDropdown to handle possible missing properties and improves type safety.
This reverts commit 5a70c70.
Refactored conditional logic for enterprise and self-hosted environments in authentication and feature flag providers. Adjusted LaunchDarkly enablement checks and subscription query conditions. Minor import reordering and cleanup in organization dropdown and better-auth components. Added new VSCode task for EE API.
Improves type safety in user profile label extraction and refines scroll handling logic in the organization dropdown. Ensures proper checks for userMemberships and fetchNext, and updates OrganizationAvatar usage to prevent potential imageUrl issues.
Adds permission-based checks using ORG_SETTINGS_WRITE to restrict editing organization settings and managing team members. Updates navigation routes after authentication actions to use ROUTES.INBOX_USECASE instead of ROUTES.ROOT for improved user flow.
Added a resolution and override for 'zod' at version ^3.23.8 in package.json and pnpm settings to ensure consistent dependency versions across the project. Updated lockfile and submodule to reflect these changes.
Removed unnecessary console.log statements from multiple files to clean up debug output. Added a type-level assertion in role-permissions.ts to ensure all PermissionsEnum values are used in ROLE_PERMISSIONS.
Centralized ROLE_PERMISSIONS definition in the shared package and updated imports in the dashboard app. Also improved comments in CORS config for BetterAuth routes.
Bump zod dependency from 3.23.8 to 3.25.0 in dashboard and root package.json, update resolutions and pnpm overrides accordingly. This ensures compatibility with the latest features and bug fixes from zod.
Upgraded zod from v3 to v4 and @hookform/resolvers to v5 in the dashboard app. Refactored schema usage to use new zod v4 methods (e.g., z.email(), z.url(), z.uuid(), z.looseObject()) and updated validation logic accordingly. This ensures compatibility with the latest zod API and improves type safety and maintainability.
Changed better-auth dependency from ~1.3.0 to ^1.3.0 in dashboard and enterprise auth packages. Updated pnpm-lock.yaml to reflect new better-auth version and its peer dependencies.
Replaces deprecated or overly generic Zod types (e.g., ZodTypeAny) with more specific ZodType usage across schema and validation utilities. Updates error handling in validation.ts to use new Zod issue codes and input references, improving compatibility with recent Zod versions.
Replaces zodResolver with standardSchemaResolver across all form components for consistency and improved validation. Updates JSON parsing and validation logic in schemas and form submit handlers to ensure correct handling of custom data fields. Also bumps react-hook-form to version 7.71.1 in package.json and pnpm-lock.yaml.
Refactor test workflow components to handle payloads as either strings or objects, improving type safety and preventing runtime errors. Update function signatures, type assertions, and payload parsing logic across related components.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/dashboard/src/components/subscribers/schema.ts`:
- Line 35: The subscriberId schema currently applies .min(1) before .trim(), so
whitespace-only strings can pass length validation; update the chain on
subscriberId (z.string()) to call .trim() before .min(1, 'SubscriberId is
required') so input is trimmed prior to length validation and whitespace-only
values are rejected.
🧹 Nitpick comments (11)
.claude/skills/better-auth-best-practices/SKILL.md (5)

14-18: Add blank lines around the “Environment Variables” heading.
This violates MD022 (blank lines around headings).

✅ Proposed fix
 ## Quick Reference
-
 ### Environment Variables
+
 - `BETTER_AUTH_SECRET` - Encryption secret (min 32 chars). Generate: `openssl rand -base64 32`

20-24: Add blank lines around the “File Location” heading.
This violates MD022.

✅ Proposed fix
 Only define `baseURL`/`secret` in config if env vars are NOT set.
-
 ### File Location
+
 CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--config` for custom path.

24-27: Add blank lines around the “CLI Commands” heading.
This violates MD022.

✅ Proposed fix
 CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--config` for custom path.
-
 ### CLI Commands
+
 - `npx `@better-auth/cli`@latest migrate` - Apply schema (built-in adapter)

121-125: Specify a language on the fenced code block.
MD040 flags missing language.

✅ Proposed fix
-```
+```ts
 import { twoFactor } from "better-auth/plugins/two-factor"
</details>

---

`162-166`: **Ensure the file ends with a single trailing newline.**  
MD047 expects exactly one newline at EOF.

</blockquote></details>
<details>
<summary>apps/dashboard/src/components/subscribers/subscriber-overview-form.tsx (1)</summary><blockquote>

`158-168`: **Consider wrapping `JSON.parse` in try-catch for defensive error handling.**

While the `SubscriberFormSchema` validates that `data` is valid JSON before submission, `JSON.parse` on line 162 could still throw if an edge case bypasses validation (e.g., form state manipulation). Consider wrapping in try-catch for robustness:

<details>
<summary>Suggested defensive handling</summary>

```diff
       if (typedKey === 'data') {
-        const data = formData.data ? JSON.parse(formData.data) : {};
-
-        return { ...acc, data: data && Object.keys(data).length > 0 ? data : {} };
+        let data = {};
+        if (formData.data) {
+          try {
+            data = JSON.parse(formData.data);
+          } catch {
+            data = {};
+          }
+        }
+
+        return { ...acc, data: Object.keys(data).length > 0 ? data : {} };
       }
apps/dashboard/src/components/contexts/schema.ts (1)

16-30: Consider extracting the JSON validation logic to reduce duplication.

The JSON validation refine logic is duplicated between CreateContextFormSchema and EditContextFormSchema. This same pattern also appears in subscribers/schema.ts. Consider extracting to a reusable helper:

Suggested refactor
// In a shared validation utils file
const isValidJsonString = (str: string | undefined | null): boolean => {
  if (!str) return true;
  try {
    JSON.parse(str);
    return true;
  } catch {
    return false;
  }
};

// Usage
const jsonDataField = z
  .string()
  .refine(isValidJsonString, { message: 'Custom data must be a valid JSON' })
  .optional();
apps/dashboard/src/components/contexts/create-context-form.tsx (1)

93-99: Consider wrapping JSON.parse in try-catch for defensive error handling.

While the schema validates JSON before submission, adding a try-catch around JSON.parse on line 93 would provide additional safety against edge cases:

Suggested defensive handling
-    const parsedData = formData.data ? JSON.parse(formData.data) : {};
+    let parsedData = {};
+    if (formData.data) {
+      try {
+        parsedData = JSON.parse(formData.data);
+      } catch {
+        // Schema validation should prevent this, but handle gracefully
+        parsedData = {};
+      }
+    }
apps/dashboard/src/components/contexts/context-overview.tsx (1)

126-132: Consider wrapping JSON.parse in try-catch for consistency.

Same pattern as the other context forms - while schema validation should catch invalid JSON, defensive error handling would be beneficial:

Suggested defensive handling
-      const parsedData = formData.data ? JSON.parse(formData.data) : {};
+      let parsedData = {};
+      if (formData.data) {
+        try {
+          parsedData = JSON.parse(formData.data);
+        } catch {
+          parsedData = {};
+        }
+      }
.cursor/skills/better-auth-best-practices/SKILL.md (2)

57-57: Clarify model name casing convention.

The example states "If Prisma model is User" but then recommends modelName: "user" (lowercase). This could confuse developers about whether Better Auth expects lowercase model references regardless of ORM casing, or if the modelName should match the Prisma model case exactly.

Consider adding a brief note clarifying the casing convention, for example:

  • "Better Auth expects lowercase model names in configuration, even when the ORM model is PascalCase"
  • Or provide the exact rule Better Auth follows for model name resolution

1-10: Consider adding Better Auth version context.

Since Better Auth APIs can evolve across versions and the PR mentions v1.4.10, consider adding a version note to help future developers understand which version this documentation applies to. This could be added near the top or in the metadata section.

Example:

**Better Auth Version:** v1.4.10+


return str;
}),
subscriberId: z.string().min(1, 'SubscriberId is required').trim(),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reorder .trim() before .min(1) to validate trimmed input.

The current order .min(1).trim() validates the length before trimming, which allows whitespace-only inputs like " " to pass the min(1) check, then get trimmed to an empty string—bypassing the required validation.

🐛 Proposed fix
-  subscriberId: z.string().min(1, 'SubscriberId is required').trim(),
+  subscriberId: z.string().trim().min(1, 'SubscriberId is required'),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
subscriberId: z.string().min(1, 'SubscriberId is required').trim(),
subscriberId: z.string().trim().min(1, 'SubscriberId is required'),
🤖 Prompt for AI Agents
In `@apps/dashboard/src/components/subscribers/schema.ts` at line 35, The
subscriberId schema currently applies .min(1) before .trim(), so whitespace-only
strings can pass length validation; update the chain on subscriberId
(z.string()) to call .trim() before .min(1, 'SubscriberId is required') so input
is trimmed prior to length validation and whitespace-only values are rejected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant