# Slack OAuth Integration - Product Requirements Document ## Executive Summary This document outlines the requirements for implementing Slack OAuth integration in the Buster application, enabling users to connect their Slack workspaces and utilize Slack messaging capabilities within the platform. ## Overview ### Purpose Enable Buster users to securely authenticate with Slack workspaces, allowing the application to: - Send messages to Slack channels - List available channels - Manage message threads - Track message delivery ### Scope - OAuth 2.0 authentication flow - Token management and storage - Database schema for Slack integrations - API endpoints for OAuth flow - Security and compliance considerations ## Technical Architecture ### Components #### 1. **@buster/slack Package** (Existing) - Standalone Slack integration with no database dependencies - Provides OAuth flow management, channel operations, and messaging - Interface-based design requiring implementation of storage interfaces #### 2. **@apps/server** (To be implemented) - OAuth initiation endpoint - OAuth callback endpoint - Integration management endpoints #### 3. **@packages/database** (Schema additions required) - Slack integration records - Token storage (via Supabase Vault) - OAuth state management ## Detailed Requirements ### OAuth Flow #### 1. OAuth Initiation **Endpoint:** `POST /api/v2/slack/auth/init` **Request Body (Optional):** ```json { "metadata": { "returnUrl": "/settings/integrations", // Where to redirect after OAuth completes "source": "settings_page", // Analytics tracking "projectId": "uuid" // Optional project context } } ``` **Response:** ```json { "authUrl": "https://slack.com/oauth/v2/authorize?...", "state": "secure-random-state" } ``` **Metadata Explained:** The `metadata` object is completely optional and serves to maintain context through the OAuth flow: 1. **returnUrl** - Where in your app to redirect after OAuth completes - Default: `/settings/integrations` - Example: `/projects/123/settings` to return to specific project 2. **source** - Track where user initiated OAuth from - Examples: `"onboarding"`, `"settings_page"`, `"project_view"` - Useful for analytics and user flow optimization 3. **projectId** - Associate integration with specific context - Optional: Since one org = one Slack integration - Useful if you want to track which project triggered the integration 4. **Custom fields** - Any additional context your app needs **How it works:** ```typescript // The metadata gets stored in the pending integration record { id: "integration-id", oauthState: "secure-random-state", oauthMetadata: { returnUrl: "/settings/integrations", source: "settings_page", projectId: "project-123", // System adds: initiatedAt: "2024-01-01T00:00:00Z", ipAddress: "192.168.1.1" }, status: "pending" } // After successful OAuth, use metadata to redirect: const returnUrl = integration.oauthMetadata.returnUrl || '/settings'; return c.redirect(`${returnUrl}?integration=success`); ``` **Requirements:** - Metadata is entirely optional (endpoint works without it) - Stored in `oauth_metadata` JSONB column - Available during callback to restore user context - Cleared after successful OAuth - Size limit: 1KB to prevent abuse #### 2. OAuth Callback **Endpoint:** `GET /api/v2/slack/auth/callback` **Authentication:** This endpoint should be **unauthenticated** because: - Slack redirects here directly after OAuth approval - The user's session might have expired during the OAuth flow - We use the `state` parameter to map back to the user/organization **Query Parameters:** - `code`: Authorization code from Slack - `state`: State parameter that maps to our pending integration **Flow:** 1. Look up pending integration by `state` parameter 2. Verify state hasn't expired (15-minute window) 3. Exchange code for access token with Slack 4. Update integration record with Slack workspace info 5. Store token in Supabase Vault 6. Redirect to success page (from metadata.returnUrl) **Success Response (Redirect):** ``` 302 Redirect to: /settings/integrations?status=success&workspace=Acme%20Corp ``` **Error Response (Redirect):** ``` 302 Redirect to: /settings/integrations?status=error&error=access_denied ``` ### Database Schema (Drizzle) Add the following to `@packages/database/src/schema.ts`: ```typescript // Enum for Slack integration status export const slackIntegrationStatusEnum = pgEnum('slack_integration_status_enum', [ 'pending', 'active', 'failed', 'revoked' ]); // Slack integrations table export const slackIntegrations = pgTable( 'slack_integrations', { id: uuid().defaultRandom().primaryKey().notNull(), organizationId: uuid('organization_id').notNull(), userId: uuid('user_id').notNull(), // OAuth state fields (for pending integrations) oauthState: varchar('oauth_state', { length: 255 }).unique(), oauthExpiresAt: timestamp('oauth_expires_at', { withTimezone: true, mode: 'string' }), oauthMetadata: jsonb('oauth_metadata').default({}), // Slack workspace info (populated after successful OAuth) teamId: varchar('team_id', { length: 255 }), teamName: varchar('team_name', { length: 255 }), teamDomain: varchar('team_domain', { length: 255 }), enterpriseId: varchar('enterprise_id', { length: 255 }), // Bot info botUserId: varchar('bot_user_id', { length: 255 }), scope: text(), // Token reference (actual token in Supabase Vault) tokenVaultKey: varchar('token_vault_key', { length: 255 }).unique(), // Metadata installedBySlackUserId: varchar('installed_by_slack_user_id', { length: 255 }), installedAt: timestamp('installed_at', { withTimezone: true, mode: 'string' }), lastUsedAt: timestamp('last_used_at', { withTimezone: true, mode: 'string' }), status: slackIntegrationStatusEnum().default('pending').notNull(), // Timestamps createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) .defaultNow() .notNull(), updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) .defaultNow() .notNull(), deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), }, (table) => [ foreignKey({ columns: [table.organizationId], foreignColumns: [organizations.id], name: 'slack_integrations_organization_id_fkey', }).onDelete('cascade'), foreignKey({ columns: [table.userId], foreignColumns: [users.id], name: 'slack_integrations_user_id_fkey', }), unique('slack_integrations_org_team_key').on(table.organizationId, table.teamId), index('idx_slack_integrations_org_id').using( 'btree', table.organizationId.asc().nullsLast().op('uuid_ops') ), index('idx_slack_integrations_team_id').using( 'btree', table.teamId.asc().nullsLast().op('text_ops') ), index('idx_slack_integrations_oauth_state').using( 'btree', table.oauthState.asc().nullsLast().op('text_ops') ), index('idx_slack_integrations_oauth_expires').using( 'btree', table.oauthExpiresAt.asc().nullsLast().op('timestamptz_ops') ), check( 'slack_integrations_status_check', sql`(status = 'pending' AND oauth_state IS NOT NULL) OR (status != 'pending' AND team_id IS NOT NULL)` ), ] ); // Slack message tracking table (optional) export const slackMessageTracking = pgTable( 'slack_message_tracking', { id: uuid().defaultRandom().primaryKey().notNull(), integrationId: uuid('integration_id').notNull(), // Internal reference internalMessageId: uuid('internal_message_id').notNull().unique(), // Slack references slackChannelId: varchar('slack_channel_id', { length: 255 }).notNull(), slackMessageTs: varchar('slack_message_ts', { length: 255 }).notNull(), slackThreadTs: varchar('slack_thread_ts', { length: 255 }), // Metadata messageType: varchar('message_type', { length: 50 }).notNull(), // 'message', 'reply', 'update' content: text(), senderInfo: jsonb('sender_info'), // Timestamps sentAt: timestamp('sent_at', { withTimezone: true, mode: 'string' }).notNull(), createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) .defaultNow() .notNull(), }, (table) => [ foreignKey({ columns: [table.integrationId], foreignColumns: [slackIntegrations.id], name: 'slack_message_tracking_integration_id_fkey', }).onDelete('cascade'), index('idx_message_tracking_integration').using( 'btree', table.integrationId.asc().nullsLast().op('uuid_ops') ), index('idx_message_tracking_channel').using( 'btree', table.slackChannelId.asc().nullsLast().op('text_ops') ), index('idx_message_tracking_thread').using( 'btree', table.slackThreadTs.asc().nullsLast().op('text_ops') ), ] ); ``` ### API Implementation Structure #### File Organization ``` @apps/server/src/api/v2/slack/ ├── index.ts # Route definitions ├── index.test.ts # Route tests ├── handler.ts # Request handlers ├── handler.test.ts # Unit tests ├── handler.integration.test.ts # Integration tests └── services/ # Business logic ├── slack-oauth-service.ts # OAuth flow logic ├── slack-oauth-service.test.ts # Service unit tests ├── slack-helpers.ts # Utility functions └── slack-helpers.test.ts # Helper tests ``` #### API Endpoints Since each organization will only have one Slack integration, we need just these endpoints: **1. Initiate OAuth Flow** `POST /api/v2/slack/auth` - Creates pending integration record - Generates OAuth URL with state **2. OAuth Callback** `GET /api/v2/slack/auth/callback` - Handles redirect from Slack - Exchanges code for token - Updates integration to active **3. Get Current Integration** `GET /api/v2/slack/integration` - Returns current integration status - Returns null if no integration exists **4. Remove Integration** `DELETE /api/v2/slack/integration` - Revokes token from Supabase Vault - Soft deletes integration record #### File Responsibilities & Separation of Concerns **index.ts** - Pure routing, no business logic ```typescript // ONLY route definitions and middleware const app = new Hono() .post('/auth', authMiddleware, handler.initiateOAuth) .get('/auth/callback', handler.handleOAuthCallback) .get('/integration', authMiddleware, handler.getIntegration) .delete('/integration', authMiddleware, handler.removeIntegration); export default app; ``` **handler.ts** - Thin HTTP layer, delegates to services ```typescript // Each handler is a thin wrapper that: // 1. Extracts and validates input // 2. Calls the appropriate service function // 3. Formats the response export const initiateOAuth = async (c: Context) => { const { organizationId, userId } = c.get('auth'); // Delegate to service - handler doesn't contain business logic const result = await slackOAuthService.initiateOAuth({ organizationId, userId, }); return c.json(result); }; ``` **services/slack-oauth-service.ts** - Business logic in pure, testable functions ```typescript // Pure functions that can be tested independently export async function initiateOAuth(params: { organizationId: string; userId: string; }): Promise<{ authUrl: string; state: string }> { // Check for existing integration const existing = await slackHelpers.getActiveIntegration(params.organizationId); if (existing) { throw new Error('Integration already exists'); } // Generate OAuth URL const { authUrl, state } = await generateOAuthUrl(params); // Store pending integration await slackHelpers.createPendingIntegration({ ...params, oauthState: state, }); return { authUrl, state }; } // Each function does ONE thing and is independently testable export async function generateOAuthUrl(params: { organizationId: string; userId: string; }): Promise<{ authUrl: string; state: string }> { // Pure OAuth URL generation logic } export async function exchangeCodeForToken(params: { code: string; state: string; }): Promise { // Pure token exchange logic } ``` **services/slack-helpers.ts** - Database operations as pure functions ```typescript // Each helper is a focused, testable function export async function getActiveIntegration( organizationId: string ): Promise { return db .select() .from(slackIntegrations) .where(and( eq(slackIntegrations.organizationId, organizationId), eq(slackIntegrations.status, 'active'), isNull(slackIntegrations.deletedAt) )) .limit(1) .then(rows => rows[0] || null); } export async function createPendingIntegration(params: { organizationId: string; userId: string; oauthState: string; }): Promise { const [integration] = await db .insert(slackIntegrations) .values({ ...params, status: 'pending', oauthExpiresAt: new Date(Date.now() + 15 * 60 * 1000), }) .returning({ id: slackIntegrations.id }); return integration.id; } export async function storeTokenInVault( integrationId: string, token: string ): Promise { // Pure function for vault storage const vaultKey = `slack-token-${integrationId}`; await supabaseVault.store(vaultKey, token); return vaultKey; } ``` #### Key Design Principles 1. **Single Responsibility** - Each function does ONE thing 2. **Pure Functions** - Functions return values based on inputs, minimal side effects 3. **Dependency Injection** - Pass dependencies as parameters for easy mocking 4. **No Business Logic in Handlers** - Handlers only handle HTTP concerns 5. **Testable in Isolation** - Each function can be unit tested independently #### Testing Structure **Unit Tests (*.test.ts)** - Mock external dependencies - Test individual functions in isolation - Focus on business logic correctness - Mock database and Slack API calls **Integration Tests (*.integration.test.ts)** - Use test database - Test full request/response cycle - Mock only external APIs (Slack) - Verify database state changes **Test Utilities** - Shared test factories for creating test data - Mock implementations of interfaces - Test database setup/teardown helpers ### Security Requirements #### Token Storage - Access tokens stored in Supabase Vault - Vault key stored in database, not the token itself - Tokens encrypted at rest - Support for token rotation #### Access Control - Integration scoped to organization - User must have appropriate permissions - Audit logging for all OAuth operations - Rate limiting on OAuth endpoints #### OAuth Security - State parameter validation with 15-minute expiry - HTTPS required for all OAuth endpoints - Redirect URI whitelist validation - PKCE support for enhanced security (future) ### Implementation Interfaces #### ISlackTokenStorage Implementation ```typescript class DatabaseTokenStorage implements ISlackTokenStorage { async storeToken(key: string, token: string): Promise { // Store in Supabase Vault // Update slack_integrations.token_vault_key } async getToken(key: string): Promise { // Retrieve from Supabase Vault } async deleteToken(key: string): Promise { // Remove from Vault // Clear slack_integrations.token_vault_key } async hasToken(key: string): Promise { // Check if token exists in Vault } } ``` #### ISlackOAuthStateStorage Implementation ```typescript class DatabaseOAuthStateStorage implements ISlackOAuthStateStorage { async storeState(state: string, data: SlackOAuthStateData): Promise { // Create a pending integration record with OAuth state await db.insert(slackIntegrations).values({ userId: data.metadata.userId, organizationId: data.metadata.organizationId, oauth_state: state, oauth_expires_at: new Date(data.expiresAt), oauth_metadata: data.metadata, status: 'pending' }); } async getState(state: string): Promise { // Query slack_integrations for pending OAuth const integration = await db .select() .from(slackIntegrations) .where(and( eq(slackIntegrations.oauth_state, state), eq(slackIntegrations.status, 'pending'), gt(slackIntegrations.oauth_expires_at, new Date()) )) .limit(1); if (!integration[0]) return null; return { expiresAt: integration[0].oauth_expires_at.getTime(), metadata: integration[0].oauth_metadata }; } async deleteState(state: string): Promise { // Clean up failed/expired OAuth attempts await db .delete(slackIntegrations) .where(and( eq(slackIntegrations.oauth_state, state), eq(slackIntegrations.status, 'pending') )); } } ``` ### Slack App Configuration #### Required OAuth Scopes - `channels:read` - List channels in workspace - `chat:write` - Send messages as bot - `chat:write.public` - Send to channels bot hasn't joined - `channels:join` - Join public channels - `users:read` - Read user information #### OAuth Redirect URLs - Development: `http://localhost:3000/api/v2/slack/auth/callback` - Staging: `https://staging.buster.so/api/v2/slack/auth/callback` - Production: `https://app.buster.so/api/v2/slack/auth/callback` ### Error Handling #### OAuth Errors - `OAUTH_ACCESS_DENIED` - User denied authorization - `OAUTH_INVALID_STATE` - Invalid or expired state - `OAUTH_TOKEN_EXCHANGE_FAILED` - Failed to exchange code - `INVALID_SCOPE` - Required scopes not granted - `WORKSPACE_LIMIT_REACHED` - Organization workspace limit #### Integration Errors - `INTEGRATION_NOT_FOUND` - Integration doesn't exist - `INTEGRATION_INACTIVE` - Integration has been deactivated - `INVALID_CHANNEL` - Channel not accessible - `RATE_LIMITED` - Slack API rate limit ### Monitoring & Analytics #### Metrics to Track - OAuth flow completion rate - Time to complete OAuth flow - Integration usage by organization - Message send success rate - Channel access patterns - Error rates by type #### Audit Events - OAuth flow initiated - OAuth flow completed/failed - Integration created/deleted - Token refreshed - Message sent/failed - Permission changes ### User Experience #### Integration Flow 1. User navigates to Integrations page 2. Clicks "Connect Slack" 3. Redirected to Slack OAuth page 4. Approves permissions 5. Redirected back to Buster 6. See success message with workspace info 7. Can immediately start using integration #### Management Interface - List all connected workspaces - Show connection status - Last used timestamp - Quick actions (test, disconnect) - Channel browser - Message history viewer ### Future Enhancements #### Phase 2 - Incoming webhooks support - Event subscriptions (message reactions, new messages) - Slash commands - Interactive components (buttons, modals) - Multiple workspace per organization #### Phase 3 - Scheduled messages - Message formatting builder - Template library - Bulk operations - Analytics dashboard ### Testing Requirements #### Test File Coverage **index.test.ts** - Route registration verification - Middleware application checks - Route parameter validation **handler.test.ts** - Test HTTP layer only ```typescript describe('SlackHandler', () => { // Mock the service layer const mockSlackOAuthService = { initiateOAuth: vi.fn(), handleCallback: vi.fn(), getIntegration: vi.fn(), removeIntegration: vi.fn(), }; describe('initiateOAuth', () => { it('should extract auth context and call service'); it('should return 200 with auth URL'); it('should handle service errors appropriately'); }); describe('handleOAuthCallback', () => { it('should validate query parameters'); it('should call service with code and state'); it('should redirect on success'); it('should handle errors with proper status codes'); }); }); ``` **services/slack-oauth-service.test.ts** - Test business logic ```typescript describe('SlackOAuthService', () => { // Mock only external dependencies const mockSlackHelpers = { getActiveIntegration: vi.fn(), createPendingIntegration: vi.fn(), updateIntegrationStatus: vi.fn(), }; describe('initiateOAuth', () => { it('should throw if integration already exists'); it('should generate valid OAuth URL'); it('should create pending integration with state'); it('should set 15-minute expiry on state'); }); describe('exchangeCodeForToken', () => { it('should validate state exists and not expired'); it('should call Slack API with correct parameters'); it('should store token in vault'); it('should update integration to active status'); it('should clear OAuth state after success'); }); }); ``` **services/slack-helpers.test.ts** - Test data layer ```typescript describe('SlackHelpers', () => { describe('getActiveIntegration', () => { it('should return active integration for org'); it('should exclude deleted integrations'); it('should return null if no integration'); }); describe('createPendingIntegration', () => { it('should create with pending status'); it('should set OAuth expiry timestamp'); it('should return integration ID'); }); describe('storeTokenInVault', () => { it('should generate correct vault key'); it('should store encrypted token'); it('should return vault key'); }); }); ``` **handler.integration.test.ts** - Full OAuth flow with database - State expiration handling - Organization isolation - Concurrent request handling **services/slack-oauth-service.test.ts** - OAuth URL generation - State management - Token exchange logic - Vault integration mocking **services/slack-helpers.test.ts** - Database query functions - Token encryption/decryption - Status checking logic - Data transformation utilities #### Testing Best Practices 1. **Test Data Isolation** - Each test creates its own organization/user - Clean up after each test - No shared state between tests 2. **Mock Strategy** - Mock Slack API responses - Mock Supabase Vault operations - Use real database in integration tests - Mock time for expiration tests 3. **Error Scenarios** - Network failures - Invalid OAuth codes - Expired states - Rate limiting - Database conflicts 4. **Security Testing** - CSRF protection validation - Authorization checks - Token exposure prevention - Cross-organization access ### Development & Testing Workflow #### Local Development Setup 1. **Environment Configuration** ```bash # .env.local SLACK_INTEGRATION_ENABLED=false # Set to true when ready to test SLACK_CLIENT_ID=your-dev-client-id SLACK_CLIENT_SECRET=your-dev-client-secret SLACK_REDIRECT_URI=http://localhost:3000/api/v2/slack/auth/callback SUPABASE_VAULT_KEY=your-vault-key ``` **Feature Flag Implementation:** ```typescript // services/slack-oauth-service.ts export async function initiateOAuth(params: { organizationId: string; userId: string; }): Promise<{ authUrl: string; state: string }> { // Check if Slack integration is enabled if (!process.env.SLACK_INTEGRATION_ENABLED || process.env.SLACK_INTEGRATION_ENABLED === 'false') { throw new Error('Slack integration is not enabled'); } // Regular OAuth flow continues... } ``` 2. **Testing with Real Slack** For local development, you'll need to set up a real Slack app and use ngrok to expose your localhost: **Slack App Setup:** 1. Go to https://api.slack.com/apps and create a new app 2. Choose "From scratch" and select your workspace 3. Add OAuth scopes under "OAuth & Permissions": - `channels:read` - `chat:write` - `chat:write.public` 4. Add redirect URL (will update with ngrok URL) **Local Development with ngrok:** ```bash # Start your local server pnpm run dev --filter=@buster-app/server # In another terminal, expose your localhost ngrok http 3000 # Copy the HTTPS URL from ngrok (e.g., https://abc123.ngrok.io) # Update your Slack app's redirect URL to: # https://abc123.ngrok.io/api/v2/slack/auth/callback # Update your .env.local SLACK_CLIENT_ID=your-slack-client-id SLACK_CLIENT_SECRET=your-slack-client-secret SLACK_REDIRECT_URI=https://abc123.ngrok.io/api/v2/slack/auth/callback ``` **Testing Flow:** 1. Start your server and ngrok 2. Navigate to your OAuth initiation endpoint 3. You'll be redirected to real Slack OAuth 4. Approve access in your test workspace 5. Slack redirects back to your ngrok URL 6. Your callback handler processes the real OAuth code 3. **Database Setup** ```bash # Run migrations pnpm run db:migrate # Seed test data pnpm run db:seed:dev ``` #### Testing Workflow **1. Unit Tests (Fast Feedback)** ```bash # Run all unit tests (works without Slack credentials) pnpm run test:unit --filter=@buster-app/server # Run specific test file pnpm run test apps/server/src/api/v2/slack/handler.test.ts # Watch mode for development pnpm run test:watch apps/server/src/api/v2/slack ``` **Unit Test Mocking Strategy:** ```typescript // handler.test.ts - Tests work without real Slack describe('SlackHandler', () => { beforeEach(() => { // Mock environment check vi.stubEnv('SLACK_INTEGRATION_ENABLED', 'true'); // Mock Slack API calls vi.mock('@buster/slack', () => ({ SlackAuthService: vi.fn(() => ({ generateAuthUrl: vi.fn().mockResolvedValue({ authUrl: 'https://mock-url', state: 'mock-state' }), handleCallback: vi.fn().mockResolvedValue({ teamId: 'T-MOCK', teamName: 'Mock Team' }) })) })); }); it('should handle disabled integration gracefully', async () => { vi.stubEnv('SLACK_INTEGRATION_ENABLED', 'false'); const response = await app.request('/api/v2/slack/auth', { method: 'POST' }); expect(response.status).toBe(503); // Service Unavailable expect(await response.json()).toEqual({ error: 'Slack integration is not enabled' }); }); }); ``` **2. Integration Tests (Conditional Execution)** ```bash # Integration tests skip Slack tests when disabled pnpm run test:integration --filter=@buster-app/server ``` ```typescript // handler.integration.test.ts describe('Slack OAuth Integration', () => { const slackEnabled = process.env.SLACK_INTEGRATION_ENABLED === 'true'; describe.skipIf(!slackEnabled)('with real Slack', () => { // These tests only run when SLACK_INTEGRATION_ENABLED=true it('should complete OAuth flow', async () => { // Real Slack OAuth test }); }); describe('with mocked Slack', () => { // These tests always run it('should handle database operations', async () => { // Test database without real Slack }); }); }); ``` **3. Manual Testing Flow** ```bash # Start server pnpm run dev --filter=@buster-app/server # Test OAuth flow curl -X POST http://localhost:3000/api/v2/slack/auth \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" # Response includes auth URL # Visit URL in browser to complete OAuth # Check integration status curl http://localhost:3000/api/v2/slack/integration \ -H "Authorization: Bearer $TOKEN" # Remove integration curl -X DELETE http://localhost:3000/api/v2/slack/integration \ -H "Authorization: Bearer $TOKEN" ``` #### Verification Checklist **OAuth Flow Verification** - [ ] Auth URL includes all required parameters - [ ] State is stored in database with expiry - [ ] Callback validates state correctly - [ ] Token exchange succeeds - [ ] Token stored in Supabase Vault - [ ] Integration record updated to 'active' - [ ] OAuth metadata cleared after success **Security Verification** - [ ] State expires after 15 minutes - [ ] Invalid state returns error - [ ] Cross-organization access blocked - [ ] Tokens never exposed in API responses - [ ] Deleted integrations can't be accessed **Error Handling Verification** - [ ] User denial handled gracefully - [ ] Network errors return appropriate status - [ ] Duplicate integration attempts blocked - [ ] Missing integration returns 404 ### CI/CD Considerations #### Environment-Based Configuration **Local Development (default):** ```env SLACK_INTEGRATION_ENABLED=false # Slack credentials optional ``` **CI/CD Pipeline:** ```env SLACK_INTEGRATION_ENABLED=false # Tests run with mocked Slack ``` **Staging:** ```env SLACK_INTEGRATION_ENABLED=true SLACK_CLIENT_ID=staging-client-id SLACK_CLIENT_SECRET=xxx (from secrets) SLACK_REDIRECT_URI=https://staging.buster.so/api/v2/slack/auth/callback ``` **Production:** ```env SLACK_INTEGRATION_ENABLED=true SLACK_CLIENT_ID=prod-client-id SLACK_CLIENT_SECRET=xxx (from Supabase Vault) SLACK_REDIRECT_URI=https://app.buster.so/api/v2/slack/auth/callback ``` #### API Response When Disabled When `SLACK_INTEGRATION_ENABLED=false`, endpoints return: ```json { "error": "Slack integration is not enabled", "code": "INTEGRATION_DISABLED", "status": 503 } ``` This allows: - Frontend to conditionally show/hide Slack features - Tests to run without Slack credentials - Gradual rollout with feature flags ### Deployment Considerations #### Environment Variables ```env SLACK_INTEGRATION_ENABLED=true/false SLACK_CLIENT_ID=xxx SLACK_CLIENT_SECRET=xxx (store in Supabase Vault) SLACK_REDIRECT_URI=https://app.buster.so/api/v2/slack/auth/callback SLACK_OAUTH_SCOPES=channels:read,chat:write,chat:write.public ``` #### Migration Strategy 1. Deploy database schema 2. Configure Supabase Vault 3. Deploy API endpoints 4. Update Slack app configuration 5. Enable feature flag 6. Gradual rollout ### Success Criteria 1. **Security**: Zero OAuth-related security incidents 2. **Reliability**: 99.9% uptime for OAuth endpoints 3. **Performance**: OAuth flow completes in <3 seconds 4. **Adoption**: 50% of organizations connect Slack within 30 days 5. **Retention**: 90% of integrations remain active after 60 days ### Dependencies - **@buster/slack** package (existing) - **Supabase Vault** for token storage - **Slack API** availability - **Database** migrations completed - **Frontend** integration UI ### Timeline - **Week 1-2**: Database schema and migrations - **Week 2-3**: OAuth endpoints implementation - **Week 3-4**: Integration management APIs - **Week 4-5**: Frontend integration - **Week 5-6**: Testing and security review - **Week 6-7**: Documentation and deployment ### Appendix #### Slack API Reference - [OAuth 2.0 Flow](https://api.slack.com/authentication/oauth-v2) - [Web API Methods](https://api.slack.com/web) - [Bot Tokens & Scopes](https://api.slack.com/authentication/token-types) #### Internal References - @buster/slack package documentation - Supabase Vault documentation - Organization permissions model