Skip to content

feat: integrations#5

Merged
kzndotsh merged 18 commits intomainfrom
feat/integrations
Jan 16, 2026
Merged

feat: integrations#5
kzndotsh merged 18 commits intomainfrom
feat/integrations

Conversation

@kzndotsh
Copy link
Contributor

@kzndotsh kzndotsh commented Jan 16, 2026

Summary by Sourcery

Introduce a generic integrations framework with registry, API routes, database schema, and UI, and migrate the existing XMPP account management onto this integrations system.

New Features:

  • Add a generic integrations core (types, base class, registry, factory, user-deletion cleanup) to manage pluggable external services and their accounts.
  • Expose authenticated REST API endpoints for listing integrations and managing per-integration accounts, including per-account CRUD operations.
  • Add client-side integrations API helpers, React Query hooks, and dynamic route generation for integration-aware navigation and data fetching.
  • Introduce a reusable IntegrationManagement UI component and an Integrations dashboard page for users to view and manage integration accounts.

Enhancements:

  • Refactor XMPP account management into an XMPP integration implementation that plugs into the new integrations framework, including lazy configuration validation and integration-specific types.
  • Update routing configuration to use a generic /app/integrations entry and support dynamic integration routes in the protected app navigation.
  • Extend query key configuration and API barrel exports to include integrations, and wire XMPP client code and env keys through the new integrations module structure.

Build:

  • Configure Next.js to treat Sentry native Node packages as server external packages to avoid bundling issues with native modules.

Deployment:

  • Add a new integration_accounts database table and enum via a migration to store generic integration account metadata for users.

Chores:

  • Ensure user deletion flows (self-service and admin) trigger cleanup of all associated integration accounts before removing the user record.

- Introduced new API routes for managing integration accounts, including GET, POST, PATCH, and DELETE methods.
- Implemented authentication and authorization checks to ensure secure access to account information.
- Enhanced error handling for various scenarios, such as account existence and integration status.
- Updated existing routes to utilize the new integrations module for improved organization and maintainability.
- Introduced IntegrationCard component for displaying integration details with title and optional description.
- Added IntegrationManagement component for managing integration accounts, including creation and deletion functionalities.
- Implemented loading and error states for improved user experience in account management.
- Enhanced UI with alert dialogs and toast notifications for user feedback during account operations.
- Introduced new hooks for managing integration accounts, including fetching, creating, updating, and deleting accounts.
- Implemented query and mutation functions using React Query for efficient data handling and state management.
- Enhanced integration with existing API routes for seamless account operations.
- Introduced a new integrations module for managing integration accounts, including functions for fetching, creating, updating, and deleting accounts.
- Updated the API index to export the new integrations module.
- Enhanced query keys to support integration-related queries, improving data management and retrieval for integration accounts.
- Refactored XMPP account management functions to utilize the new integrations module for streamlined account operations.
- Created a new SQL migration for the integration_accounts table, including fields for id, user_id, integration_type, status, created_at, updated_at, and metadata.
- Defined an ENUM type for integration account statuses (active, suspended, deleted).
- Added integration account schema definition in the Drizzle ORM, including necessary indexes and foreign key constraints for user_id.
- Added base integration classes and types for managing integration accounts, including methods for creating, fetching, updating, and deleting accounts.
- Introduced an integration registry for registering and retrieving integrations, along with utility functions for integration management.
- Defined integration status constants and labels for better status handling.
- Implemented user account cleanup functionality to ensure proper deletion of integration accounts during user deletion processes.
- Added a new client module for interacting with the Prosody REST API, including functions for creating, deleting, and checking XMPP accounts.
- Introduced configuration management for Prosody integration, validating environment variables and ensuring secure access.
- Developed an implementation class for XMPP integration, providing methods for account creation, retrieval, updating, and deletion.
- Created utility functions for username validation and JID formatting, enhancing account management capabilities.
- Established TypeScript types for XMPP account management and Prosody API responses, improving type safety and clarity.
- Implemented a beforeDelete hook in the user deletion configuration to ensure external integration accounts are cleaned up prior to user deletion.
- Integrated the cleanupIntegrationAccounts function to handle the removal of associated integration accounts, enhancing user account management.
- Deleted the XMPP client module, configuration management, TypeScript types, and utility functions for Prosody integration.
- This cleanup removes unused code and simplifies the codebase, preparing for a potential redesign or alternative integration approach.
- Introduced a new configuration option for server external packages in next.config.ts to specify packages that should not be bundled by Next.js.
- Added entries for "@sentry/node-native" and "@sentry-internal/node-native-stacktrace" to support native modules with dynamic requires, enhancing runtime package management.
- Replaced the import of XMPP keys from the deprecated path to the new integration path, ensuring proper access to the required keys for environment configuration.
- This change maintains the integrity of the environment setup while aligning with recent refactoring efforts.
- Updated route configuration to replace the XMPP path with a new integrations path, improving clarity and organization.
- Introduced functions to build and merge integration routes dynamically, allowing for better navigation management of integration accounts.
- Added TypeScript interfaces for integration route options, enhancing type safety and flexibility in route handling.
- Introduced a new function to register all integrations in an idempotent manner, ensuring that integrations are only registered once.
- Implemented the registration of the XMPP integration, enhancing the integration management framework.
- Introduced the IntegrationsContent component to display and manage various integrations, including XMPP account management.
- Created the IntegrationsPage component to serve as the main entry point for managing integrations, incorporating session verification and metadata generation.
- Enhanced user experience with loading states and informative messages for integration availability.
- Replaced the XMPP section in routes.json with a new integrations section for improved clarity.
- Updated metadata to reflect the broader scope of service integrations, including XMPP and IRC management.
@sourcery-ai
Copy link

sourcery-ai bot commented Jan 16, 2026

Reviewer's Guide

Introduce a generic integrations framework (registry, core types, DB schema, user cleanup) and migrate the existing XMPP account functionality onto it, exposing new API routes, React Query hooks, and UI for managing integrations under /app/integrations while keeping XMPP as a first concrete integration.

Sequence diagram for creating an XMPP account via integrations page

sequenceDiagram
  actor U as User
  participant UI as IntegrationsPage
  participant Hook as useCreateIntegrationAccount(xmpp)
  participant API as POST api_integrations_xmpp_accounts
  participant Reg as registerIntegrations
  participant Registry as IntegrationRegistry
  participant XInt as XmppIntegration
  participant DB as PostgreSQL
  participant Prosody as ProsodyServer

  U->>UI: Click Create XMPP Account
  UI->>Hook: call mutateAsync(input)
  Hook->>API: HTTP POST /api/integrations/xmpp/accounts
  API->>Reg: registerIntegrations()
  Reg->>Registry: register XmppIntegration
  API->>Registry: get("xmpp")
  Registry-->>API: XmppIntegration instance
  API->>XInt: createAccount(userId, input)
  XInt->>DB: select xmppAccount where userId
  DB-->>XInt: no existing account
  XInt->>DB: select user email by userId
  DB-->>XInt: user email
  XInt->>XInt: determineUsername(input.username, email)
  XInt->>DB: check username uniqueness in xmpp_account
  DB-->>XInt: username available
  XInt->>Prosody: createProsodyAccount(username)
  Prosody-->>XInt: success
  XInt->>DB: insert xmppAccount row
  DB-->>XInt: new xmppAccount
  XInt-->>API: XmppAccount
  API-->>Hook: JSON { ok: true, account }
  Hook-->>UI: resolved XmppAccount
  UI-->>U: Show success toast and account details
Loading

Entity relationship diagram for integration_accounts and XMPP accounts

