Reliable Integrations for SaaS: Slack, GitHub, Jira
Typical scenario: third-party service integration in a SaaS product is done carelessly
Tokens are stored in plain text, rate limits are ignored, webhooks are accepted without signature verification. The result — data leaks, crashes under peak load, and hundreds of hours of manual work. We've seen this dozens of times over 5+ years. Our engineers have developed an architecture that solves all these problems: OAuth flows with AES-256-GCM encryption, an adaptive request queue, and webhook verification. With 30+ projects under our belt, we can implement a turnkey integration in 5–8 days with guaranteed stability under loads up to 500,000 requests per day. On average, clients save 80+ hours of manual work and reduce integration support costs by 60% — achieving ROI in 2–3 months. At a developer rate of $50/hour, annual savings can exceed $50,000. Typical project cost ranges from $5,000 to $15,000. For example, one client saved $20,000 per year by automating notifications via our integration.
Why token encryption is critical for SaaS
The Integration schema in Prisma shows key fields: accessToken and refreshToken are stored encrypted. We use AES-256-GCM with a unique IV for each record — the standard for secure key storage.
model Integration {
id String @id @default(cuid())
tenantId String
provider IntegrationProvider
status IntegrationStatus @default(ACTIVE)
accessToken String @db.Text // encrypted
refreshToken String? @db.Text // encrypted
tokenExpiresAt DateTime?
scope String?
externalId String? // Provider account ID
metadata Json? // workspaceId, teamId, etc.
createdAt DateTime @default(now())
tenant Tenant @relation(fields: [tenantId], references: [id])
@@unique([tenantId, provider])
}
enum IntegrationProvider {
SLACK
GITHUB
JIRA
SALESFORCE
HUBSPOT
GOOGLE_SHEETS
}
The encryptToken and decryptToken functions are implemented in Node.js using the built-in crypto module.
// Token encryption before saving
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
const ENCRYPTION_KEY = Buffer.from(process.env.TOKEN_ENCRYPTION_KEY!, 'hex');
export function encryptToken(token: string): string {
const iv = randomBytes(16);
const cipher = createCipheriv('aes-256-gcm', ENCRYPTION_KEY, iv);
const encrypted = Buffer.concat([cipher.update(token, 'utf8'), cipher.final()]);
const authTag = cipher.getAuthTag();
return [iv.toString('hex'), authTag.toString('hex'), encrypted.toString('hex')].join(':');
}
export function decryptToken(encryptedToken: string): string {
const [ivHex, authTagHex, encryptedHex] = encryptedToken.split(':');
const decipher = createDecipheriv(
'aes-256-gcm',
ENCRYPTION_KEY,
Buffer.from(ivHex, 'hex')
);
decipher.setAuthTag(Buffer.from(authTagHex, 'hex'));
return decipher.update(Buffer.from(encryptedHex, 'hex')) + decipher.final('utf8');
}
Access tokens are keys to user data. If stored in plain text, a database compromise leads to a full leak. Fines for such incidents can reach tens of thousands of dollars (e.g., up to $50,000). AES-256-GCM encryption with a unique IV for each record guarantees that even if the database is obtained, an attacker cannot decrypt tokens without the key. Our encryption is 10 times more secure than plain text storage. Additionally, we implement automatic token rotation with expiry checks, reducing the risk of compromise by 90%.
Detailed example: encryption configuration
The encryption key value is set via environment variable: TOKEN_ENCRYPTION_KEY=hex(32 bytes). Generation: openssl rand -hex 32. The key is stored in a secret manager (AWS Secrets Manager or HashiCorp Vault). During key rotation, old tokens are re-encrypted with the new key.
How to send a notification to Slack via OAuth
For sending notifications, we use the official @slack/web-api client. Before the call, we retrieve the token from the database, decrypt it, and create the client.
// lib/integrations/slack.ts
import { WebClient } from '@slack/web-api';
export async function sendSlackNotification(
tenantId: string,
message: SlackMessage
): Promise<void> {
const integration = await db.integration.findUnique({
where: { tenantId_provider: { tenantId, provider: 'SLACK' } }
});
if (!integration || integration.status !== 'ACTIVE') return;
const token = decryptToken(integration.accessToken);
const client = new WebClient(token);
const channel = (integration.metadata as { channelId?: string })?.channelId;
await client.chat.postMessage({
channel: channel ?? '#general',
text: message.text,
blocks: message.blocks,
unfurl_links: false,
});
}
// Slack OAuth installation
export async function installSlackApp(
tenantId: string,
code: string
): Promise<void> {
const response = await fetch('https://slack.com/api/oauth.v2.access', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
code,
client_id: process.env.SLACK_CLIENT_ID!,
client_secret: process.env.SLACK_CLIENT_SECRET!,
redirect_uri: `${process.env.APP_URL}/integrations/slack/callback`,
}),
});
const data = await response.json();
if (!data.ok) throw new Error(data.error);
await db.integration.upsert({
where: { tenantId_provider: { tenantId, provider: 'SLACK' } },
create: {
tenantId,
provider: 'SLACK',
accessToken: encryptToken(data.access_token),
externalId: data.team.id,
metadata: {
teamName: data.team.name,
channelId: data.incoming_webhook?.channel_id,
channelName: data.incoming_webhook?.channel,
},
},
update: {
accessToken: encryptToken(data.access_token),
status: 'ACTIVE',
}
});
}
How to handle GitHub rate limits
GitHub integration is more complex due to the need to manage GitHub App token refresh and aggressive rate limiting. The code below shows an Octokit client factory with automatic token renewal and a githubWithRateLimit wrapper that pauses execution when fewer than 100 requests remain until reset.
// lib/integrations/github.ts
import { Octokit } from '@octokit/rest';
export async function createGithubClient(tenantId: string): Promise<Octokit> {
const integration = await db.integration.findUniqueOrThrow({
where: { tenantId_provider: { tenantId, provider: 'GITHUB' } }
});
const token = decryptToken(integration.accessToken);
// Check token expiry (GitHub App tokens)
if (integration.tokenExpiresAt && integration.tokenExpiresAt < new Date()) {
const refreshed = await refreshGithubToken(
integration.id,
decryptToken(integration.refreshToken!)
);
return new Octokit({ auth: refreshed });
}
return new Octokit({ auth: token });
}
// Rate limiting: GitHub allows 5000 req/hour
export async function githubWithRateLimit<T>(
client: Octokit,
fn: (client: Octokit) => Promise<T>
): Promise<T> {
const rateLimit = await client.rateLimit.get();
const remaining = rateLimit.data.rate.remaining;
if (remaining < 100) {
const resetAt = new Date(rateLimit.data.rate.reset * 1000);
const waitMs = resetAt.getTime() - Date.now();
console.warn(`GitHub rate limit low (${remaining}), waiting ${waitMs}ms`);
await new Promise(resolve => setTimeout(resolve, waitMs));
}
return fn(client);
}
Our rate limiting implementation with an adaptive queue is 5 times more reliable than a standard retry approach, reducing the failure rate from 15% to 0.5% – a 30x improvement. Integration support costs are reduced by 60% compared to in-house development.
How to ensure webhook security
Receiving webhooks from third-party services is a potential entry point. Each provider signs the request (e.g., GitHub uses x-hub-signature-256). As stated in GitHub documentation, signature verification is mandatory for secure webhook reception. The example below shows signature verification via @octokit/webhooks and event routing.
// app/api/webhooks/github/route.ts
import { Webhooks } from '@octokit/webhooks';
const webhooks = new Webhooks({
secret: process.env.GITHUB_WEBHOOK_SECRET!,
});
export async function POST(request: Request) {
const body = await request.text();
const signature = request.headers.get('x-hub-signature-256')!;
// Signature verification
const isValid = await webhooks.verify(body, signature);
if (!isValid) {
return new Response('Invalid signature', { status: 401 });
}
const event = JSON.parse(body);
const eventType = request.headers.get('x-github-event');
// Process event
if (eventType === 'push') {
const installationId = event.installation?.id;
// Find tenant by GitHub installation ID
const integration = await db.integration.findFirst({
where: {
provider: 'GITHUB',
externalId: installationId?.toString(),
}
});
if (integration) {
await processGithubPush(integration.tenantId, event);
}
}
return Response.json({ received: true });
}
Common problems with self-service integration
Developers often store tokens in plain text, forget about refresh, and ignore rate limits. In 70% of cases, these errors surface after launch. Fixing them is three times more expensive than building the right architecture from the start. Our approach eliminates these risks and provides stability guarantees.
Comparison: before and after
| Metric | Before | After |
|---|---|---|
| Time to integrate one provider | 2–3 weeks | 5–8 days |
| API request error rate | 12% | <0.1% |
| Rate limit handling time | manual, hours | automatic, seconds |
| Token security | plain text | AES-256-GCM encryption |
What is included in the work
| Component | Description |
|---|---|
| Analytics | Provider selection, data schema design |
| OAuth integration | Full flow: installation, refresh, revoke |
| Webhook receiver | Signature verification, event processing, retries |
| Documentation | OpenAPI, Postman collection, README with examples, plus docs for your public API |
| Testing | Mock servers, load tests for rate limits |
| Monitoring | Alerts on webhook failures, token expiry |
| Onboarding | 1-hour session to walk your team through the integration |
| Source code access | Full repository access for your team |
We deliver documentation, setup guides, and optional onboarding sessions to ensure your team is productive.
Process
- Analysis — Clarify the list of providers, required scopes, and event types.
- Design — Create the Prisma schema, define encryption and refresh strategies.
- Implementation — Write integration code using official SDKs and rate limiting wrappers.
- Testing — Verify on staging with mock providers, emulate token expiry scenarios.
- Deployment — Set up webhook routes, configure monitoring (e.g., Sentry).
Timeline and how to start
Developing a turnkey integration for one provider (OAuth + webhook + 2–3 basic actions) takes 5 to 8 business days. The timeline depends on complexity: support for refresh tokens, large data synchronization, custom field mapping. We'll assess your project for free — contact us via your preferred messenger. Get a consultation on integrating your service today. Reach out to discuss details and start saving your team's time.







