Skip to content

feat(integrations): Add builtin provider install flow#700

Merged
2witstudios merged 2 commits intomasterfrom
feat/builtin-integration-install-flow
Feb 18, 2026
Merged

feat(integrations): Add builtin provider install flow#700
2witstudios merged 2 commits intomasterfrom
feat/builtin-integration-install-flow

Conversation

@2witstudios
Copy link
Owner

@2witstudios 2witstudios commented Feb 18, 2026

Summary

  • Builtin providers (GitHub, Generic Webhook) are defined in code but never inserted into the database, so the settings UI shows nothing to connect
  • Adds an explicit Install step: new "Built-in Integrations" section in /settings/integrations lists uninstalled builtins with Install buttons
  • Clicking Install copies the builtin config into integrationProviders table, after which the existing Connect → OAuth flow works

Changes

  • GET /api/integrations/providers/available — returns builtins not yet in DB
  • POST /api/integrations/providers/install — copies builtin config into DB (admin-only via verifyAdminAuth + isAdminAuthError, which handles session + CSRF + role-version validation)
  • useAvailableBuiltins() SWR hook in useIntegrations.ts
  • New "Built-in Integrations" card section in the settings integrations page, with proper error state handling

Review feedback addressed (e8dce77)

  • P1 — Admin authorization: Install endpoint now uses verifyAdminAuth + isAdminAuthError instead of session-only auth. Also fixed the !adminAuth bug (both VerifiedUser and NextResponse are truthy, so the check never caught errors)
  • P2 — Error state: Settings UI now surfaces builtinsError with a red error banner instead of showing misleading "all installed" when the fetch fails

Test plan

  • Visit /settings/integrations — confirm "Built-in Integrations" section shows GitHub and Generic Webhook
  • Click Install on GitHub — confirm it moves to "Available Providers" section
  • Click Connect on GitHub — confirm OAuth dialog opens and redirects to GitHub
  • After OAuth callback, confirm GitHub appears in "Connected" section
  • Refresh page — confirm "Built-in Integrations" shows "All built-in integrations have been installed" when all are installed
  • Verify non-admin users get 403 when attempting to install a builtin provider

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Built-in integrations discovery: Browse and view all available built-in providers from your integrations settings page
    • One-click installation: Install built-in integrations with automatic updates, progress indicators, and confirmation messages
    • Enhanced feedback: Loading states, error handling, and success notifications throughout the installation process

Builtin providers (GitHub, Generic Webhook) are defined in code but never
inserted into the database, so the settings UI shows nothing. This adds an
explicit install step so users can activate builtins before connecting.

- GET /api/integrations/providers/available — returns uninstalled builtins
- POST /api/integrations/providers/install — copies builtin config into DB
- useAvailableBuiltins SWR hook for the new endpoint
- "Built-in Integrations" card section in /settings/integrations UI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 18, 2026

📝 Walkthrough

Walkthrough

This PR introduces functionality to discover and install built-in integration providers. It adds two API routes: one to list available (not-yet-installed) built-in providers, and another to handle provider installation. The frontend integrations page is updated to display available built-ins with install buttons, while new hooks manage data fetching and state synchronization.

Changes

Cohort / File(s) Summary
API Routes - Provider Discovery & Installation
apps/web/src/app/api/integrations/providers/available/route.ts, apps/web/src/app/api/integrations/providers/install/route.ts
New GET and POST handlers for listing available built-in providers and installing them. Available route fetches enabled providers and returns non-installed built-ins; install route validates requests, checks for existing installations, and creates new provider records with appropriate error handling.
Frontend Page & UI Components
apps/web/src/app/settings/integrations/page.tsx
Extends integrations settings page with built-in providers section, install handlers, loading/error/empty states, and per-item installation tracking. Integrates with new hooks and refreshes provider lists after installation.
Data Fetching & Integration Hooks
apps/web/src/hooks/useIntegrations.ts
Adds useAvailableBuiltins hook to fetch available providers and AvailableBuiltin interface. Updates useProviders to export mutate method for data refresh after installations.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client (Browser)
    participant API as GET /api/.../available
    participant Auth as Auth/Session
    participant DB as Database
    
    Client->>API: Request available providers
    API->>Auth: Verify session scope
    Auth-->>API: Session valid
    API->>DB: Fetch enabled providers
    DB-->>API: Provider list
    API->>API: Compute uninstalled built-ins
    API-->>Client: Return { providers: [...] }
Loading
sequenceDiagram
    participant Client as Client (Browser)
    participant API as POST /api/.../install
    participant Auth as Admin Auth
    participant DB as Database
    
    Client->>API: POST { builtinId }
    API->>Auth: Verify admin authentication
    Auth-->>API: Admin confirmed
    API->>API: Validate schema
    API->>DB: Resolve builtin provider
    DB-->>API: Builtin details
    API->>DB: Check existing installation
    DB-->>API: Not found (available)
    API->>DB: Create provider record
    DB-->>API: New provider created
    API-->>Client: 201 { id, slug, name }
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 A flutter of whiskers, a hop of delight,
Built-ins now nestled, installed just right!
From available lists to the database's care,
Fresh integrations bloom everywhere! 🌱

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the primary change: adding a built-in provider installation flow with API endpoints, a data fetching hook, and UI components.