erDiagram
  user {
    text id PK
    text email
  }

  integration_accounts {
    text id PK
    text user_id FK
    text integration_type
    enum status
    timestamp created_at
    timestamp updated_at
    jsonb metadata
  }

  xmpp_account {
    text id PK
    text user_id FK
    text jid
    text username
    enum status
    jsonb metadata
    timestamp created_at
    timestamp updated_at
  }

  user ||--o{ integration_accounts : has
  user ||--o{ xmpp_account : has

  integration_accounts ||..|| xmpp_account : optional_overlap_by_integration
Loading

Class diagram for integrations core and XMPP integration

classDiagram
  %% Core types
  class IntegrationAccount {
    +string id
    +string userId
    +string integrationId
    +string status
    +Date createdAt
    +Date updatedAt
    +Record~string,unknown~ metadata
  }

  class IntegrationPublicInfo {
    +string id
    +string name
    +string description
    +boolean enabled
  }

  class Integration {
    <<interface>>
    +string id
    +string name
    +string description
    +boolean enabled
    +createAccount(userId string, input IntegrationCreateInput) Promise~IntegrationAccount~
    +getAccount(userId string) Promise~IntegrationAccount or null~
    +getAccountById(accountId string) Promise~IntegrationAccount or null~
    +updateAccount(accountId string, input IntegrationUpdateInput) Promise~IntegrationAccount~
    +deleteAccount(accountId string) Promise~void~
    +validateIdentifier(identifier string) boolean
    +generateIdentifier(email string) string
  }

  class IntegrationBaseOptions {
    +string id
    +string name
    +string description
    +boolean enabled
  }

  class IntegrationBase {
    <<abstract>>
    +string id
    +string name
    +string description
    +boolean enabled
    +IntegrationBase(options IntegrationBaseOptions)
    +createAccount(userId string, input IntegrationCreateInput) Promise~IntegrationAccount~
    +getAccount(userId string) Promise~IntegrationAccount or null~
    +updateAccount(accountId string, input IntegrationUpdateInput) Promise~IntegrationAccount~
    +deleteAccount(accountId string) Promise~void~
  }

  IntegrationBase ..|> Integration

  class IntegrationRegistry {
    -Map~string,Integration~ integrations
    +register(integration Integration) void
    +get(id string) Integration
    +getAll() Integration[]
    +getEnabled() Integration[]
    +isEnabled(id string) boolean
    +getPublicInfo() IntegrationPublicInfo[]
  }

  class XmppAccount {
    +string id
    +string userId
    +"xmpp" integrationId
    +string jid
    +string username
    +string status
    +Date createdAt
    +Date updatedAt
    +Record~string,unknown~ metadata
  }

  class CreateXmppAccountRequest {
    +string username
  }

  class UpdateXmppAccountRequest {
    +string username
    +string status
    +Record~string,unknown~ metadata
  }

  class XmppIntegration {
    +XmppIntegration()
    +createAccount(userId string, input CreateXmppAccountRequest) Promise~XmppAccount~
    +getAccount(userId string) Promise~XmppAccount or null~
    +getAccountById(accountId string) Promise~XmppAccount or null~
    +updateAccount(accountId string, input UpdateXmppAccountRequest) Promise~XmppAccount~
    +deleteAccount(accountId string) Promise~void~
    -determineUsername(providedUsername string, userEmail string) username or error
    -checkUsernameAvailability(username string) availability or error
  }

  XmppIntegration --|> IntegrationBase
  XmppIntegration ..> XmppAccount
  XmppIntegration ..> CreateXmppAccountRequest
  XmppIntegration ..> UpdateXmppAccountRequest
  IntegrationRegistry o--> Integration

  class IntegrationApiResponse~T~ {
    +boolean ok
    +string error
    +T account
    +IntegrationPublicInfo[] integrations
    +string message
  }

  class IntegrationApiClient {
    +fetchIntegrations() Promise~IntegrationPublicInfo[]~
    +fetchIntegrationAccount(integrationId string) Promise~T or null~
    +fetchIntegrationAccountById(integrationId string, id string) Promise~T~
    +createIntegrationAccount(integrationId string, input Record~string,unknown~) Promise~T~
    +updateIntegrationAccount(integrationId string, id string, input Record~string,unknown~) Promise~T~
    +deleteIntegrationAccount(integrationId string, id string) Promise~void~
  }

  IntegrationApiClient ..> IntegrationApiResponse
Loading

File-Level Changes

Change Details Files
Refactor XMPP API client to use generic integration account endpoints and types.
  • Replace direct /api/xmpp/accounts fetch logic with integration-aware helpers that call /api/integrations/xmpp/accounts endpoints for CRUD operations.
  • Swap XMPP-specific request/response typings to integration-scoped XMPP types that include integrationId and generic payloads.
  • Centralize account API error handling and null-on-404 semantics in shared integration helpers.
src/lib/api/xmpp.ts
src/lib/integrations/xmpp/types.ts
src/components/xmpp/xmpp-account-management.tsx
Add a generic integrations backend (registry, core interfaces, DB schema, and XMPP implementation) and API routes for managing integration accounts.
  • Define core integration types, base class, registry, factory, and status labels to model integrations and their accounts.
  • Introduce a shared integration_accounts table and migration for storing per-user integration account metadata.
  • Implement XmppIntegration atop the core abstraction, wiring it to Drizzle models and Prosody client functions, with validation, creation, update, and delete flows.
  • Add a user deletion helper that iterates all registered integrations and deletes related accounts with Sentry-logged failures.
  • Expose authenticated API endpoints for listing integrations and CRUD operations on integration accounts (current user and by id).
src/lib/integrations/core/types.ts
src/lib/integrations/core/base.ts
src/lib/integrations/core/registry.ts
src/lib/integrations/core/factory.ts
src/lib/integrations/core/constants.ts
src/lib/db/schema/integrations/base.ts
drizzle/20260107000000_integration_accounts/migration.sql
src/lib/integrations/xmpp/implementation.ts
src/lib/integrations/xmpp/index.ts
src/lib/integrations/index.ts
src/lib/integrations/core/user-deletion.ts
src/app/api/integrations/route.ts
src/app/api/integrations/[integration]/accounts/route.ts
src/app/api/integrations/[integration]/accounts/[id]/route.ts
src/app/api/xmpp/accounts/route.ts
src/app/api/xmpp/accounts/[id]/route.ts
src/lib/auth/config.ts
src/app/api/admin/users/[id]/route.ts
Introduce client-side integration APIs, hooks, and query keys for integrations and integration accounts.
  • Add a client API module that wraps /api/integrations* routes, handling lists, per-integration accounts, CRUD, and response typing.
  • Extend React Query queryKeys with integration-specific hierarchies for lists and accounts (current and by id).
  • Create generic hooks for fetching integrations and integration accounts, and for creating, updating, and deleting integration accounts while managing cache invalidation.
  • Export integration hooks and API helpers via existing barrel files.
src/lib/api/integrations.ts
src/lib/api/query-keys.ts
src/hooks/use-integration.ts
src/hooks/index.ts
src/lib/api/index.ts
Add a generic integrations dashboard page and UI components, and migrate XMPP management into it as the first concrete integration.
  • Replace the /app/xmpp route with /app/integrations in the static route config and update metadata lookups accordingly.
  • Add helpers to build dynamic integration routes from IntegrationPublicInfo and to merge them into the base RouteConfig.
  • Create a generic IntegrationManagement card that handles loading, error, create, and delete flows for any integration account, plus a simpler IntegrationCard wrapper.
  • Implement the IntegrationsContent client component that fetches available integrations and renders a per-integration management card, with XMPP-specific details (JID, username, status, timestamps, clipboard copy).
  • Swap the previous XmppPage to a new IntegrationsPage that uses IntegrationsContent and keeps per-request React Query hydration.
src/lib/routes/config.ts
src/app/(dashboard)/app/integrations/page.tsx
src/app/(dashboard)/app/integrations/integrations-content.tsx
src/components/integrations/integration-management.tsx
src/components/integrations/integration-card.tsx
Adjust XMPP integration configuration and client to be integration-scoped and lazily validated, and to live under the new integrations namespace.
  • Move XMPP config, client, utils, keys, and types under src/lib/integrations/xmpp and update all imports to point to the new paths.
  • Change XMPP configuration validation from eager (module-load) to exported validateXmppConfig plus an isXmppConfigured helper, and gate integration.enabled and client operations on these checks.
  • Ensure Prosody client operations call validateXmppConfig immediately before network calls and handle deletion rollback errors gracefully.
  • Update env.ts to pull XMPP env keys from the new integrations/xmpp module instead of the old path.
src/lib/integrations/xmpp/config.ts
src/lib/integrations/xmpp/client.ts
src/lib/integrations/xmpp/utils.ts
src/lib/integrations/xmpp/keys.ts
src/env.ts
src/app/api/xmpp/accounts/route.ts
src/app/api/xmpp/accounts/[id]/route.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 3 issues, and left some high level feedback:

  • In IntegrationManagement and IntegrationsContent, you're manually capitalizing and branching on status strings; consider reusing the integrationStatusLabels (and possibly a shared mapping to badge variants) so status rendering is consistent across all integrations and easier to extend.
  • The cleanupIntegrationAccounts helper processes integrations sequentially; if you expect multiple integrations per user, consider running the per-integration cleanup in parallel with Promise.allSettled to avoid unnecessary latency in user deletion workflows while still isolating individual failures.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `IntegrationManagement` and `IntegrationsContent`, you're manually capitalizing and branching on status strings; consider reusing the `integrationStatusLabels` (and possibly a shared mapping to badge variants) so status rendering is consistent across all integrations and easier to extend.
- The `cleanupIntegrationAccounts` helper processes integrations sequentially; if you expect multiple integrations per user, consider running the per-integration cleanup in parallel with `Promise.allSettled` to avoid unnecessary latency in user deletion workflows while still isolating individual failures.

## Individual Comments

### Comment 1
<location> `src/lib/integrations/core/base.ts:17` </location>
<code_context>
+  enabled?: boolean;
+}
+
+export abstract class IntegrationBase<
+  TAccount extends IntegrationAccount = IntegrationAccount,
+  TCreateInput extends IntegrationCreateInput = IntegrationCreateInput,
</code_context>

<issue_to_address>
**issue (complexity):** Consider replacing the abstract IntegrationBase class with a small factory helper so integrations implement the Integration interface directly and avoid an extra inheritance layer.

You can get the same type-safety and shared metadata without the extra inheritance layer by replacing `IntegrationBase` with a small helper and having implementations conform directly to `Integration`.

### 1. Replace `IntegrationBase` with a helper

```ts
// integrationBase.ts
import type {
  Integration,
  IntegrationAccount,
  IntegrationCreateInput,
  IntegrationUpdateInput,
} from "./types";

export interface IntegrationBaseOptions {
  id: string;
  name: string;
  description: string;
  enabled?: boolean;
}

export function createIntegrationBase(options: IntegrationBaseOptions) {
  return {
    id: options.id,
    name: options.name,
    description: options.description,
    enabled: options.enabled ?? true,
  } as const;
}
```

### 2. Implement `Integration` directly and spread the base metadata

```ts
// xmppIntegration.ts
import type {
  Integration,
  IntegrationAccount,
  IntegrationCreateInput,
  IntegrationUpdateInput,
} from "./types";
import { createIntegrationBase } from "./integrationBase";

type XmppAccount = IntegrationAccount;
type XmppCreateInput = IntegrationCreateInput;
type XmppUpdateInput = IntegrationUpdateInput;

export const XmppIntegration: Integration<
  XmppAccount,
  XmppCreateInput,
  XmppUpdateInput
> = {
  ...createIntegrationBase({
    id: "xmpp",
    name: "XMPP",
    description: "XMPP integration",
    enabled: true,
  }),

  async createAccount(userId, input) {
    // existing logic
  },

  async getAccount(userId) {
    // existing logic
  },

  async updateAccount(accountId, input) {
    // existing logic
  },

  async deleteAccount(accountId) {
    // existing logic
  },
};
```

This keeps:

- The same fields (`id`, `name`, `description`, `enabled`) with identical defaulting behavior.
- The same `Integration<TAccount, TCreateInput, TUpdateInput>` contract.

But it removes:

- The abstract class and inheritance.
- Duplication of method signatures between `Integration` and `IntegrationBase`.

If you need to keep a class for some integrations, you can still use the helper inside a class without making it a base class:

```ts
class XmppIntegration
  implements Integration<XmppAccount, XmppCreateInput, XmppUpdateInput>
{
  readonly id: string;
  readonly name: string;
  readonly description: string;
  readonly enabled: boolean;

  constructor() {
    Object.assign(this, createIntegrationBase({
      id: "xmpp",
      name: "XMPP",
      description: "XMPP integration",
    }));
  }

  // same method implementations as before...
}
```
</issue_to_address>

### Comment 2
<location> `src/lib/db/schema/integrations/base.ts:13` </location>
<code_context>
+
+import { user } from "../auth";
+
+export const integrationAccountStatusEnum = pgEnum(
+  "integration_account_status",
+  ["active", "suspended", "deleted"]
</code_context>

<issue_to_address>
**issue (complexity):** Consider reducing conceptual duplication by reusing a shared account status enum and introducing an explicit enum for integrationType, plus a brief comment documenting the future role of this table.

You’re effectively introducing a second “account” model and a second status enum without wiring this into any runtime path yet, which is what’s driving the complexity feedback.

Two concrete ways to reduce conceptual duplication while keeping this table and all functionality:

1. **Share the status enum with the existing account model**

If `xmppAccount` already has a status enum, you can extract it into a shared enum and reuse it here instead of defining `integrationAccountStatusEnum`. That way there’s only one “account status” concept to reason about.

```ts
// shared/account-status.ts
import { pgEnum } from "drizzle-orm/pg-core";

export const accountStatusEnum = pgEnum("account_status", [
  "active",
  "suspended",
  "deleted",
]);

// xmpp-account.ts
import { accountStatusEnum } from "../shared/account-status";

export const xmppAccount = pgTable("xmpp_accounts", {
  // ...
  status: accountStatusEnum("status").default("active").notNull(),
  // ...
});

// integration-account.ts
import { accountStatusEnum } from "../shared/account-status";

export const integrationAccount = pgTable(
  "integration_accounts",
  {
    // ...
    status: accountStatusEnum("status").default("active").notNull(),
    // ...
  },
  // ...
);
```

This makes it clear that “status” means the same thing across all account tables and removes one mental branch.

2. **Make `integrationType` explicit with an enum (even if there’s only one for now)**

Right now `integrationType` is a `text`, which encourages arbitrary strings and makes it harder to reason about what’s valid. A small enum constrained to the integrations you plan to support keeps the abstraction but makes it more concrete and self-documenting:

```ts
// shared/integration-type.ts
import { pgEnum } from "drizzle-orm/pg-core";

export const integrationTypeEnum = pgEnum("integration_type", [
  "xmpp",
  // add future integrations here: "slack", "discord", ...
]);

// integration-account.ts
import { integrationTypeEnum } from "../shared/integration-type";

export const integrationAccount = pgTable(
  "integration_accounts",
  {
    // ...
    integrationType: integrationTypeEnum("integration_type").notNull(),
    // ...
  },
  (table) => [
    index("integration_accounts_userId_idx").on(table.userId),
    index("integration_accounts_type_idx").on(table.integrationType),
    uniqueIndex("integration_accounts_userId_type_idx").on(
      table.userId,
      table.integrationType,
    ),
  ],
);
```

This doesn’t change behavior but reduces “generic string” ambiguity and makes it clearer how this table will evolve as more integrations are added.

If you keep the table unused for now, consider at least adding a brief comment above it describing the planned migration path (e.g., “will back all integrations, including xmppAccount, once X/Y rollout is done”) so future readers don’t have to infer intent.
</issue_to_address>

### Comment 3
<location> `src/components/integrations/integration-management.tsx:180` </location>
<code_context>
+    );
+  }
+
+  const status =
+    "status" in account && typeof account.status === "string"
+      ? account.status
</code_context>

<issue_to_address>
**issue (complexity):** Consider tightening the account typing and extracting the create and account-present branches into smaller components (optionally via a hook) to simplify IntegrationManagement’s logic and layout.

You can keep all functionality while reducing complexity by:

1. **Tightening the account type instead of runtime duck-typing**
2. **Extracting small presentational components for the “no account” and “has account” branches**
3. **Optionally extracting the data/mutation wiring into a hook**

### 1. Use a more concrete account shape

You’re already assuming `status` is a string when present. Make that part of the type instead of checking at runtime:

```ts
interface IntegrationAccountBase {
  id: string;
  status?: string; // optional but typed
}

interface IntegrationManagementProps<TAccount extends IntegrationAccountBase> {
  integrationId: string;
  title: string;
  description: string;
  createLabel: string;
  createInputLabel?: string;
  createInputPlaceholder?: string;
  createInputHelp?: string;
  createInputToPayload?: (value: string) => Record<string, unknown>;
  renderAccountDetails?: (account: TAccount) => ReactNode;
}

export function IntegrationManagement<TAccount extends IntegrationAccountBase>(props: IntegrationManagementProps<TAccount>) {
  // ...
  const status = account?.status ?? null;
  // ...
}
```

This removes the need for:

```ts
const status =
  "status" in account && typeof account.status === "string"
    ? account.status
    : null;
```

and avoids mixing generics with runtime duck-typing.

### 2. Extract the “no account yet” create form

The `if (!account)` branch is large; pulling it out into a small child component cuts branching and makes the main component easier to scan:

