The LFX Changelog integrates with Slack so users can share published changelog entries directly to Slack channels. The integration uses OAuth 2.0 with token rotation and encrypts all tokens at rest using AES-256-GCM.
| Component | Description |
|---|---|
| OAuth flow | User-initiated Slack workspace connection with CSRF-protected state |
| Token storage | AES-256-GCM encrypted tokens with automatic refresh before expiry |
| Posting | Rich BlockKit messages posted as the connected user (not a bot) |
| Notifications | Delivery tracking per changelog per channel |
Users connect their Slack workspace from the admin settings:
- Click Connect Slack in the admin UI
- Redirected to Slack's OAuth authorize page
- Select a workspace and approve the requested permissions
- Redirected back to the app with an authorization code
- Server exchanges the code for access + refresh tokens
- Tokens are encrypted and stored in the database
After connecting, any published changelog can be shared to Slack:
- Open a published changelog entry
- Click Post to Slack
- Select a workspace and channel from the dialog
- Click Post --- the changelog is sent as a rich BlockKit message
The integration requests user token scopes (not bot scopes):
| Scope | Purpose |
|---|---|
chat:write |
Post messages to channels the user is a member of |
channels:read |
List public channels for the channel picker |
groups:read |
List private channels the user is a member of |
Messages are posted as the user, not as a bot. This means the message appears under the user's name in Slack.
The OAuth state parameter is signed with HMAC-SHA256 using WEBHOOK_STATE_SECRET:
- A JSON payload is created containing the user ID and a timestamp:
{ userId, ts } - The payload is serialized to a JSON string
- An HMAC-SHA256 hex digest is computed over the JSON string
- Both are wrapped into
{ d: <json-string>, s: <hmac-hex> }and base64url-encoded
On callback, the server decodes the state, recomputes the HMAC over d, and compares with s using crypto.timingSafeEqual(). States older than 10 minutes are rejected.
Slack's token rotation is enabled, providing short-lived access tokens with automatic refresh:
| Token | Lifetime | Storage |
|---|---|---|
| Access token | 24 hours | AES-256-GCM encrypted |
| Refresh token | Long-lived | AES-256-GCM encrypted |
The server automatically refreshes tokens 5 minutes before expiration on every API call. If a refresh fails (token revoked, account inactive), the integration is marked as revoked and the user must reconnect.
Tokens are encrypted at rest using AES-256-GCM:
- Algorithm: AES-256-GCM (authenticated encryption)
- Key: 32-byte key from
SLACK_TOKEN_ENCRYPTION_KEY(64 hex characters) - IV: Random 12 bytes per encryption
- Storage format:
base64url(iv):base64url(authTag):base64url(ciphertext)
This ensures that even if the database is compromised, Slack tokens cannot be used without the encryption key.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/slack/connect |
OAuth only | Generate Slack OAuth URL |
| GET | /api/slack/integrations |
OAuth only | List user's connected workspaces |
| GET | /api/slack/integrations/:id/channels |
OAuth only | Fetch channels for a workspace |
| POST | /api/slack/integrations/:id/channels |
OAuth only | Save default channel |
| DELETE | /api/slack/integrations/:id |
OAuth only | Disconnect a workspace |
| POST | /api/changelogs/:id/share/slack |
OAuth only (editor on product) | Post changelog to a channel |
| GET | /webhooks/slack-callback |
None | OAuth callback handler |
All Slack endpoints are OAuth-only --- API keys cannot be used.
Changelog entries are posted as Slack BlockKit messages with the following structure:
┌──────────────────────────────────────────────┐
│ 📋 New Changelog: Feature Title │ ← Header
├──────────────────────────────────────────────┤
│ Product: Organization Dashboard │ ← Fields section
│ Version: 2.1.0 │
├──────────────────────────────────────────────┤
│ Description preview text (up to 500 chars) │ ← Description section
│ truncated if longer... │
├──────────────────────────────────────────────┤
│ [ View Full Changelog ] │ ← Action button (links to entry)
├──────────────────────────────────────────────┤
│ 👤 John Doe · Posted via LFX Changelog │ ← Context footer
└──────────────────────────────────────────────┘
The "View Full Changelog" button links to the entry's public URL using its slug (or ID if no slug is set).
The Slack integration uses three database tables (see the Prisma schema for exact field definitions):
-
SlackIntegration --- stores the connection between a user and a Slack workspace. Each record holds the encrypted access and refresh tokens, token expiry, granted scopes, and status (
activeorrevoked). Unique per user + workspace combination. -
SlackChannel --- tracks which channels a user has configured for posting within a workspace. One channel per workspace can be marked as the default. Unique per integration + channel ID combination.
-
SlackNotification --- tracks delivery of each changelog post to a channel. Records the Slack message timestamp (for future updates), delivery status (
sentorfailed), and any error message. Unique per channel + changelog entry combination to prevent duplicate posts.
| Scenario | Behavior |
|---|---|
| Token expired | Auto-refreshed 5 minutes before expiry |
| Refresh token revoked | Integration marked as revoked, user must reconnect |
| Invalid auth | Integration marked as revoked |
| Account inactive | Integration marked as revoked |
| Slack API rate limited | Auto-retry up to 3 times with exponential backoff |
| Post fails | Notification status set to failed with error message |
| OAuth state expired | Callback rejected, user must restart the OAuth flow |
| Variable | Required | Description |
|---|---|---|
SLACK_CLIENT_ID |
Yes | Slack app OAuth client ID |
SLACK_CLIENT_SECRET |
Yes | Slack app OAuth client secret |
SLACK_TOKEN_ENCRYPTION_KEY |
Yes | AES-256-GCM encryption key (64 hex chars = 32 bytes) |
WEBHOOK_STATE_SECRET |
Yes | HMAC-SHA256 secret for OAuth state CSRF protection |
apps/lfx-changelog/src/server/
├── controllers/
│ └── slack.controller.ts # HTTP handlers for Slack endpoints
├── services/
│ └── slack.service.ts # OAuth flow, token encryption, posting
├── routes/
│ └── slack.route.ts # /api/slack/* route definitions
apps/lfx-changelog/src/app/
└── shared/
├── services/slack/
│ └── slack.service.ts # Angular HTTP wrapper
└── components/
└── post-to-slack-dialog/ # Channel picker + post dialog