✏️ 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/builtin-integration-install-flow

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

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bdf9f26cc6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +23 to +24
const auth = await authenticateRequestWithOptions(request, AUTH_OPTIONS);
if (isAuthError(auth)) return auth.error;

Choose a reason for hiding this comment

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

P1 Badge Restrict builtin provider installs to admins

This handler allows any authenticated session to create a new global integration_providers row (isSystem: true, driveId: null) without an admin check, so any regular user can change integration availability for all users. That bypasses the admin gate used by other provider-creation endpoints and creates an authorization gap in multi-user deployments.

Useful? React with 👍 / 👎.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Good catch — this was indeed an authorization gap.

Fixed in e8dce77: Replaced the session-only authenticateRequestWithOptions with verifyAdminAuth + isAdminAuthError, which handles session authentication, CSRF validation, and admin role-version validation in one call.

The previous inline pattern (if (!adminAuth)) was also a bug: verifyAdminAuth returns VerifiedUser | NextResponse, both truthy, so the check never caught errors. Now using the correct isAdminAuthError() type guard (same as the withAdminAuth HOF uses internally).

Changes:

  • apps/web/src/app/api/integrations/providers/install/route.ts — lines 3, 16-17: switched to verifyAdminAuth + isAdminAuthError, removed redundant authenticateRequestWithOptions + AUTH_OPTIONS
  • createdBy now uses adminUser.id from the verified admin user