```tsx
interface NoAccountCardProps {
  integrationId: string;
  title: string;
  description: string;
  createLabel: string;
  createInputLabel?: string;
  createInputPlaceholder?: string;
  createInputHelp?: string;
  isCreating: boolean;
  inputValue: string;
  onInputChange: (value: string) => void;
  onCreate: () => void;
}

function NoAccountCard({
  integrationId,
  title,
  description,
  createLabel,
  createInputLabel,
  createInputPlaceholder,
  createInputHelp,
  isCreating,
  inputValue,
  onInputChange,
  onCreate,
}: NoAccountCardProps) {
  return (
    <Card>
      <CardHeader>
        <div className="font-semibold text-lg">{title}</div>
        <p className="text-muted-foreground text-sm">{description}</p>
      </CardHeader>
      <CardContent className="space-y-4">
        {createInputLabel ? (
          <div className="space-y-2">
            <Label htmlFor={`${integrationId}-identifier`}>{createInputLabel}</Label>
            <Input
              disabled={isCreating}
              id={`${integrationId}-identifier`}
              onChange={(event) => onInputChange(event.target.value)}
              placeholder={createInputPlaceholder}
              value={inputValue}
            />
            {createInputHelp ? (
              <p className="text-muted-foreground text-sm">{createInputHelp}</p>
            ) : null}
          </div>
        ) : null}
      </CardContent>
      <CardFooter>
        <Button className="w-full" disabled={isCreating} onClick={onCreate}>
          {isCreating ? (
            <>
              <Loader2 className="mr-2 h-4 w-4 animate-spin" />
              Creating...
            </>
          ) : (
            createLabel
          )}
        </Button>
      </CardFooter>
    </Card>
  );
}
```

Then in `IntegrationManagement`:

```tsx
if (!account) {
  return (
    <NoAccountCard
      integrationId={integrationId}
      title={title}
      description={description}
      createLabel={createLabel}
      createInputLabel={createInputLabel}
      createInputPlaceholder={createInputPlaceholder}
      createInputHelp={createInputHelp}
      isCreating={createMutation.isPending}
      inputValue={inputValue}
      onInputChange={setInputValue}
      onCreate={handleCreate}
    />
  );
}
```

### 3. Extract the “account present” card

Likewise, the “has account” branch can be a focused presentational component:

```tsx
interface AccountCardProps<TAccount extends IntegrationAccountBase> {
  title: string;
  description: string;
  account: TAccount;
  status?: string | null;
  isDeleting: boolean;
  renderAccountDetails?: (account: TAccount) => ReactNode;
  onDelete: () => void;
}

function AccountCard<TAccount extends IntegrationAccountBase>({
  title,
  description,
  account,
  status,
  isDeleting,
  renderAccountDetails,
  onDelete,
}: AccountCardProps<TAccount>) {
  return (
    <Card>
      <CardHeader>
        <div className="flex items-center justify-between">
          <div>
            <div className="font-semibold text-lg">{title}</div>
            <p className="text-muted-foreground text-sm">{description}</p>
          </div>
          {status ? (
            <Badge variant={status === "deleted" ? "destructive" : "default"}>
              {status.charAt(0).toUpperCase() + status.slice(1)}
            </Badge>
          ) : null}
        </div>
      </CardHeader>
      <CardContent className="space-y-4">
        {renderAccountDetails ? renderAccountDetails(account) : null}
      </CardContent>
      <CardFooter className="flex justify-end">
        <AlertDialog>
          <AlertDialogTrigger asChild>
            <Button disabled={isDeleting} variant="destructive">
              {isDeleting ? (
                <>
                  <Loader2 className="mr-2 h-4 w-4 animate-spin" />
                  Deleting...
                </>
              ) : (
                <>
                  <Trash2 className="mr-2 h-4 w-4" />
                  Delete Account
                </>
              )}
            </Button>
          </AlertDialogTrigger>
          <AlertDialogContent>
            <AlertDialogHeader>
              <AlertDialogTitle>Delete {title} Account?</AlertDialogTitle>
              <AlertDialogDescription>
                This will permanently delete your {title} account. This action cannot be undone. You will need to create a new account if you want to use {title} again.
              </AlertDialogDescription>
            </AlertDialogHeader>
            <AlertDialogFooter>
              <AlertDialogCancel>Cancel</AlertDialogCancel>
              <AlertDialogAction
                className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
                onClick={onDelete}
              >
                Delete Account
              </AlertDialogAction>
            </AlertDialogFooter>
          </AlertDialogContent>
        </AlertDialog>
      </CardFooter>
    </Card>
  );
}
```

Usage in `IntegrationManagement`:

```tsx
return (
  <AccountCard
    title={title}
    description={description}
    account={account}
    status={status}
    isDeleting={deleteMutation.isPending}
    renderAccountDetails={renderAccountDetails}
    onDelete={handleDelete}
  />
);
```

### 4. Optional: move data/mutation wiring to a hook

If you want to further separate concerns, you can wrap the query/mutations + handlers into a small hook:

```ts
function useIntegrationManagement<TAccount extends IntegrationAccountBase>(
  integrationId: string,
  title: string,
  createInputToPayload?: (value: string) => Record<string, unknown>
) {
  const { data: account, isLoading, error } = useIntegrationAccount<TAccount>(integrationId);
  const createMutation = useCreateIntegrationAccount<TAccount>(integrationId);
  const deleteMutation = useDeleteIntegrationAccount(integrationId);
  const [inputValue, setInputValue] = useState("");

  const handleCreate = async () => { /* existing logic */ };
  const handleDelete = async () => { /* existing logic */ };

  return {
    account,
    isLoading,
    error,
    createMutation,
    deleteMutation,
    inputValue,
    setInputValue,
    handleCreate,
    handleDelete,
  };
}
```

Then `IntegrationManagement` becomes mostly composition of `NoAccountCard` / `AccountCard`, improving readability and testability without changing behavior.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

enabled?: boolean;
}

export abstract class IntegrationBase<
Copy link

Choose a reason for hiding this comment

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

issue (complexity): Consider replacing the abstract IntegrationBase class with a small factory helper so integrations implement the Integration interface directly and avoid an extra inheritance layer.

You can get the same type-safety and shared metadata without the extra inheritance layer by replacing IntegrationBase with a small helper and having implementations conform directly to Integration.

1. Replace IntegrationBase with a helper

// integrationBase.ts
import type {
  Integration,
  IntegrationAccount,
  IntegrationCreateInput,
  IntegrationUpdateInput,
} from "./types";

export interface IntegrationBaseOptions {
  id: string;
  name: string;
  description: string;
  enabled?: boolean;
}

export function createIntegrationBase(options: IntegrationBaseOptions) {
  return {
    id: options.id,
    name: options.name,
    description: options.description,
    enabled: options.enabled ?? true,
  } as const;
}

2. Implement Integration directly and spread the base metadata

// xmppIntegration.ts
import type {
  Integration,
  IntegrationAccount,
  IntegrationCreateInput,
  IntegrationUpdateInput,
} from "./types";
import { createIntegrationBase } from "./integrationBase";

type XmppAccount = IntegrationAccount;
type XmppCreateInput = IntegrationCreateInput;
type XmppUpdateInput = IntegrationUpdateInput;

export const XmppIntegration: Integration<
  XmppAccount,
  XmppCreateInput,
  XmppUpdateInput
> = {
  ...createIntegrationBase({
    id: "xmpp",
    name: "XMPP",
    description: "XMPP integration",
    enabled: true,
  }),

  async createAccount(userId, input) {
    // existing logic
  },

  async getAccount(userId) {
    // existing logic
  },

  async updateAccount(accountId, input) {
    // existing logic
  },

  async deleteAccount(accountId) {
    // existing logic
  },
};

This keeps:

  • The same fields (id, name, description, enabled) with identical defaulting behavior.
  • The same Integration<TAccount, TCreateInput, TUpdateInput> contract.

But it removes:

  • The abstract class and inheritance.
  • Duplication of method signatures between Integration and IntegrationBase.

If you need to keep a class for some integrations, you can still use the helper inside a class without making it a base class:

class XmppIntegration
  implements Integration<XmppAccount, XmppCreateInput, XmppUpdateInput>
{
  readonly id: string;
  readonly name: string;
  readonly description: string;
  readonly enabled: boolean;

  constructor() {
    Object.assign(this, createIntegrationBase({
      id: "xmpp",
      name: "XMPP",
      description: "XMPP integration",
    }));
  }

  // same method implementations as before...
}


import { user } from "../auth";

export const integrationAccountStatusEnum = pgEnum(
Copy link

Choose a reason for hiding this comment

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

issue (complexity): Consider reducing conceptual duplication by reusing a shared account status enum and introducing an explicit enum for integrationType, plus a brief comment documenting the future role of this table.

You’re effectively introducing a second “account” model and a second status enum without wiring this into any runtime path yet, which is what’s driving the complexity feedback.

Two concrete ways to reduce conceptual duplication while keeping this table and all functionality:

  1. Share the status enum with the existing account model

If xmppAccount already has a status enum, you can extract it into a shared enum and reuse it here instead of defining integrationAccountStatusEnum. That way there’s only one “account status” concept to reason about.

// shared/account-status.ts
import { pgEnum } from "drizzle-orm/pg-core";

export const accountStatusEnum = pgEnum("account_status", [
  "active",
  "suspended",
  "deleted",
]);

// xmpp-account.ts
import { accountStatusEnum } from "../shared/account-status";

export const xmppAccount = pgTable("xmpp_accounts", {
  // ...
  status: accountStatusEnum("status").default("active").notNull(),
  // ...
});

// integration-account.ts
import { accountStatusEnum } from "../shared/account-status";

export const integrationAccount = pgTable(
  "integration_accounts",
  {
    // ...
    status: accountStatusEnum("status").default("active").notNull(),
    // ...
  },
  // ...
);

This makes it clear that “status” means the same thing across all account tables and removes one mental branch.

  1. Make integrationType explicit with an enum (even if there’s only one for now)

Right now integrationType is a text, which encourages arbitrary strings and makes it harder to reason about what’s valid. A small enum constrained to the integrations you plan to support keeps the abstraction but makes it more concrete and self-documenting:

// shared/integration-type.ts
import { pgEnum } from "drizzle-orm/pg-core";

export const integrationTypeEnum = pgEnum("integration_type", [
  "xmpp",
  // add future integrations here: "slack", "discord", ...
]);

// integration-account.ts
import { integrationTypeEnum } from "../shared/integration-type";

export const integrationAccount = pgTable(
  "integration_accounts",
  {
    // ...
    integrationType: integrationTypeEnum("integration_type").notNull(),
    // ...
  },
  (table) => [
    index("integration_accounts_userId_idx").on(table.userId),
    index("integration_accounts_type_idx").on(table.integrationType),
    uniqueIndex("integration_accounts_userId_type_idx").on(
      table.userId,
      table.integrationType,
    ),
  ],
);

This doesn’t change behavior but reduces “generic string” ambiguity and makes it clearer how this table will evolve as more integrations are added.

If you keep the table unused for now, consider at least adding a brief comment above it describing the planned migration path (e.g., “will back all integrations, including xmppAccount, once X/Y rollout is done”) so future readers don’t have to infer intent.

);
}

const status =
Copy link

Choose a reason for hiding this comment

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

issue (complexity): Consider tightening the account typing and extracting the create and account-present branches into smaller components (optionally via a hook) to simplify IntegrationManagement’s logic and layout.

You can keep all functionality while reducing complexity by:

  1. Tightening the account type instead of runtime duck-typing
  2. Extracting small presentational components for the “no account” and “has account” branches
  3. Optionally extracting the data/mutation wiring into a hook

1. Use a more concrete account shape

You’re already assuming status is a string when present. Make that part of the type instead of checking at runtime:

interface IntegrationAccountBase {
  id: string;
  status?: string; // optional but typed
}

interface IntegrationManagementProps<TAccount extends IntegrationAccountBase> {
  integrationId: string;
  title: string;
  description: string;
  createLabel: string;
  createInputLabel?: string;
  createInputPlaceholder?: string;
  createInputHelp?: string;
  createInputToPayload?: (value: string) => Record<string, unknown>;
  renderAccountDetails?: (account: TAccount) => ReactNode;
}

export function IntegrationManagement<TAccount extends IntegrationAccountBase>(props: IntegrationManagementProps<TAccount>) {
  // ...
  const status = account?.status ?? null;
  // ...
}

This removes the need for:

const status =
  "status" in account && typeof account.status === "string"
    ? account.status
    : null;

and avoids mixing generics with runtime duck-typing.

2. Extract the “no account yet” create form

The if (!account) branch is large; pulling it out into a small child component cuts branching and makes the main component easier to scan:

interface NoAccountCardProps {
  integrationId: string;
  title: string;
  description: string;
  createLabel: string;
  createInputLabel?: string;
  createInputPlaceholder?: string;
  createInputHelp?: string;
  isCreating: boolean;
  inputValue: string;
  onInputChange: (value: string) => void;
  onCreate: () => void;
}

