30 KiB
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):
{
"metadata": {
"returnUrl": "/settings/integrations", // Where to redirect after OAuth completes
"source": "settings_page", // Analytics tracking
"projectId": "uuid" // Optional project context
}
}
Response:
{
"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:
-
returnUrl - Where in your app to redirect after OAuth completes
- Default:
/settings/integrations
- Example:
/projects/123/settings
to return to specific project
- Default:
-
source - Track where user initiated OAuth from
- Examples:
"onboarding"
,"settings_page"
,"project_view"
- Useful for analytics and user flow optimization
- Examples:
-
projectId - Associate integration with specific context
- Optional: Since one org = one Slack integration
- Useful if you want to track which project triggered the integration
-
Custom fields - Any additional context your app needs
How it works:
// 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 Slackstate
: State parameter that maps to our pending integration
Flow:
- Look up pending integration by
state
parameter - Verify state hasn't expired (15-minute window)
- Exchange code for access token with Slack
- Update integration record with Slack workspace info
- Store token in Supabase Vault
- 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
:
// 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
// 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
// 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
// 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<SlackTokenResponse> {
// Pure token exchange logic
}
services/slack-helpers.ts - Database operations as pure functions
// Each helper is a focused, testable function
export async function getActiveIntegration(
organizationId: string
): Promise<SlackIntegration | null> {
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<string> {
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<string> {
// Pure function for vault storage
const vaultKey = `slack-token-${integrationId}`;
await supabaseVault.store(vaultKey, token);
return vaultKey;
}
Key Design Principles
- Single Responsibility - Each function does ONE thing
- Pure Functions - Functions return values based on inputs, minimal side effects
- Dependency Injection - Pass dependencies as parameters for easy mocking
- No Business Logic in Handlers - Handlers only handle HTTP concerns
- 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
class DatabaseTokenStorage implements ISlackTokenStorage {
async storeToken(key: string, token: string): Promise<void> {
// Store in Supabase Vault
// Update slack_integrations.token_vault_key
}
async getToken(key: string): Promise<string | null> {
// Retrieve from Supabase Vault
}
async deleteToken(key: string): Promise<void> {
// Remove from Vault
// Clear slack_integrations.token_vault_key
}
async hasToken(key: string): Promise<boolean> {
// Check if token exists in Vault
}
}
ISlackOAuthStateStorage Implementation
class DatabaseOAuthStateStorage implements ISlackOAuthStateStorage {
async storeState(state: string, data: SlackOAuthStateData): Promise<void> {
// 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<SlackOAuthStateData | null> {
// 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<void> {
// 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 workspacechat:write
- Send messages as botchat:write.public
- Send to channels bot hasn't joinedchannels:join
- Join public channelsusers: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 authorizationOAUTH_INVALID_STATE
- Invalid or expired stateOAUTH_TOKEN_EXCHANGE_FAILED
- Failed to exchange codeINVALID_SCOPE
- Required scopes not grantedWORKSPACE_LIMIT_REACHED
- Organization workspace limit
Integration Errors
INTEGRATION_NOT_FOUND
- Integration doesn't existINTEGRATION_INACTIVE
- Integration has been deactivatedINVALID_CHANNEL
- Channel not accessibleRATE_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
- User navigates to Integrations page
- Clicks "Connect Slack"
- Redirected to Slack OAuth page
- Approves permissions
- Redirected back to Buster
- See success message with workspace info
- 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
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
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
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
-
Test Data Isolation
- Each test creates its own organization/user
- Clean up after each test
- No shared state between tests
-
Mock Strategy
- Mock Slack API responses
- Mock Supabase Vault operations
- Use real database in integration tests
- Mock time for expiration tests
-
Error Scenarios
- Network failures
- Invalid OAuth codes
- Expired states
- Rate limiting
- Database conflicts
-
Security Testing
- CSRF protection validation
- Authorization checks
- Token exposure prevention
- Cross-organization access
Development & Testing Workflow
Local Development Setup
- Environment Configuration
# .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:
// 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...
}
- 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:
- Go to https://api.slack.com/apps and create a new app
- Choose "From scratch" and select your workspace
- Add OAuth scopes under "OAuth & Permissions":
channels:read
chat:write
chat:write.public
- Add redirect URL (will update with ngrok URL)
Local Development with ngrok:
# 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:
-
Start your server and ngrok
-
Navigate to your OAuth initiation endpoint
-
You'll be redirected to real Slack OAuth
-
Approve access in your test workspace
-
Slack redirects back to your ngrok URL
-
Your callback handler processes the real OAuth code
-
Database Setup
# Run migrations
pnpm run db:migrate
# Seed test data
pnpm run db:seed:dev
Testing Workflow
1. Unit Tests (Fast Feedback)
# 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:
// 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)
# Integration tests skip Slack tests when disabled
pnpm run test:integration --filter=@buster-app/server
// 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
# 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):
SLACK_INTEGRATION_ENABLED=false
# Slack credentials optional
CI/CD Pipeline:
SLACK_INTEGRATION_ENABLED=false
# Tests run with mocked Slack
Staging:
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:
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:
{
"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
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
- Deploy database schema
- Configure Supabase Vault
- Deploy API endpoints
- Update Slack app configuration
- Enable feature flag
- Gradual rollout
Success Criteria
- Security: Zero OAuth-related security incidents
- Reliability: 99.9% uptime for OAuth endpoints
- Performance: OAuth flow completes in <3 seconds
- Adoption: 50% of organizations connect Slack within 30 days
- 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
Internal References
- @buster/slack package documentation
- Supabase Vault documentation
- Organization permissions model