Comment on lines +309 to +312
) : builtins.length === 0 ? (
<p className="text-sm text-muted-foreground text-center py-6">
All built-in integrations have been installed.
</p>

Choose a reason for hiding this comment

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

P2 Badge Show builtin fetch failures instead of 'all installed'

When the /api/integrations/providers/available request fails, the hook falls back to an empty array and this branch renders “All built-in integrations have been installed,” which is incorrect and hides a real backend/auth error from the user. In that failure mode users are told setup is complete even though installable builtins may exist.

Useful? React with 👍 / 👎.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Good catch — fixed in e8dce77.

Changes in apps/web/src/app/settings/integrations/page.tsx:

  • Line 36: Now destructures error: builtinsError from useAvailableBuiltins()
  • Lines 309-313: Added error state check (builtinsError ?) before the empty-array check, showing a red error banner ("Failed to load available integrations") consistent with how providersError and connectionsError are displayed in the other cards on this page.

The "All built-in integrations have been installed" message now only renders when the fetch actually succeeded and returned an empty array.

- Install endpoint: Replace session-only auth with verifyAdminAuth +
  isAdminAuthError to properly restrict builtin installs to admins.
  The previous pattern (`!adminAuth`) never caught errors since both
  VerifiedUser and NextResponse are truthy.
- Settings UI: Surface builtinsError state instead of showing misleading
  "all installed" message when the /available fetch fails.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (4)
apps/web/src/hooks/useIntegrations.ts (1)

60-65: Consider exporting AvailableBuiltin for downstream consumers.

The interface is only used internally by useAvailableBuiltins, but exporting it would let consuming components type-annotate variables or props explicitly, consistent with how SafeProvider, SafeConnection, etc. are exported from types.ts.

-interface AvailableBuiltin {
+export interface AvailableBuiltin {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/hooks/useIntegrations.ts` around lines 60 - 65, Export the
AvailableBuiltin interface so downstream consumers can reference it; update the
declaration of AvailableBuiltin to be exported (export interface
AvailableBuiltin ...) in the same module where useAvailableBuiltins is defined
and ensure any local imports/usages still compile — this lets components import
AvailableBuiltin from the hook file just like other exported types (e.g.,
SafeProvider/SafeConnection).
apps/web/src/app/api/integrations/providers/install/route.ts (2)

56-56: Double cast through unknown circumvents type safety.

builtin as unknown as Record<string, unknown> is effectively an unchecked cast. If the config column is meant to hold the full builtin definition, consider defining a shared type or using a proper serialization step so the compiler can verify the shape. This also stores redundant data (name, description, etc.) already saved in dedicated columns.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/api/integrations/providers/install/route.ts` at line 56, The
assignment config: builtin as unknown as Record<string, unknown> bypasses type
checks; replace the double-cast by giving config a proper type or serializing
only the intended data: either define and import a shared BuiltinConfig (or
Builtin) interface and use config: builtin as BuiltinConfig, or explicitly build
a minimal object/JSON to store (e.g., pick only the fields needed) and assign
config: JSON.parse(JSON.stringify(minimalBuiltin)) or similar; update the
assignment referencing the builtin variable and the config property so the
compiler can verify shape and you avoid storing redundant full builtin metadata.

25-34: Malformed JSON body returns 500 instead of 400.

If the client sends a non-JSON body, request.json() on line 26 throws and falls through to the generic catch on line 64, returning a 500. Parsing the body before the try block or adding a dedicated catch would yield a more accurate 400 response.

Proposed fix
+  let body: unknown;
+  try {
+    body = await request.json();
+  } catch {
+    return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
+  }
+
   try {
-    const body = await request.json();
     const validation = installSchema.safeParse(body);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/api/integrations/providers/install/route.ts` around lines 25
- 34, The handler currently calls request.json() and lets any JSON parse errors
escape to the outer catch (causing a 500); wrap the body parsing in its own
try/catch (or add a dedicated catch for request.json()) before running
installSchema.safeParse so that a malformed/non-JSON body returns
NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) instead of falling
through to the generic 500; update the code around the request.json() call and
subsequent use of installSchema.safeParse to return a 400 on parse errors.
apps/web/src/app/settings/integrations/page.tsx (1)

90-102: Concurrent installs can clobber installingBuiltin state.

Since installingBuiltin is a single string | null, clicking Install on a second builtin while the first is still in-flight overwrites the tracking state — the first button loses its spinner/disabled state. This is unlikely with only a few builtins, but if you want to be safe:

Track multiple in-flight installs with a Set
-  const [installingBuiltin, setInstallingBuiltin] = useState<string | null>(null);
+  const [installingBuiltins, setInstallingBuiltins] = useState<Set<string>>(new Set());

   const handleInstallBuiltin = async (builtinId: string) => {
-    setInstallingBuiltin(builtinId);
+    setInstallingBuiltins((prev) => new Set(prev).add(builtinId));
     try {
       await post('/api/integrations/providers/install', { builtinId });
       toast.success('Integration installed');
       mutateProviders();
       mutateBuiltins();
     } catch {
       toast.error('Failed to install integration');
     } finally {
-      setInstallingBuiltin(null);
+      setInstallingBuiltins((prev) => {
+        const next = new Set(prev);
+        next.delete(builtinId);
+        return next;
+      });
     }
   };

And update the button check:

-  disabled={installingBuiltin === builtin.id}
+  disabled={installingBuiltins.has(builtin.id)}
-  {installingBuiltin === builtin.id ? (
+  {installingBuiltins.has(builtin.id) ? (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/settings/integrations/page.tsx` around lines 90 - 102, The
handler handleInstallBuiltin uses a single installingBuiltin string which gets
clobbered by concurrent installs; change the state from installingBuiltin:
string | null to a Set<string> (e.g., installingBuiltins) and update
handleInstallBuiltin to add the builtinId to the set before the async post,
remove it in finally, and use set state updates (prev => new Set(prev).add(...)
/ delete(...)) to avoid races; also update the button rendering logic that
currently checks installingBuiltin === id to instead check
installingBuiltins.has(id) so each install button shows its own spinner/disabled
state without being overwritten.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/web/src/app/api/integrations/providers/install/route.ts`:
- Line 56: The assignment config: builtin as unknown as Record<string, unknown>
bypasses type checks; replace the double-cast by giving config a proper type or
serializing only the intended data: either define and import a shared
BuiltinConfig (or Builtin) interface and use config: builtin as BuiltinConfig,
or explicitly build a minimal object/JSON to store (e.g., pick only the fields
needed) and assign config: JSON.parse(JSON.stringify(minimalBuiltin)) or
similar; update the assignment referencing the builtin variable and the config
property so the compiler can verify shape and you avoid storing redundant full
builtin metadata.
- Around line 25-34: The handler currently calls request.json() and lets any
JSON parse errors escape to the outer catch (causing a 500); wrap the body
parsing in its own try/catch (or add a dedicated catch for request.json())
before running installSchema.safeParse so that a malformed/non-JSON body returns
NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) instead of falling
through to the generic 500; update the code around the request.json() call and
subsequent use of installSchema.safeParse to return a 400 on parse errors.

In `@apps/web/src/app/settings/integrations/page.tsx`:
- Around line 90-102: The handler handleInstallBuiltin uses a single
installingBuiltin string which gets clobbered by concurrent installs; change the
state from installingBuiltin: string | null to a Set<string> (e.g.,
installingBuiltins) and update handleInstallBuiltin to add the builtinId to the
set before the async post, remove it in finally, and use set state updates (prev
=> new Set(prev).add(...) / delete(...)) to avoid races; also update the button
rendering logic that currently checks installingBuiltin === id to instead check
installingBuiltins.has(id) so each install button shows its own spinner/disabled
state without being overwritten.

In `@apps/web/src/hooks/useIntegrations.ts`:
- Around line 60-65: Export the AvailableBuiltin interface so downstream
consumers can reference it; update the declaration of AvailableBuiltin to be
exported (export interface AvailableBuiltin ...) in the same module where
useAvailableBuiltins is defined and ensure any local imports/usages still
compile — this lets components import AvailableBuiltin from the hook file just
like other exported types (e.g., SafeProvider/SafeConnection).

@2witstudios 2witstudios merged commit 82a968a into master Feb 18, 2026
3 checks passed
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