function NoAccountCard({
  integrationId,
  title,
  description,
  createLabel,
  createInputLabel,
  createInputPlaceholder,
  createInputHelp,
  isCreating,
  inputValue,
  onInputChange,
  onCreate,
}: NoAccountCardProps) {
  return (
    <Card>
      <CardHeader>
        <div className="font-semibold text-lg">{title}</div>
        <p className="text-muted-foreground text-sm">{description}</p>
      </CardHeader>
      <CardContent className="space-y-4">
        {createInputLabel ? (
          <div className="space-y-2">
            <Label htmlFor={`${integrationId}-identifier`}>{createInputLabel}</Label>
            <Input
              disabled={isCreating}
              id={`${integrationId}-identifier`}
              onChange={(event) => onInputChange(event.target.value)}
              placeholder={createInputPlaceholder}
              value={inputValue}
            />
            {createInputHelp ? (
              <p className="text-muted-foreground text-sm">{createInputHelp}</p>
            ) : null}
          </div>
        ) : null}
      </CardContent>
      <CardFooter>
        <Button className="w-full" disabled={isCreating} onClick={onCreate}>
          {isCreating ? (
            <>
              <Loader2 className="mr-2 h-4 w-4 animate-spin" />
              Creating...
            </>
          ) : (
            createLabel
          )}
        </Button>
      </CardFooter>
    </Card>
  );
}

Then in IntegrationManagement:

if (!account) {
  return (
    <NoAccountCard
      integrationId={integrationId}
      title={title}
      description={description}
      createLabel={createLabel}
      createInputLabel={createInputLabel}
      createInputPlaceholder={createInputPlaceholder}
      createInputHelp={createInputHelp}
      isCreating={createMutation.isPending}
      inputValue={inputValue}
      onInputChange={setInputValue}
      onCreate={handleCreate}
    />
  );
}

3. Extract the “account present” card

Likewise, the “has account” branch can be a focused presentational component:

interface AccountCardProps<TAccount extends IntegrationAccountBase> {
  title: string;
  description: string;
  account: TAccount;
  status?: string | null;
  isDeleting: boolean;
  renderAccountDetails?: (account: TAccount) => ReactNode;
  onDelete: () => void;
}

function AccountCard<TAccount extends IntegrationAccountBase>({
  title,
  description,
  account,
  status,
  isDeleting,
  renderAccountDetails,
  onDelete,
}: AccountCardProps<TAccount>) {
  return (
    <Card>
      <CardHeader>
        <div className="flex items-center justify-between">
          <div>
            <div className="font-semibold text-lg">{title}</div>
            <p className="text-muted-foreground text-sm">{description}</p>
          </div>
          {status ? (
            <Badge variant={status === "deleted" ? "destructive" : "default"}>
              {status.charAt(0).toUpperCase() + status.slice(1)}
            </Badge>
          ) : null}
        </div>
      </CardHeader>
      <CardContent className="space-y-4">
        {renderAccountDetails ? renderAccountDetails(account) : null}
      </CardContent>
      <CardFooter className="flex justify-end">
        <AlertDialog>
          <AlertDialogTrigger asChild>
            <Button disabled={isDeleting} variant="destructive">
              {isDeleting ? (
                <>
                  <Loader2 className="mr-2 h-4 w-4 animate-spin" />
                  Deleting...
                </>
              ) : (
                <>
                  <Trash2 className="mr-2 h-4 w-4" />
                  Delete Account
                </>
              )}
            </Button>
          </AlertDialogTrigger>
          <AlertDialogContent>
            <AlertDialogHeader>
              <AlertDialogTitle>Delete {title} Account?</AlertDialogTitle>
              <AlertDialogDescription>
                This will permanently delete your {title} account. This action cannot be undone. You will need to create a new account if you want to use {title} again.
              </AlertDialogDescription>
            </AlertDialogHeader>
            <AlertDialogFooter>
              <AlertDialogCancel>Cancel</AlertDialogCancel>
              <AlertDialogAction
                className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
                onClick={onDelete}
              >
                Delete Account
              </AlertDialogAction>
            </AlertDialogFooter>
          </AlertDialogContent>
        </AlertDialog>
      </CardFooter>
    </Card>
  );
}

Usage in IntegrationManagement:

return (
  <AccountCard
    title={title}
    description={description}
    account={account}
    status={status}
    isDeleting={deleteMutation.isPending}
    renderAccountDetails={renderAccountDetails}
    onDelete={handleDelete}
  />
);

4. Optional: move data/mutation wiring to a hook

If you want to further separate concerns, you can wrap the query/mutations + handlers into a small hook:

function useIntegrationManagement<TAccount extends IntegrationAccountBase>(
  integrationId: string,
  title: string,
  createInputToPayload?: (value: string) => Record<string, unknown>
) {
  const { data: account, isLoading, error } = useIntegrationAccount<TAccount>(integrationId);
  const createMutation = useCreateIntegrationAccount<TAccount>(integrationId);
  const deleteMutation = useDeleteIntegrationAccount(integrationId);
  const [inputValue, setInputValue] = useState("");

  const handleCreate = async () => { /* existing logic */ };
  const handleDelete = async () => { /* existing logic */ };

  return {
    account,
    isLoading,
    error,
    createMutation,
    deleteMutation,
    inputValue,
    setInputValue,
    handleCreate,
    handleDelete,
  };
}

Then IntegrationManagement becomes mostly composition of NoAccountCard / AccountCard, improving readability and testability without changing behavior.

@coderabbitai
Copy link

coderabbitai bot commented Jan 16, 2026

Caution

Review failed

The pull request is closed.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Generalizes XMPP into a pluggable Integrations framework: adds integration_accounts DB schema, core integration types/registry/base, XMPP integration implementation, server APIs for integrations and accounts, client hooks and UI components, route/config updates, and user-deletion cleanup to remove integration accounts.

Changes

Cohort / File(s) Summary
Database schema & migration
drizzle/20260107000000_integration_accounts/migration.sql, src/lib/db/schema/integrations/base.ts, src/lib/db/schema/index.ts
New integration_accounts table and integration_account_status enum; indexes on user/type and unique (user,type); FK to user(id) ON DELETE CASCADE; exported in schema.
Core integration framework
src/lib/integrations/core/*, src/lib/integrations/index.ts
New types, IntegrationBase abstract, registry, factory helper, status labels, cleanup utility, and idempotent registerIntegrations entrypoint.
XMPP integration
src/lib/integrations/xmpp/*
New XmppIntegration implementation, lazy config validation and isXmppConfigured, client calls validateXmppConfig, updated types (integrationId), barrel exports.
Server API routes (integrations & accounts)
src/app/api/integrations/route.ts, src/app/api/integrations/[integration]/accounts/route.ts, src/app/api/integrations/[integration]/accounts/[id]/route.ts
New GET integrations list; GET/POST for accounts list; GET/PATCH/DELETE for individual accounts with auth, registry resolution, owner/admin checks, and centralized API error handling.
XMPP API surface removed / refactor
src/app/api/xmpp/* (deleted), src/lib/api/xmpp.ts (deleted), src/components/xmpp/* (deleted), src/hooks/use-xmpp-account.ts (deleted), src/env.ts
Removed legacy XMPP-specific API, hooks and component files; imports redirected to new integrations XMPP modules; env import path updated.
Client API & query keys
src/lib/api/integrations.ts, src/lib/api/index.ts, src/lib/api/query-keys.ts
New client API for integrations/accounts (fetch/create/update/delete); integrations queryKeys added; API barrel updated to export integrations and remove xmpp.
React hooks & exports
src/hooks/use-integration.ts, src/hooks/index.ts
New React Query hooks: useIntegrations, useIntegrationAccount(s), and create/update/delete mutations; exported from hooks barrel.
UI components & pages
src/components/integrations/integration-card.tsx, src/components/integrations/integration-management.tsx, src/app/(dashboard)/app/integrations/integrations-content.tsx, src/app/(dashboard)/app/integrations/page.tsx
New IntegrationCard and generic IntegrationManagement components; IntegrationsContent added and page renamed from XmppPage → IntegrationsPage.
Routes config & locale
src/lib/routes/config.ts, locale/en/routes.json
Renamed protected route id/path from xmppintegrations; added helpers to build per-integration protected routes and merge into route config; locale route label updated.
Auth/admin cleanup integration
src/lib/auth/config.ts, src/app/api/admin/users/[id]/route.ts
Added calls to cleanupIntegrationAccounts in user.deleteUser beforeDelete hook and in admin user DELETE flow to remove integration accounts prior to user deletion.
Build config
next.config.ts
Added serverExternalPackages entries for native Sentry packages to avoid bundling.
Docs
docs/INTEGRATIONS.md
New comprehensive documentation describing the Integrations framework, patterns, and XMPP reference integration.

Sequence Diagram(s)

sequenceDiagram
    participant Browser as Client
    participant UI as Integrations UI
    participant API as Integrations API
    participant Registry as Integration Registry
    participant Integration as XmppIntegration
    participant DB as Database
    participant Prosody as Prosody Server

    Browser->>UI: Open /app/integrations
    UI->>API: GET /api/integrations
    API->>Registry: getPublicInfo()
    Registry-->>API: [IntegrationPublicInfo]
    API-->>UI: 200 { integrations }

    Browser->>UI: Create XMPP account (input)
    UI->>API: POST /api/integrations/xmpp/accounts
    API->>Registry: get("xmpp")
    Registry-->>API: XmppIntegration
    API->>Integration: createAccount(userId, input)
    Integration->>DB: check existing account / insert record
    Integration->>Prosody: createProsodyAccount(...)
    Prosody-->>Integration: success
    Integration-->>API: created account
    API-->>UI: 201 { account }
    UI-->>Browser: render account
Loading
sequenceDiagram
    participant Admin as Admin Client
    participant Auth as Auth Service
    participant Cleanup as Integration Cleanup
    participant Registry as Integration Registry
    participant Integration as Integration
    participant DB as Database

    Admin->>Auth: DELETE /api/admin/users/{id}
    Auth->>Cleanup: cleanupIntegrationAccounts(userId)
    Cleanup->>Registry: getEnabled()
    Registry-->>Cleanup: [integrations]
    loop per integration
        Cleanup->>Integration: getAccount(userId)
        Integration->>DB: fetch account
        alt account exists
            Cleanup->>Integration: deleteAccount(accountId)
            Integration->>DB: update status -> "deleted"
        end
    end
    Cleanup-->>Auth: done
    Auth->>DB: delete user record
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'feat: integrations' is concise and directly reflects the main objective of the PR, which is to introduce a generic integrations framework.
Description check ✅ Passed The PR description comprehensively covers the changes across all affected files and systems, providing context for the generic integrations framework, API routes, UI components, and migration of XMPP management.
Docstring Coverage ✅ Passed Docstring coverage is 88.10% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/integrations

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@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: 5

🤖 Fix all issues with AI agents
In `@src/app/`(dashboard)/app/integrations/integrations-content.tsx:
- Around line 63-95: Wrap the copy routine in a Sentry span and add an
aria-label to the icon-only Button: start a Sentry span (e.g., name
"copy_jid_button_click") when the onClick handler begins, finish the span after
the clipboard operation completes (both success and fallback), and capture any
thrown error with Sentry.captureException inside the catch block (in addition to
the existing toast.error) so failures are reported; also add an aria-label prop
to the Button (e.g., aria-label="Copy JID") to make the Copy icon accessible.

In `@src/app/api/integrations/`[integration]/accounts/[id]/route.ts:
- Around line 134-154: The handler verifies integration.getAccountById but never
checks that integration.deleteAccount exists before calling it; update the route
to verify the delete capability (e.g., check integration.deleteAccount) and
throw an APIError (or return 400) like the getAccountById check if the
integration does not support deletion, then call integration.deleteAccount(id)
only after that check and existing authorization/account checks; reference the
existing symbols getAccountById, deleteAccount, APIError, and the current
account and isAdmin(userId) checks when adding the capability guard.
- Around line 102-103: Validate the incoming PATCH body before calling
integration.updateAccount by parsing request.json() into a defined schema (e.g.,
check required fields/types and return a 400/422 on invalid input) and then
verify the integration supports updates the same way getAccountById checks
capabilities (e.g., check an integration capability flag or presence of an
update capability/method) before invoking integration.updateAccount(id, body);
if the integration does not support updates, return the appropriate 405/400
error response.

In `@src/app/api/integrations/`[integration]/accounts/route.ts:
- Around line 69-71: Replace the silent JSON swallow and add explicit Zod
validation at the route boundary: remove .catch(()=>({})) on request.json(),
define/import a Zod schema (e.g. AccountCreateSchema) for the expected request
body, parse and validate the JSON payload using AccountCreateSchema.parse or
safeParse and return a 400 error when validation fails, and then pass the
validated data to integration.createAccount(userId, validatedBody); ensure you
import zod and the schema at the top of the file and keep
integration.createAccount calls unchanged.

In `@src/lib/integrations/xmpp/config.ts`:
- Around line 39-44: The validateXmppConfig function currently runs lazily;
change startup flow so XMPP config is validated during backend initialization:
update the integration registration/startup code that decides whether XMPP is
enabled (e.g., the function or flag like isXmppEnabled(), enableXmpp, or similar
integration registration routine) to call validateXmppConfig() immediately when
XMPP is enabled, and ensure validateXmppConfig throws on missing/invalid env so
startup fails fast; do not rely on first-use calls—invoke validateXmppConfig
from the integration registration/initialization path so misconfigurations
surface at startup.
🧹 Nitpick comments (13)
src/lib/api/index.ts (1)

8-8: Consider importing integrations directly instead of expanding the API barrel.

This re-export widens the barrel surface; if feasible, keep consumers importing from @/lib/api/integrations to preserve selective imports and tree‑shaking. As per coding guidelines, prefer selective imports and avoid barrel exports.

src/lib/integrations/core/types.ts (1)

3-5: Consider deriving the array from the type to prevent drift.

The union type and the array are defined separately, which could lead to inconsistencies if one is updated without the other.

♻️ Suggested refactor
-export type IntegrationStatus = "active" | "suspended" | "deleted";
-
-export const integrationStatuses = ["active", "suspended", "deleted"] as const;
+export const integrationStatuses = ["active", "suspended", "deleted"] as const;
+
+export type IntegrationStatus = (typeof integrationStatuses)[number];
src/app/api/integrations/route.ts (1)

1-23: Add Sentry span instrumentation for the API handler to improve performance monitoring.

The route handler would benefit from explicit span instrumentation with Sentry.startSpan() to track this endpoint's execution. Use op: "http.server" with a descriptive name like "GET /api/integrations" and optionally attach attributes for request metrics.

Note: Errors are already captured via handleAPIError(), so this is an enhancement for observability and performance monitoring rather than error tracking.

Proposed implementation
+import * as Sentry from "@sentry/nextjs";
 import type { NextRequest } from "next/server";

 import { handleAPIError, requireAuth } from "@/lib/api/utils";
 import { registerIntegrations } from "@/lib/integrations";
 import { getIntegrationRegistry } from "@/lib/integrations/core/registry";

 export const dynamic = "force-dynamic";

 /**
  * GET /api/integrations
  * List available integrations
  */
 export async function GET(request: NextRequest) {
-  try {
-    await requireAuth(request);
-
-    registerIntegrations();
-    const integrations = getIntegrationRegistry().getPublicInfo();
-
-    return Response.json({ ok: true, integrations });
-  } catch (error) {
-    return handleAPIError(error);
-  }
+  return Sentry.startSpan(
+    { name: "GET /api/integrations", op: "http.server" },
+    async () => {
+      try {
+        await requireAuth(request);
+        registerIntegrations();
+        const integrations = getIntegrationRegistry().getPublicInfo();
+        return Response.json({ ok: true, integrations });
+      } catch (error) {
+        return handleAPIError(error);
+      }
+    }
+  );
 }
