Third-Party Service Integrations
Integration Architecture
Third-party service integrations in flow8 follow a consistent pattern:
User/Admin flow8 External Service │ │ │ │──────────────────────→│ │ │ "Connect Slack" │ │ │ │──── Redirect to Auth ───→│ │ │ (OAuth2 flow) │ │ │←───── Auth Code ────────│ │ │ │ │ │─────────────────────────→│ │ │ Exchange code for token │ │ │←──── Access Token ──────│ │ │ │ │←──────────────────────│ │ │ "Connected!" │ │ │ (store encrypted) │ │Integration Types
OAuth2 Integrations
These integrations use delegated authentication via OAuth2:
| Service | Flow | Scopes | Refresh | Notes |
|---|---|---|---|---|
| Microsoft | /auth/callback | Mail.Send, Mail.ReadWrite, Calendars.ReadWrite | Yes (auto-refresh) | Azure AD integration, auto-user creation |
| /auth/callback | gmail.send, drive, sheets | Yes (auto-refresh) | Broad API coverage, reliable | |
| Slack | /auth/callback | chat:write, files:write | Yes (auto-refresh) | Simple bot API, good for notifications |
| Clio | /auth/callback | client.read, document.read | No (long-lived) | Legal CRM, manual refresh needed |
| QuickBooks Online | /auth/callback | quickbooks | Yes (refresh token) | Accounting, Intuit OAuth |
| Bexio | /auth/callback | client.read, contact.read | No (long-lived) | Swiss accounting software |
API Key Integrations
These use API keys stored securely:
| Service | Auth | Credential Type | Notes |
|---|---|---|---|
| AWS S3 | IAM | Access Key ID + Secret Key | Used for storage, encrypted in DB |
| Anthropic | API Key | API Key | Direct API access |
| OpenAI | API Key | API Key | Direct API access |
| SMTP | Basic Auth | Username + Password | Email sending |
| FTP | Basic Auth | Username + Password | File transfer |
| HTTP APIs | Various | API Key / Token | Custom integrations |
OAuth2 Link Management
DBLink Schema
type DBLink struct { ID primitive.ObjectID UserID primitive.ObjectID CompanyID primitive.ObjectID IntegrationType string // "microsoft", "slack", "google", etc. AccessToken string // [encrypted] RefreshToken string // [encrypted] TokenExpiry time.Time Scopes []string ProviderUserID string // Unique ID from provider ProviderUserEmail string // Email from provider ProviderMetadata map[string]interface{} // Additional data WebhookSignatureSecret string // [encrypted] For webhook validation CreatedAt time.Time UpdatedAt time.Time}Creating OAuth2 Integration
1. User initiates connection:
GET /api/v1/integrations/authorize?service=slack
Response:{ "auth_url": "https://slack.com/oauth/v2/authorize?client_id=...&redirect_uri=..."}2. User is redirected to service (browser):
https://slack.com/oauth/v2/authorize ?client_id=123... &redirect_uri=https://app.flow8.io/auth/callback &scope=chat:write,files:write &state=abc123... (CSRF protection)3. User authorizes, service redirects back with code:
https://app.flow8.io/auth/callback ?code=xoxb-123... &state=abc1234. flow8 exchanges code for token:
func (s *IntegrationService) ExchangeOAuthCode(ctx context.Context, code string, service string) (*DBLink, error) { // Exchange code for token token, err := s.slackClient.Exchange(ctx, code)
// Decrypt, verify, store link := &DBLink{ IntegrationType: "slack", AccessToken: encrypt(token.AccessToken), RefreshToken: encrypt(token.RefreshToken), TokenExpiry: token.Expiry, ProviderUserID: token.SlackUserID, UserID: ctx.Value("user_id").(primitive.ObjectID), CompanyID: ctx.Value("company_id").(primitive.ObjectID), }
s.db.Collection("db_links").InsertOne(ctx, link) return link, nil}5. Link is stored encrypted in MongoDB:
{ "_id": ObjectId("..."), "user_id": ObjectId("..."), "company_id": ObjectId("..."), "integration_type": "slack", "access_token": { "encrypted": true, "nonce": "...", "ciphertext": "..." }, "refresh_token": { "encrypted": true, "nonce": "...", "ciphertext": "..." }, "token_expiry": ISODate("2026-07-04T10:00:00Z"), "scopes": ["chat:write", "files:write"], "provider_user_id": "U123...", "provider_user_email": "user@company.com", "created_at": ISODate("2026-04-04T10:00:00Z")}Token Refresh
Tokens are automatically refreshed before expiry:
func (s *IntegrationService) RefreshTokenIfNeeded(ctx context.Context, link *DBLink) error { // Check if token expires within 1 hour if time.Until(link.TokenExpiry) > 1*time.Hour { return nil // Token still valid }
// Refresh token newToken, err := s.slackClient.RefreshToken(ctx, decrypt(link.RefreshToken))
// Update encrypted tokens link.AccessToken = encrypt(newToken.AccessToken) link.RefreshToken = encrypt(newToken.RefreshToken) link.TokenExpiry = newToken.Expiry
s.db.Collection("db_links").UpdateByID(ctx, link.ID, link) return nil}Unlinking Integration
DELETE /api/v1/integrations/slack/:link_id
# Result:# 1. DBLink deleted# 2. Tokens revoked (best effort)# 3. Flows using this integration get access_deniedAPI Key Integrations
Storing API Keys
API keys are encrypted and stored in component configs or integration-specific documents:
Component config (for AI/Storage):
{ "_id": ObjectId("..."), "name": "aws-s3-prod", "kind": "storage", "company_id": ObjectId("..."), "config": { "provider": "s3", "access_key_id": { "encrypted": true, "nonce": "...", "ciphertext": "..." }, "secret_access_key": { "encrypted": true, "nonce": "...", "ciphertext": "..." } }}REST API (creation):
POST /api/v1/admin/components{ "name": "aws-s3-prod", "kind": "storage", "config": { "provider": "s3", "bucket": "flow8-artifacts", "access_key_id": "AKIAIOSFODNN7EXAMPLE", "secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" }}
# Response (no credentials in response):{ "id": "component_123", "name": "aws-s3-prod", "kind": "storage", "config_summary": { "provider": "s3", "bucket": "flow8-artifacts", "region": "us-east-1" }, "is_default": false}Using API Keys in Modules
Modules receive decrypted credentials at runtime:
func (m *HTTPRequestModule) Execute(params Params) Response { // Get component config component := params.GetComponent("request")
// Credentials are already decrypted by framework timeout := component.Config["timeout_seconds"] maxRetries := component.Config["max_retries"]
// Make request with decrypted creds // ...}Specific Integration Guides
Microsoft Graph (Office 365)
Setup:
oauth2: microsoft: client_id: "550e8400-e29b-41d4-a716-446655440000" client_secret: "[encrypted]" redirect_uri: "https://app.flow8.io/auth/callback" scopes: - openid - profile - email - Mail.Send - Calendars.ReadWrite auto_create_users: trueCapabilities:
- Send email via Outlook
- Read mail (delta query for incremental sync)
- Create calendar events
- List contacts
- Read OneDrive files
Module integration:
{ "name": "send-meeting-invite", "module_ref": "microsoft-calendar", "input_mapping": { "link_id": "link_123", "event_title": "flow8 Demo", "attendees": ["user@company.com"], "start_time": "2026-04-05T10:00:00Z", "end_time": "2026-04-05T11:00:00Z" }}AWS S3
Configuration:
{ "name": "s3-prod", "kind": "storage", "config": { "provider": "s3", "bucket": "flow8-artifacts", "region": "us-east-1", "access_key_id": "[encrypted]", "secret_access_key": "[encrypted]" }}Usage:
{ "name": "upload-document", "module_ref": "s3-upload", "input_mapping": { "bucket": "flow8-artifacts", "key": "invoices/2026-04/invoice-001.pdf", "file_path": "/tmp/invoice.pdf", "acl": "private" }}Slack
Setup:
# 1. Create Slack App at https://api.slack.com/apps# 2. Set redirect URL: https://app.flow8.io/auth/callback# 3. Request OAuth scopes: chat:write, files:write# 4. Copy Client ID and Secret to .env# 5. User connects via UIUsage:
{ "name": "notify-slack", "module_ref": "slack-message", "input_mapping": { "link_id": "link_slack_123", "channel": "#notifications", "message": "Invoice #001 processed successfully", "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": "Invoice Processed" } } ] }}Google Drive/Sheets
Capabilities:
- Upload files to Google Drive
- Create Google Sheets
- Write data to Sheets
- Read Sheet data
- Share files with collaborators
Configuration:
# Google Cloud Console setup# - Create OAuth 2.0 credentials (Web application)# - Add redirect URI: https://app.flow8.io/auth/callback# - Scopes: drive, sheets, gmailCustom APIs
For HTTP-based APIs, use the generic HTTP Request module:
{ "name": "call-custom-api", "module_ref": "http-request", "input_mapping": { "url": "https://api.example.com/process", "method": "POST", "headers": { "Authorization": "Bearer token_123", "Content-Type": "application/json" }, "body": { "document_id": "doc_001", "action": "analyze" } }}Credential Rotation
Rotating OAuth2 Tokens
For long-lived access, manually revoke and reconnect:
# Revoke all tokens for a serviceDELETE /api/v1/integrations/links?service=slack
# User must reconnect to re-authorizeRotating API Keys
# 1. Create new component with new API keycurl -X POST http://localhost:4454/api/v1/admin/components \ -d '{"name": "aws-s3-prod-v2", "config": {"access_key_id": "new_key", ...}}'
# 2. Update flows to reference new component# 3. Revoke old API key in provider console# 4. Delete old componentIntegration Health Monitoring
Testing Connectivity
# Test OAuth2 linkcurl -X POST http://localhost:4454/api/v1/integrations/test \ -H "Authorization: Bearer $TOKEN" \ -d '{"link_id": "link_123"}'
Response:{ "status": "ok", "service": "slack", "latency_ms": 123, "token_valid": true, "scopes_granted": ["chat:write", "files:write"], "user": "U123..."}Monitoring Token Expiry
# Query links expiring within 7 daysdb.db_links.find({ token_expiry: { $gt: new Date(), $lt: new Date(Date.now() + 7*24*60*60*1000) }})Audit Logging
All integration events are audited:
{ "action_type": "integration_linked", "resource_type": "integration", "resource_id": "link_123", "before_state": {}, "after_state": { "integration_type": "slack", "provider_user_id": "U123...", "scopes": ["chat:write", "files:write"] }, "timestamp": "2026-04-04T10:00:00Z"}Security Best Practices
-
Encrypt all credentials at rest: NaCl SecretBox ensures encrypted storage
-
Use short-lived tokens: OAuth2 access tokens expire quickly; refresh tokens are stored encrypted
-
Validate API key format: Reject malformed keys before storing
-
Monitor token usage: Alert on unusual API activity
-
Rotate keys regularly: Quarterly rotation recommended
-
Revoke immediately: If key is compromised, revoke before attackers use it
-
Use scopes minimally: Request only necessary permissions (principle of least privilege)
-
Test integrations regularly: Verify tokens still work via health checks
Troubleshooting
”Invalid OAuth Code”
Error: invalid_grantSolution:
- Code expires after 10 minutes
- Code can only be used once
- Ensure redirect_uri matches exactly
- Verify client_id and client_secret
”Access Denied” in Module
Error: insufficient_scopeSolution:
- Reconnect integration to request missing scopes
- Verify user authorized all required permissions
- Check token expiry (may need refresh)
“Token Revoked”
Error: token_revokedSolution:
- User revoked token in provider (e.g., Slack, Google settings)
- Admin revoked token in flow8
- User must reconnect
Rate Limit from External Service
Error: rate_limit_exceeded (Slack API)Solution:
- Implement exponential backoff in module
- Reduce concurrent executions
- Contact provider for higher limits
- Use batching APIs where available