src/components/integrations/integration-card.tsx (1)

18-28: Consider using CardTitle and CardDescription for semantic consistency.

The component works correctly, but the Card component likely exports CardTitle and CardDescription sub-components that provide semantic markup and consistent styling. Using these would align with shadcn/ui conventions.

♻️ Optional: Use semantic Card sub-components
-import { Card, CardContent, CardHeader } from "@/components/ui/card";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";

 // ...

 return (
   <Card>
     <CardHeader className="space-y-1">
-      <div className="font-semibold text-lg">{title}</div>
-      {description ? (
-        <p className="text-muted-foreground text-sm">{description}</p>
-      ) : null}
+      <CardTitle>{title}</CardTitle>
+      {description ? <CardDescription>{description}</CardDescription> : null}
     </CardHeader>
     <CardContent>{children}</CardContent>
   </Card>
 );
src/lib/integrations/xmpp/client.ts (2)

3-64: Add a Sentry span around Prosody REST calls.

Wrap the HTTP call in Sentry.startSpan with meaningful op and name, and include request attributes.

✅ Suggested update
-import { validateXmppConfig, xmppConfig } from "./config";
+import * as Sentry from "@sentry/nextjs";
+import { validateXmppConfig, xmppConfig } from "./config";

...
-  const response = await fetch(url, {
-    ...options,
-    headers: {
-      "Content-Type": "application/xml",
-      Authorization: createAuthHeader(),
-      ...options.headers,
-    },
-  });
+  const response = await Sentry.startSpan(
+    {
+      op: "http.client",
+      name: `Prosody REST ${options.method ?? "GET"} ${endpoint}`,
+      attributes: { endpoint, url },
+    },
+    () =>
+      fetch(url, {
+        ...options,
+        headers: {
+          "Content-Type": "application/xml",
+          Authorization: createAuthHeader(),
+          ...options.headers,
+        },
+      })
+  );

114-137: Capture Prosody errors in Sentry before rethrowing.

Add Sentry.captureException for unexpected errors (skip expected cases like “not found” / “conflict”).

✅ Example (apply similarly to the other catch blocks)
   } catch (error) {
     if (error instanceof Error) {
       // Check if account already exists
       if (
         error.message.includes("exists") ||
         error.message.includes("409") ||
         error.message.includes("conflict")
       ) {
         throw new Error(`XMPP account already exists: ${jid}`);
       }
+      Sentry.captureException(error);
       throw error;
     }
+    Sentry.captureException(error);
     throw new Error("Failed to create Prosody account");
   }

Also applies to: 162-186, 199-213

src/components/integrations/integration-management.tsx (2)

67-87: Missing Sentry error capture in catch block.

Per coding guidelines, exceptions should be captured with Sentry.captureException(error) in catch blocks. The error is shown to the user via toast but not reported to Sentry for monitoring.

🔧 Proposed fix
+"use client";
+
+import * as Sentry from "@sentry/nextjs";
 import type { ReactNode } from "react";
 // ... other imports
 
   const handleCreate = async () => {
     try {
       const trimmed = inputValue.trim();
       const payload =
         createInputToPayload?.(trimmed) ??
         (trimmed ? { identifier: trimmed } : {});

       await createMutation.mutateAsync(payload);
       toast.success(`${title} account created`, {
         description: `Your ${title} account has been created successfully.`,
       });
       setInputValue("");
-    } catch (error) {
+    } catch (err) {
+      Sentry.captureException(err);
       toast.error(`Failed to create ${title.toLowerCase()} account`, {
         description:
-          error instanceof Error
-            ? error.message
+          err instanceof Error
+            ? err.message
             : "An error occurred while creating your account.",
       });
     }
   };

89-107: Missing Sentry error capture in delete handler.

Same issue as handleCreate - the error should be captured with Sentry.captureException(error) for observability.

🔧 Proposed fix
   const handleDelete = async () => {
     if (!account) {
       return;
     }

     try {
       await deleteMutation.mutateAsync(account.id);
       toast.success(`${title} account deleted`, {
         description: `Your ${title} account has been deleted successfully.`,
       });
-    } catch (error) {
+    } catch (err) {
+      Sentry.captureException(err);
       toast.error(`Failed to delete ${title.toLowerCase()} account`, {
         description:
-          error instanceof Error
-            ? error.message
+          err instanceof Error
+            ? err.message
             : "An error occurred while deleting your account.",
       });
     }
   };
src/lib/api/query-keys.ts (1)

70-94: Query key factory follows established patterns with minor inconsistency.

The new integrations query keys follow the factory pattern. However, integrations.list() doesn't accept optional filters unlike other list() methods (e.g., users.list(filters?), sessions.list(filters?)). Consider adding optional filters for consistency if filtering will be needed in the future.

♻️ Optional: Add filters parameter for consistency
   integrations: {
     all: ["integrations"] as const,
     lists: () => [...queryKeys.integrations.all, "list"] as const,
-    list: () => [...queryKeys.integrations.lists()] as const,
+    list: (filters?: { enabled?: boolean }) =>
+      [...queryKeys.integrations.lists(), { filters }] as const,
src/app/api/integrations/[integration]/accounts/[id]/route.ts (1)

14-57: Consider extracting shared validation logic.

The authorization and integration lookup logic (register → get integration → check enabled → check capability → get account → check ownership) is duplicated across GET, PATCH, and DELETE handlers. Consider extracting a helper function.

♻️ Example helper extraction
async function getAuthorizedAccount(
  request: NextRequest,
  integrationId: string,
  accountId: string
) {
  const { userId } = await requireAuth(request);
  
  registerIntegrations();
  const integration = getIntegrationRegistry().get(integrationId);
  
  if (!integration) {
    throw new APIError("Unknown integration", 404);
  }
  if (!integration.enabled) {
    throw new APIError("Integration is disabled", 403);
  }
  if (!integration.getAccountById) {
    throw new APIError("Integration does not support account lookup", 400);
  }
  
  const account = await integration.getAccountById(accountId);
  if (!account) {
    throw new APIError("Integration account not found", 404);
  }
  
  const isAdminUser = await isAdmin(userId);
  if (account.userId !== userId && !isAdminUser) {
    throw new APIError("Forbidden - Access denied", 403);
  }
  
  return { userId, integration, account, isAdminUser };
}
src/lib/integrations/xmpp/implementation.ts (1)

262-274: Consider validating status values.

The updateAccount method accepts input.status without validating it's a valid status value. If there's a defined set of allowed statuses (e.g., "active", "suspended", "deleted"), consider validating before applying the update.

♻️ Optional: Add status validation
+const VALID_STATUSES = ["active", "suspended"] as const;
+
     const updates: Partial<typeof xmppAccount.$inferInsert> = {};

     if (input.status && input.status !== account.status) {
+      if (!VALID_STATUSES.includes(input.status as typeof VALID_STATUSES[number])) {
+        throw new Error(`Invalid status. Allowed values: ${VALID_STATUSES.join(", ")}`);
+      }
       updates.status = input.status;
     }
src/hooks/use-integration.ts (2)

53-68: Consider invalidating the all accounts key for broader cache consistency.

When creating an account, invalidating accounts.current is correct for the current user's view. However, if any list views use accounts.all, they won't refresh automatically.

♻️ Optional: Broader cache invalidation
     onSuccess: () => {
       queryClient.invalidateQueries({
         queryKey: queryKeys.integrations.accounts.current(integrationId),
       });
+      // Also invalidate the parent key to refresh any list views
+      queryClient.invalidateQueries({
+        queryKey: queryKeys.integrations.accounts.all(integrationId),
+      });
     },

96-110: Consider clearing the detail cache entry for the deleted account.

When deleting an account, invalidating accounts.all is good, but the specific detail entry remains cached until it naturally expires. This could cause stale data issues if the ID is reused or queried directly.

♻️ Optional: Remove deleted account from cache
-export function useDeleteIntegrationAccount(integrationId: string) {
+export function useDeleteIntegrationAccount(integrationId: string) {
   const queryClient = useQueryClient();

   return useMutation({
     mutationFn: (id: string) => deleteIntegrationAccount(integrationId, id),
-    onSuccess: () => {
+    onSuccess: (_data, id) => {
+      // Remove the specific detail entry
+      queryClient.removeQueries({
+        queryKey: queryKeys.integrations.accounts.detail(integrationId, id),
+      });
       queryClient.invalidateQueries({
         queryKey: queryKeys.integrations.accounts.all(integrationId),
       });
     },
   });
 }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 20919af and d16d498.

📒 Files selected for processing (40)
  • drizzle/20260107000000_integration_accounts/migration.sql
  • locale/en/routes.json
  • next.config.ts
  • src/app/(dashboard)/app/integrations/integrations-content.tsx
  • src/app/(dashboard)/app/integrations/page.tsx
  • src/app/api/admin/users/[id]/route.ts
  • src/app/api/integrations/[integration]/accounts/[id]/route.ts
  • src/app/api/integrations/[integration]/accounts/route.ts
  • src/app/api/integrations/route.ts
  • src/app/api/xmpp/accounts/[id]/route.ts
  • src/app/api/xmpp/accounts/route.ts
  • src/components/integrations/integration-card.tsx
  • src/components/integrations/integration-management.tsx
  • src/components/xmpp/xmpp-account-management.tsx
  • src/env.ts
  • src/hooks/index.ts
  • src/hooks/use-integration.ts
  • src/hooks/use-xmpp-account.ts
  • src/lib/api/index.ts
  • src/lib/api/integrations.ts
  • src/lib/api/query-keys.ts
  • src/lib/api/xmpp.ts
  • src/lib/auth/config.ts
  • src/lib/db/schema/index.ts
  • src/lib/db/schema/integrations/base.ts
  • src/lib/integrations/core/base.ts
  • src/lib/integrations/core/constants.ts
  • src/lib/integrations/core/factory.ts
  • src/lib/integrations/core/registry.ts
  • src/lib/integrations/core/types.ts
  • src/lib/integrations/core/user-deletion.ts
  • src/lib/integrations/index.ts
  • src/lib/integrations/xmpp/client.ts
  • src/lib/integrations/xmpp/config.ts
  • src/lib/integrations/xmpp/implementation.ts
  • src/lib/integrations/xmpp/index.ts
  • src/lib/integrations/xmpp/keys.ts
  • src/lib/integrations/xmpp/types.ts
  • src/lib/integrations/xmpp/utils.ts
  • src/lib/routes/config.ts
🧰 Additional context used
📓 Path-based instructions (13)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/sentry.mdc)

**/*.{js,jsx,ts,tsx}: Use Sentry.captureException(error) to capture exceptions and log errors in Sentry, particularly in try-catch blocks or areas where exceptions are expected
Use Sentry.startSpan() function to create spans for meaningful actions within applications like button clicks, API calls, and function calls

Files:

  • next.config.ts
  • src/lib/integrations/core/user-deletion.ts
  • src/lib/integrations/core/factory.ts
  • src/components/xmpp/xmpp-account-management.tsx
  • src/lib/api/index.ts
  • src/lib/integrations/xmpp/types.ts
  • src/app/api/integrations/[integration]/accounts/route.ts
  • src/app/api/admin/users/[id]/route.ts
  • src/components/integrations/integration-card.tsx
  • src/lib/routes/config.ts
  • src/app/(dashboard)/app/integrations/page.tsx
  • src/lib/integrations/core/base.ts
  • src/lib/db/schema/integrations/base.ts
  • src/app/api/integrations/[integration]/accounts/[id]/route.ts
  • src/hooks/use-integration.ts
  • src/lib/integrations/core/constants.ts
  • src/app/api/xmpp/accounts/[id]/route.ts
  • src/lib/integrations/core/registry.ts
  • src/hooks/index.ts
  • src/lib/integrations/xmpp/index.ts
  • src/components/integrations/integration-management.tsx
  • src/hooks/use-xmpp-account.ts
  • src/env.ts
  • src/lib/integrations/xmpp/client.ts
  • src/app/api/integrations/route.ts
  • src/lib/api/query-keys.ts
  • src/lib/db/schema/index.ts
  • src/lib/integrations/core/types.ts
  • src/lib/integrations/xmpp/implementation.ts
  • src/lib/auth/config.ts
  • src/lib/integrations/index.ts
  • src/app/(dashboard)/app/integrations/integrations-content.tsx
  • src/lib/integrations/xmpp/config.ts
  • src/app/api/xmpp/accounts/route.ts
  • src/lib/api/xmpp.ts
  • src/lib/api/integrations.ts
**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/sentry.mdc)

When creating custom spans for API calls with Sentry.startSpan(), ensure the name and op properties are meaningful (e.g., op: "http.client" with descriptive names like GET /api/users/${userId}), and attach attributes based on relevant request information and metrics

Files:

  • next.config.ts
  • src/lib/integrations/core/user-deletion.ts
  • src/lib/integrations/core/factory.ts
  • src/lib/api/index.ts
  • src/lib/integrations/xmpp/types.ts
  • src/app/api/integrations/[integration]/accounts/route.ts
  • src/app/api/admin/users/[id]/route.ts
  • src/lib/routes/config.ts
  • src/lib/integrations/core/base.ts
  • src/lib/db/schema/integrations/base.ts
  • src/app/api/integrations/[integration]/accounts/[id]/route.ts
  • src/hooks/use-integration.ts
  • src/lib/integrations/core/constants.ts
  • src/app/api/xmpp/accounts/[id]/route.ts
  • src/lib/integrations/core/registry.ts
  • src/hooks/index.ts
  • src/lib/integrations/xmpp/index.ts
  • src/hooks/use-xmpp-account.ts
  • src/env.ts
  • src/lib/integrations/xmpp/client.ts
  • src/app/api/integrations/route.ts
  • src/lib/api/query-keys.ts
  • src/lib/db/schema/index.ts
  • src/lib/integrations/core/types.ts
  • src/lib/integrations/xmpp/implementation.ts
  • src/lib/auth/config.ts
  • src/lib/integrations/index.ts
  • src/lib/integrations/xmpp/config.ts
  • src/app/api/xmpp/accounts/route.ts
  • src/lib/api/xmpp.ts
  • src/lib/api/integrations.ts
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (.cursor/rules/sentry.mdc)

**/*.{js,ts,jsx,tsx}: Import Sentry using import * as Sentry from "@sentry/nextjs" when using logs in NextJS projects
Reference the Sentry logger using const { logger } = Sentry when using logging functionality
Use logger.fmt as a template literal function to bring variables into structured logs, providing better log formatting and variable interpolation

Files:

  • next.config.ts
  • src/lib/integrations/core/user-deletion.ts
  • src/lib/integrations/core/factory.ts
  • src/components/xmpp/xmpp-account-management.tsx
  • src/lib/api/index.ts
  • src/lib/integrations/xmpp/types.ts
  • src/app/api/integrations/[integration]/accounts/route.ts
  • src/app/api/admin/users/[id]/route.ts
  • src/components/integrations/integration-card.tsx
  • src/lib/routes/config.ts
  • src/app/(dashboard)/app/integrations/page.tsx
  • src/lib/integrations/core/base.ts
  • src/lib/db/schema/integrations/base.ts
  • src/app/api/integrations/[integration]/accounts/[id]/route.ts
  • src/hooks/use-integration.ts
  • src/lib/integrations/core/constants.ts
  • src/app/api/xmpp/accounts/[id]/route.ts
  • src/lib/integrations/core/registry.ts
  • src/hooks/index.ts
  • src/lib/integrations/xmpp/index.ts
  • src/components/integrations/integration-management.tsx
  • src/hooks/use-xmpp-account.ts
  • src/env.ts
  • src/lib/integrations/xmpp/client.ts
  • src/app/api/integrations/route.ts
  • src/lib/api/query-keys.ts
  • src/lib/db/schema/index.ts
  • src/lib/integrations/core/types.ts
  • src/lib/integrations/xmpp/implementation.ts
  • src/lib/auth/config.ts
  • src/lib/integrations/index.ts
  • src/app/(dashboard)/app/integrations/integrations-content.tsx
  • src/lib/integrations/xmpp/config.ts
  • src/app/api/xmpp/accounts/route.ts
  • src/lib/api/xmpp.ts
  • src/lib/api/integrations.ts
**/*.{css,ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/tailwind-v4.mdc)

Use container query support with @container, @sm:, @md: for container-based breakpoints and @max-md: for max-width queries

Files:

  • next.config.ts
  • src/lib/integrations/core/user-deletion.ts
  • src/lib/integrations/core/factory.ts
  • src/components/xmpp/xmpp-account-management.tsx
  • src/lib/api/index.ts
  • src/lib/integrations/xmpp/types.ts
  • src/app/api/integrations/[integration]/accounts/route.ts
  • src/app/api/admin/users/[id]/route.ts
  • src/components/integrations/integration-card.tsx
  • src/lib/routes/config.ts
  • src/app/(dashboard)/app/integrations/page.tsx
  • src/lib/integrations/core/base.ts
  • src/lib/db/schema/integrations/base.ts
  • src/app/api/integrations/[integration]/accounts/[id]/route.ts
  • src/hooks/use-integration.ts
  • src/lib/integrations/core/constants.ts
  • src/app/api/xmpp/accounts/[id]/route.ts
  • src/lib/integrations/core/registry.ts
  • src/hooks/index.ts
  • src/lib/integrations/xmpp/index.ts
  • src/components/integrations/integration-management.tsx
  • src/hooks/use-xmpp-account.ts
  • src/env.ts
  • src/lib/integrations/xmpp/client.ts
  • src/app/api/integrations/route.ts
  • src/lib/api/query-keys.ts
  • src/lib/db/schema/index.ts
  • src/lib/integrations/core/types.ts
  • src/lib/integrations/xmpp/implementation.ts
  • src/lib/auth/config.ts
  • src/lib/integrations/index.ts
  • src/app/(dashboard)/app/integrations/integrations-content.tsx
  • src/lib/integrations/xmpp/config.ts
  • src/app/api/xmpp/accounts/route.ts
  • src/lib/api/xmpp.ts
  • src/lib/api/integrations.ts
**/*.{ts,tsx,js,jsx,html}

📄 CodeRabbit inference engine (.cursor/rules/tailwind-v4.mdc)

**/*.{ts,tsx,js,jsx,html}: Use 3D transform utilities: transform-3d, rotate-x-*, rotate-y-*, rotate-z-*, scale-z-*, translate-z-*, perspective-*, and perspective-origin-*
Use linear gradient angles with bg-linear-45 syntax and gradient interpolation like bg-linear-to-r/oklch or bg-linear-to-r/srgb
Use conic and radial gradients with bg-conic and bg-radial-[at_25%_25%] utilities
Use inset-shadow-* and inset-ring-* utilities instead of deprecated shadow opacity utilities
Use field-sizing-content utility for auto-resizing textareas
Use scheme-light and scheme-dark utilities for color-scheme property
Use font-stretch-* utilities for variable font configuration
Chain variants together for composable variants (e.g., group-has-data-potato:opacity-100)
Use starting variant for @starting-style transitions
Use not-* variant for :not() pseudo-class (e.g., not-first:mb-4)
Use inert variant for styling elements with the inert attribute
Use nth-* variants: nth-3:, nth-last-5:, nth-of-type-4:, nth-last-of-type-6: for targeting specific elements
Use in-* variant as a simpler alternative to group-* without adding group class
Use open variant to support :popover-open pseudo-class
Use ** variant for targeting all descendants
Replace deprecated bg-opacity-* utilities with color values using slash notation (e.g., bg-black/50)
Replace deprecated text-opacity-* utilities with color values using slash notation (e.g., text-black/50)
Replace deprecated border-opacity-*, divide-opacity-* and similar opacity utilities with color slash notation
Use shadow-xs instead of shadow-sm and shadow-sm instead of shadow
Use drop-shadow-xs instead of drop-shadow-sm and drop-shadow-sm instead of drop-shadow
Use blur-xs instead of blur-sm and blur-sm instead of blur
Use rounded-xs instead of rounded-sm and rounded-sm instead of rounded
Use outline-hidden instead of outline-none for...

Files:

  • next.config.ts
  • src/lib/integrations/core/user-deletion.ts
  • src/lib/integrations/core/factory.ts
  • src/components/xmpp/xmpp-account-management.tsx
  • src/lib/api/index.ts
  • src/lib/integrations/xmpp/types.ts
  • src/app/api/integrations/[integration]/accounts/route.ts
  • src/app/api/admin/users/[id]/route.ts
  • src/components/integrations/integration-card.tsx
  • src/lib/routes/config.ts
  • src/app/(dashboard)/app/integrations/page.tsx
  • src/lib/integrations/core/base.ts
  • src/lib/db/schema/integrations/base.ts
  • src/app/api/integrations/[integration]/accounts/[id]/route.ts
  • src/hooks/use-integration.ts
  • src/lib/integrations/core/constants.ts
  • src/app/api/xmpp/accounts/[id]/route.ts
  • src/lib/integrations/core/registry.ts
  • src/hooks/index.ts
  • src/lib/integrations/xmpp/index.ts
  • src/components/integrations/integration-management.tsx
  • src/hooks/use-xmpp-account.ts
  • src/env.ts
  • src/lib/integrations/xmpp/client.ts
  • src/app/api/integrations/route.ts
  • src/lib/api/query-keys.ts
  • src/lib/db/schema/index.ts
  • src/lib/integrations/core/types.ts
  • src/lib/integrations/xmpp/implementation.ts
  • src/lib/auth/config.ts
  • src/lib/integrations/index.ts
  • src/app/(dashboard)/app/integrations/integrations-content.tsx
  • src/lib/integrations/xmpp/config.ts
  • src/app/api/xmpp/accounts/route.ts
  • src/lib/api/xmpp.ts
  • src/lib/api/integrations.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/tanstack-query.mdc)

**/*.{ts,tsx}: Create QueryClient using getQueryClient() function that automatically handles server/client isolation (new instance per server request, singleton on client)
Always use the centralized query key factory from src/lib/api/query-keys.ts instead of creating keys manually
Include all variables used in queryFn in the query key to ensure proper cache management and query invalidation

**/*.{ts,tsx}: Use explicit types for function parameters and return values when they enhance clarity
Prefer unknown over any when the type is genuinely unknown
Use const assertions (as const) for immutable values and literal types
Leverage TypeScript's type narrowing instead of type assertions

**/*.{ts,tsx}: TypeScript strict mode enabled
Prefer composition over inheritance
Use selective imports over barrel exports for performance
Write accessible, performant, type-safe code
Use explicit types for clarity, prefer unknown over any
Always await promises in async functions
Use @/auth module for all authentication operations
Use @/db for all database operations
Use BetterAuth for all authentication
Validate all inputs with Zod schemas
Implement proper RBAC with permissions module
Use TypeScript paths for imports (configured in tsconfig.json)
Use TanStack Query for all server state management
Clear separation between client/server code following module boundaries

Files:

  • next.config.ts
  • src/lib/integrations/core/user-deletion.ts
  • src/lib/integrations/core/factory.ts
  • src/components/xmpp/xmpp-account-management.tsx
  • src/lib/api/index.ts
  • src/lib/integrations/xmpp/types.ts
  • src/app/api/integrations/[integration]/accounts/route.ts
  • src/app/api/admin/users/[id]/route.ts
  • src/components/integrations/integration-card.tsx
  • src/lib/routes/config.ts
  • src/app/(dashboard)/app/integrations/page.tsx
  • src/lib/integrations/core/base.ts
  • src/lib/db/schema/integrations/base.ts
  • src/app/api/integrations/[integration]/accounts/[id]/route.ts
  • src/hooks/use-integration.ts
  • src/lib/integrations/core/constants.ts
  • src/app/api/xmpp/accounts/[id]/route.ts
  • src/lib/integrations/core/registry.ts
  • src/hooks/index.ts
  • src/lib/integrations/xmpp/index.ts
  • src/components/integrations/integration-management.tsx
  • src/hooks/use-xmpp-account.ts
  • src/env.ts
  • src/lib/integrations/xmpp/client.ts
  • src/app/api/integrations/route.ts
  • src/lib/api/query-keys.ts
  • src/lib/db/schema/index.ts
  • src/lib/integrations/core/types.ts
  • src/lib/integrations/xmpp/implementation.ts
  • src/lib/auth/config.ts
  • src/lib/integrations/index.ts
  • src/app/(dashboard)/app/integrations/integrations-content.tsx
  • src/lib/integrations/xmpp/config.ts
  • src/app/api/xmpp/accounts/route.ts
  • src/lib/api/xmpp.ts
  • src/lib/api/integrations.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

**/*.{ts,tsx,js,jsx}: Use meaningful variable names instead of magic numbers - extract constants with descriptive names
Use arrow functions for callbacks and short functions
Prefer for...of loops over .forEach() and indexed for loops
Use optional chaining (?.) and nullish coalescing (??) for safer property access
Prefer template literals over string concatenation
Use destructuring for object and array assignments
Use const by default, let only when reassignment is needed, never var
Always await promises in async functions - don't forget to use the return value
Use async/await syntax instead of promise chains for better readability
Handle errors appropriately in async code with try-catch blocks
Don't use async functions as Promise executors
Use function components over class components
Call hooks at the top level only, never conditionally
Specify all dependencies in hook dependency arrays correctly
Use the key prop for elements in iterables (prefer unique IDs over array indices)
Nest children between opening and closing tags instead of passing as props in React components
Don't define components inside other components
Use semantic HTML and ARIA attributes for accessibility: provide meaningful alt text for images, use proper heading hierarchy, add labels for form inputs, include keyboard event handlers alongside mouse events, and use semantic elements instead of divs with roles
Remove console.log, debugger, and alert statements from production code
Throw Error objects with descriptive messages, not strings or other values
Use try-catch blocks meaningfully - don't catch errors just to rethrow them
Prefer early returns over nested conditionals for error cases
Keep functions focused and under reasonable cognitive complexity limits
Extract complex conditions into well-named boolean variables
Use early returns to reduce nesting
Prefer simple conditionals over nested ternary operators
Group related code together and separate concerns
Add `...

Files:

  • next.config.ts
  • src/lib/integrations/core/user-deletion.ts
  • src/lib/integrations/core/factory.ts
  • src/components/xmpp/xmpp-account-management.tsx
  • src/lib/api/index.ts
  • src/lib/integrations/xmpp/types.ts
  • src/app/api/integrations/[integration]/accounts/route.ts
  • src/app/api/admin/users/[id]/route.ts
  • src/components/integrations/integration-card.tsx
  • src/lib/routes/config.ts
  • src/app/(dashboard)/app/integrations/page.tsx
  • src/lib/integrations/core/base.ts
  • src/lib/db/schema/integrations/base.ts
  • src/app/api/integrations/[integration]/accounts/[id]/route.ts
  • src/hooks/use-integration.ts
  • src/lib/integrations/core/constants.ts
  • src/app/api/xmpp/accounts/[id]/route.ts
  • src/lib/integrations/core/registry.ts
  • src/hooks/index.ts
  • src/lib/integrations/xmpp/index.ts
  • src/components/integrations/integration-management.tsx
  • src/hooks/use-xmpp-account.ts
  • src/env.ts
  • src/lib/integrations/xmpp/client.ts
  • src/app/api/integrations/route.ts
  • src/lib/api/query-keys.ts
  • src/lib/db/schema/index.ts
  • src/lib/integrations/core/types.ts
  • src/lib/integrations/xmpp/implementation.ts
  • src/lib/auth/config.ts
  • src/lib/integrations/index.ts
  • src/app/(dashboard)/app/integrations/integrations-content.tsx
  • src/lib/integrations/xmpp/config.ts
  • src/app/api/xmpp/accounts/route.ts
  • src/lib/api/xmpp.ts
  • src/lib/api/integrations.ts
src/lib/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Modules should import and use their own keys() function, not direct process.env access

Files:

  • src/lib/integrations/core/user-deletion.ts
  • src/lib/integrations/core/factory.ts
  • src/lib/api/index.ts
  • src/lib/integrations/xmpp/types.ts
  • src/lib/routes/config.ts
  • src/lib/integrations/core/base.ts
  • src/lib/db/schema/integrations/base.ts
  • src/lib/integrations/core/constants.ts
  • src/lib/integrations/core/registry.ts
  • src/lib/integrations/xmpp/index.ts
  • src/lib/integrations/xmpp/client.ts
  • src/lib/api/query-keys.ts
  • src/lib/db/schema/index.ts
  • src/lib/integrations/core/types.ts
  • src/lib/integrations/xmpp/implementation.ts
  • src/lib/auth/config.ts
  • src/lib/integrations/index.ts
  • src/lib/integrations/xmpp/config.ts
  • src/lib/api/xmpp.ts
  • src/lib/api/integrations.ts
**/*.{jsx,tsx}

📄 CodeRabbit inference engine (.cursor/rules/sentry.mdc)

When creating custom spans for component actions with Sentry.startSpan(), ensure the name and op properties are meaningful for the activities in the call, and attach attributes based on relevant information and metrics

Files:

  • src/components/xmpp/xmpp-account-management.tsx
  • src/components/integrations/integration-card.tsx
  • src/app/(dashboard)/app/integrations/page.tsx
  • src/components/integrations/integration-management.tsx
  • src/app/(dashboard)/app/integrations/integrations-content.tsx
{src/app/**,src/components/**}/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/tanstack-query.mdc)

{src/app/**,src/components/**}/*.{ts,tsx}: Use useUsers(), useUser(), useSessions(), useAdminStats(), useUpdateUser(), useDeleteUser(), useDeleteSession() hooks from src/hooks/use-admin.ts for standard queries
Use corresponding Suspense hooks (useUsersSuspense(), useUserSuspense(), etc.) from src/hooks/use-admin-suspense.ts when wrapping components in Suspense and Error Boundaries

Files:

  • src/components/xmpp/xmpp-account-management.tsx
  • src/app/api/integrations/[integration]/accounts/route.ts
  • src/app/api/admin/users/[id]/route.ts
  • src/components/integrations/integration-card.tsx
  • src/app/(dashboard)/app/integrations/page.tsx
  • src/app/api/integrations/[integration]/accounts/[id]/route.ts
  • src/app/api/xmpp/accounts/[id]/route.ts
  • src/components/integrations/integration-management.tsx
  • src/app/api/integrations/route.ts
  • src/app/(dashboard)/app/integrations/integrations-content.tsx
  • src/app/api/xmpp/accounts/route.ts
**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

**/*.tsx: Use functional components and hooks
Use semantic HTML and ARIA attributes
Use shadcn/ui components from @/components/ui/*
Implement proper error boundaries
Use Suspense for loading states
Optimize images with next/image

Files:

  • src/components/xmpp/xmpp-account-management.tsx
  • src/components/integrations/integration-card.tsx
  • src/app/(dashboard)/app/integrations/page.tsx
  • src/components/integrations/integration-management.tsx
  • src/app/(dashboard)/app/integrations/integrations-content.tsx
locale/**/*

📄 CodeRabbit inference engine (AGENTS.md)

Internationalization handled via next-intl with locale files in locale/ directory

Files:

  • locale/en/routes.json
src/lib/api/query-keys.ts

📄 CodeRabbit inference engine (.cursor/rules/tanstack-query.mdc)

src/lib/api/query-keys.ts: Query keys must maintain consistent array order (array order matters) and use serializable values only (no functions or non-serializable objects)
All query keys must follow the factory pattern with structure: resource.all, resource.lists(), resource.list(filters?), resource.detail(id), resource.details()

Files:

  • src/lib/api/query-keys.ts
🧠 Learnings (38)
📚 Learning: 2026-01-15T06:15:18.973Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/sentry.mdc:0-0
Timestamp: 2026-01-15T06:15:18.973Z
Learning: Applies to {instrumentation-client.{js,ts},sentry.server.config.{js,ts},sentry.edge.config.{js,ts}} : In NextJS projects, perform client-side Sentry initialization in `instrumentation-client.(js|ts)`, server initialization in `sentry.server.config.ts`, and edge initialization in `sentry.edge.config.ts` - do not repeat initialization in other files

Applied to files:

  • next.config.ts
📚 Learning: 2026-01-15T06:15:18.973Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/sentry.mdc:0-0
Timestamp: 2026-01-15T06:15:18.973Z
Learning: Applies to **/*.{js,ts,jsx,tsx} : Import Sentry using `import * as Sentry from "sentry/nextjs"` when using logs in NextJS projects

Applied to files:

  • next.config.ts
📚 Learning: 2025-12-18T18:18:05.202Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/anti-slop.mdc:0-0
Timestamp: 2025-12-18T18:18:05.202Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use Server Components by default; `use client` only when essential in frontend

Applied to files:

  • next.config.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to {src/app/**,src/components/**}/*.{ts,tsx} : Use useUsers(), useUser(), useSessions(), useAdminStats(), useUpdateUser(), useDeleteUser(), useDeleteSession() hooks from src/hooks/use-admin.ts for standard queries

Applied to files:

  • src/components/xmpp/xmpp-account-management.tsx
  • src/app/api/admin/users/[id]/route.ts
  • src/hooks/use-integration.ts
  • src/hooks/index.ts
  • src/hooks/use-xmpp-account.ts
  • src/lib/api/query-keys.ts
  • src/lib/db/schema/index.ts
  • src/lib/auth/config.ts
📚 Learning: 2026-01-16T06:31:42.976Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T06:31:42.976Z
Learning: Applies to **/*.{ts,tsx} : Use selective imports over barrel exports for performance

Applied to files:

  • src/lib/api/index.ts
  • src/hooks/index.ts
  • src/lib/integrations/xmpp/index.ts
📚 Learning: 2026-01-15T06:16:30.014Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/ultracite.mdc:0-0
Timestamp: 2026-01-15T06:16:30.014Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Avoid barrel files (index files that re-export everything)

Applied to files:

  • src/lib/api/index.ts
  • src/hooks/index.ts
  • src/lib/integrations/xmpp/index.ts
📚 Learning: 2025-12-18T18:18:05.202Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/anti-slop.mdc:0-0
Timestamp: 2025-12-18T18:18:05.202Z
Learning: Applies to **/*route*.{ts,tsx,js,jsx} : Clean, non-duplicated REST routes in backend code

Applied to files:

  • src/app/api/admin/users/[id]/route.ts
  • src/lib/routes/config.ts
  • src/app/api/integrations/[integration]/accounts/[id]/route.ts
  • src/app/api/integrations/route.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to src/lib/api/admin.ts : All API query functions must throw errors (not return undefined) and check response.ok for fetch() calls with proper error handling

Applied to files:

  • src/app/api/admin/users/[id]/route.ts
📚 Learning: 2026-01-16T06:31:42.976Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T06:31:42.976Z
Learning: Applies to **/*.tsx : Use functional components and hooks

Applied to files:

  • src/components/integrations/integration-card.tsx
  • src/hooks/index.ts
📚 Learning: 2026-01-15T06:16:30.014Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/ultracite.mdc:0-0
Timestamp: 2026-01-15T06:16:30.014Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Nest children between opening and closing tags instead of passing as props in React components

Applied to files:

  • src/components/integrations/integration-card.tsx
📚 Learning: 2026-01-16T06:31:42.976Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T06:31:42.976Z
Learning: Applies to app/**/*.{ts,tsx} : Follow Next.js App Router conventions

Applied to files:

  • src/lib/routes/config.ts
  • src/app/api/integrations/route.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to src/app/**/*.{tsx} : Use HydrationBoundary component from TanStack Query to transfer server-prefetched data to client cache in Server Components

Applied to files:

  • src/app/(dashboard)/app/integrations/page.tsx
  • src/hooks/use-integration.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to src/app/**/*.{tsx} : In Server Components, create a QueryClient per request using getServerQueryClient(), prefetch data in parallel with Promise.all(), and dehydrate using dehydrate(queryClient)

Applied to files:

  • src/app/(dashboard)/app/integrations/page.tsx
  • src/hooks/use-integration.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to {src/app/**,src/components/**}/*.{tsx} : Use useSuspenseQueries for parallel queries to avoid waterfalls and improve performance in Server Components and Client Components

Applied to files:

  • src/app/(dashboard)/app/integrations/page.tsx
  • src/hooks/use-integration.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to src/app/**/*.{tsx} : Perform auth checks using Better Auth's auth.api.getSession() in Server Components before prefetching data, and redirect if not authenticated

Applied to files:

  • src/app/(dashboard)/app/integrations/page.tsx
📚 Learning: 2025-12-18T18:18:05.202Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/anti-slop.mdc:0-0
Timestamp: 2025-12-18T18:18:05.202Z
Learning: Applies to **/api/**/*.{ts,tsx,js,jsx} : API, DB, auth contracts, or tokens require mandatory review

Applied to files:

  • src/app/api/integrations/[integration]/accounts/[id]/route.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to src/hooks/use-admin.ts : Use queryClient.setQueryData() to update specific cache entries and queryClient.invalidateQueries() to invalidate related queries in mutation onSuccess handlers

Applied to files:

  • src/hooks/use-integration.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to {src/app/**,src/components/**}/*.{tsx} : In regular query hooks, check isPending first, then isError, then use data to ensure TypeScript type narrowing works correctly

Applied to files:

  • src/hooks/use-integration.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to **/*.{ts,tsx} : Create QueryClient using getQueryClient() function that automatically handles server/client isolation (new instance per server request, singleton on client)

Applied to files:

  • src/hooks/use-integration.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Add new resources by creating query keys, types, client API functions, server query functions, regular hooks, and Suspense hooks following the established pattern

Applied to files:

  • src/hooks/use-integration.ts
  • src/lib/api/query-keys.ts
📚 Learning: 2026-01-16T06:31:42.976Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T06:31:42.976Z
Learning: Applies to **/*.{ts,tsx} : Use TanStack Query for all server state management

Applied to files:

  • src/hooks/use-integration.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to src/app/**/*.{tsx} : Use fetchQuery for critical queries that must succeed (throws errors for 404/500), and prefetchQuery for optional data that can gracefully degrade

Applied to files:

  • src/hooks/use-integration.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to src/app/providers.tsx : Include ReactQueryDevtools in src/app/providers.tsx with NODE_ENV === 'development' check for development-only access

Applied to files:

  • src/hooks/use-integration.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to {src/app/**,src/components/**}/*.{ts,tsx} : Use corresponding Suspense hooks (useUsersSuspense(), useUserSuspense(), etc.) from src/hooks/use-admin-suspense.ts when wrapping components in Suspense and Error Boundaries

Applied to files:

  • src/hooks/index.ts
📚 Learning: 2026-01-16T06:31:42.976Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T06:31:42.976Z
Learning: Applies to **/*.{ts,tsx} : Use `@/auth` module for all authentication operations

Applied to files:

  • src/hooks/index.ts
  • src/hooks/use-xmpp-account.ts
  • src/lib/db/schema/index.ts
  • src/app/api/xmpp/accounts/route.ts
📚 Learning: 2026-01-16T06:31:42.976Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T06:31:42.976Z
Learning: Applies to **/*.{ts,tsx} : Use TypeScript paths for imports (configured in tsconfig.json)

Applied to files:

  • src/hooks/use-xmpp-account.ts
📚 Learning: 2026-01-16T06:31:42.976Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T06:31:42.976Z
Learning: Applies to src/lib/**/*.ts : Modules should import and use their own `keys()` function, not direct `process.env` access

Applied to files:

  • src/env.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to **/*.{ts,tsx} : Always use the centralized query key factory from src/lib/api/query-keys.ts instead of creating keys manually

Applied to files:

  • src/env.ts
  • src/lib/api/query-keys.ts
📚 Learning: 2026-01-16T06:31:42.976Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T06:31:42.976Z
Learning: Applies to src/lib/*/keys.ts : Each lib module should export a `keys()` function that defines and validates its environment variables using Zod schemas

Applied to files:

  • src/env.ts
📚 Learning: 2026-01-16T06:31:42.976Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T06:31:42.976Z
Learning: Applies to app/**/*.tsx : Never expose API keys in client code

Applied to files:

  • src/env.ts
📚 Learning: 2026-01-15T06:16:30.014Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/ultracite.mdc:0-0
Timestamp: 2026-01-15T06:16:30.014Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use `next/head` or App Router metadata API for head elements

Applied to files:

  • src/app/api/integrations/route.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to src/lib/api/query-keys.ts : All query keys must follow the factory pattern with structure: resource.all, resource.lists(), resource.list(filters?), resource.detail(id), resource.details()

Applied to files:

  • src/lib/api/query-keys.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to src/lib/api/query-keys.ts : Query keys must maintain consistent array order (array order matters) and use serializable values only (no functions or non-serializable objects)

Applied to files:

  • src/lib/api/query-keys.ts
📚 Learning: 2026-01-16T06:31:42.976Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T06:31:42.976Z
Learning: Applies to **/*.{ts,tsx} : Use BetterAuth for all authentication

Applied to files:

  • src/lib/db/schema/index.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to src/lib/api/types.ts : TypeScript types for resources must be derived from database schema and include type interfaces for list filters (e.g., UserListFilters, PostListFilters)

Applied to files:

  • src/lib/integrations/core/types.ts
📚 Learning: 2026-01-15T06:16:30.014Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/ultracite.mdc:0-0
Timestamp: 2026-01-15T06:16:30.014Z
Learning: Applies to **/*.{ts,tsx} : Use explicit types for function parameters and return values when they enhance clarity

Applied to files:

  • src/lib/integrations/core/types.ts
📚 Learning: 2025-12-18T18:18:05.202Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/anti-slop.mdc:0-0
Timestamp: 2025-12-18T18:18:05.202Z
Learning: Applies to **/config*.{ts,tsx,js,jsx} : ENV must be validated at startup in backend

Applied to files:

  • src/lib/integrations/xmpp/config.ts
📚 Learning: 2026-01-16T06:31:42.976Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T06:31:42.976Z
Learning: Applies to **/*.{ts,tsx} : Clear separation between client/server code following module boundaries

Applied to files:

  • src/app/api/xmpp/accounts/route.ts
🧬 Code graph analysis (16)
src/lib/integrations/core/user-deletion.ts (3)
src/lib/integrations/index.ts (1)
  • registerIntegrations (10-17)
src/lib/integrations/core/registry.ts (1)
  • getIntegrationRegistry (66-68)
src/lib/db/schema/auth.ts (1)
  • account (48-70)
src/app/api/integrations/[integration]/accounts/route.ts (4)
src/app/api/integrations/route.ts (1)
  • dynamic (7-7)
src/lib/api/utils.ts (3)
  • requireAuth (68-81)
  • APIError (86-96)
  • handleAPIError (102-124)
src/lib/integrations/index.ts (1)
  • registerIntegrations (10-17)
src/lib/integrations/core/registry.ts (1)
  • getIntegrationRegistry (66-68)
src/app/api/admin/users/[id]/route.ts (1)
src/lib/integrations/core/user-deletion.ts (1)
  • cleanupIntegrationAccounts (11-36)
src/components/integrations/integration-card.tsx (1)
src/components/ui/card.tsx (3)
  • Card (85-85)
  • CardHeader (86-86)
  • CardContent (91-91)
src/lib/routes/config.ts (1)
src/lib/integrations/core/types.ts (1)
  • IntegrationPublicInfo (17-22)
src/app/(dashboard)/app/integrations/page.tsx (2)
src/lib/api/hydration.ts (1)
  • getServerQueryClient (21-23)
src/app/(dashboard)/app/integrations/integrations-content.tsx (1)
  • IntegrationsContent (17-150)
src/lib/integrations/core/base.ts (1)
src/lib/integrations/core/types.ts (4)
  • IntegrationAccount (7-15)
  • IntegrationCreateInput (24-24)
  • IntegrationUpdateInput (25-25)
  • Integration (36-85)
src/app/api/integrations/[integration]/accounts/[id]/route.ts (5)
src/app/api/integrations/route.ts (2)
  • dynamic (7-7)
  • GET (13-24)
src/lib/api/utils.ts (3)
  • requireAuth (68-81)
  • APIError (86-96)
  • handleAPIError (102-124)
src/lib/integrations/index.ts (1)
  • registerIntegrations (10-17)
src/lib/integrations/core/registry.ts (1)
  • getIntegrationRegistry (66-68)
src/lib/auth/check-role.ts (1)
  • isAdmin (46-49)
src/hooks/use-integration.ts (2)
src/lib/api/query-keys.ts (1)
  • queryKeys (13-103)
src/lib/api/integrations.ts (6)
  • fetchIntegrations (17-31)
  • fetchIntegrationAccount (36-56)
  • fetchIntegrationAccountById (61-84)
  • createIntegrationAccount (89-116)
  • updateIntegrationAccount (121-152)
  • deleteIntegrationAccount (157-177)
src/lib/integrations/core/constants.ts (1)
src/lib/integrations/core/types.ts (1)
  • IntegrationStatus (3-3)
src/lib/integrations/xmpp/client.ts (1)
src/lib/integrations/xmpp/config.ts (1)
  • validateXmppConfig (44-84)
src/app/api/integrations/route.ts (3)
src/lib/api/utils.ts (2)
  • requireAuth (68-81)
  • handleAPIError (102-124)
src/lib/integrations/index.ts (1)
  • registerIntegrations (10-17)
src/lib/integrations/core/registry.ts (1)
  • getIntegrationRegistry (66-68)
src/lib/auth/config.ts (1)
src/lib/integrations/core/user-deletion.ts (1)
  • cleanupIntegrationAccounts (11-36)
src/app/(dashboard)/app/integrations/integrations-content.tsx (3)
src/hooks/use-integration.ts (1)
  • useIntegrations (18-24)
src/components/integrations/integration-management.tsx (1)
  • IntegrationManagement (47-243)
src/lib/integrations/xmpp/types.ts (1)
  • XmppAccount (14-24)
src/lib/api/xmpp.ts (2)
src/lib/integrations/xmpp/types.ts (3)
  • XmppAccount (14-24)
  • CreateXmppAccountRequest (29-31)
  • UpdateXmppAccountRequest (36-40)
src/lib/api/integrations.ts (5)
  • fetchIntegrationAccount (36-56)
  • fetchIntegrationAccountById (61-84)
  • createIntegrationAccount (89-116)
  • updateIntegrationAccount (121-152)
  • deleteIntegrationAccount (157-177)
src/lib/api/integrations.ts (1)
src/lib/integrations/core/types.ts (1)
  • IntegrationPublicInfo (17-22)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Seer Code Review
  • GitHub Check: Sourcery review

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines 39 to +44
/**
* Validate XMPP configuration
* Called at module load time to catch configuration errors early
* Validate XMPP configuration lazily
* Only validates when actually needed (when XMPP operations are attempted)
* This prevents blocking the entire application if XMPP is not configured
*/
function validateXmppConfig(): void {
export function validateXmppConfig(): void {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Startup config validation is required for backend config modules.

Lazy validation means misconfigurations surface only at first use. Please ensure XMPP env validation runs during backend startup (for example in integration registration when XMPP is enabled), not only at runtime. Based on learnings, please validate env at startup in backend.

🤖 Prompt for AI Agents
In `@src/lib/integrations/xmpp/config.ts` around lines 39 - 44, The
validateXmppConfig function currently runs lazily; change startup flow so XMPP
config is validated during backend initialization: update the integration
registration/startup code that decides whether XMPP is enabled (e.g., the
function or flag like isXmppEnabled(), enableXmpp, or similar integration
registration routine) to call validateXmppConfig() immediately when XMPP is
enabled, and ensure validateXmppConfig throws on missing/invalid env so startup
fails fast; do not rely on first-use calls—invoke validateXmppConfig from the
integration registration/initialization path so misconfigurations surface at
startup.

- Added Sentry error capturing to improve error tracking during account operations.
- Updated the JID copy functionality to include performance tracing with spans.
- Introduced integration status labels for better user feedback on account statuses.
- Enhanced request body validation in API routes to ensure proper error handling for invalid inputs.
- Deleted the XMPP account management routes, including creation, retrieval, update, and deletion functionalities.
- This cleanup simplifies the codebase and aligns with recent refactoring efforts towards integration management.
- Introduced comprehensive documentation for the new integrations framework, detailing architecture, core components, database schema, type system, API routes, client-side API, and UI components.
- This documentation serves as a guide for developers to implement and manage various integrations effectively, enhancing overall understanding and usability of the framework.
- Removed the XMPP account management component and related hooks to streamline the codebase in favor of the new unified integrations approach.
@kzndotsh kzndotsh merged commit 0cdc9ea into main Jan 16, 2026
4 of 5 checks passed
@kzndotsh kzndotsh deleted the feat/integrations branch January 16, 2026 22:40
This was referenced Jan 27, 2026
@coderabbitai coderabbitai bot mentioned this pull request Feb 27, 2026
18 tasks
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