Third-Party Integrations Documentation
Overview
This document describes all third-party services and integrations used in the Howden Backend (be-howden). These integrations provide essential functionality including email delivery, file storage, authentication, and database services.
Table of Contents
Email Services
Mandrill (Mailchimp Transactional)
Purpose: Primary transactional email service for sending templated emails including OTP codes, welcome emails, enrollment confirmations, and membership notifications.
Package: @mailchimp/mailchimp_transactional
Location: src/mail/template/mandrillTemplate/
Configuration
MANDRILL_API_KEY=md-YourApiKeyHereClient Setup:
// src/mail/template/mandrillTemplate/mandrillClient.ts
import Mailchimp from '@mailchimp/mailchimp_transactional';
const mailchimpTransactionalApiKey = process.env.MANDRILL_API_KEY;
if (!mailchimpTransactionalApiKey) {
throw new Error('Missing MANDRILL_API_KEY environment variable');
}
export const mailchimpClient = Mailchimp(mailchimpTransactionalApiKey);Usage Patterns
Sending Templated Emails:
import { MessagesSendTemplateRequest } from '@mailchimp/mailchimp_transactional';
import { mailchimpClient } from './mandrillClient';
export const sendTemplatedEmail = async (
recipientEmail: string,
templateName: string,
mergeVars: Array<{ name: string; content: string }>
): Promise<void> => {
const response = await mailchimpClient.messages.sendTemplate({
template_name: templateName,
template_content: [],
message: {
to: [{ email: recipientEmail }],
merge: true,
merge_vars: [
{
rcpt: recipientEmail,
vars: mergeVars,
},
],
},
} as MessagesSendTemplateRequest);
console.log('Email sent:', response);
};Example: OTP Request Email:
// src/mail/template/mandrillTemplate/otpRequest.ts
export const otpLoginRequest = async (
recipientEmail: string,
otp_code: string,
fullName: string,
): Promise<any> => {
const response = await mailchimpClient.messages.sendTemplate({
template_name: 'send-otp-code',
template_content: [],
message: {
to: [{ email: recipientEmail }],
merge: true,
merge_vars: [
{
rcpt: recipientEmail,
vars: [
{ name: 'USERNAME', content: fullName },
{ name: 'OTP_CODE', content: otp_code },
{ name: 'ENVIRONMENT', content: isDevelopment.toString() },
],
},
],
},
} as MessagesSendTemplateRequest);
return response;
};Available Email Templates
| Template File | Purpose | Mandrill Template Name |
|---|---|---|
otpRequest.ts | OTP authentication | send-otp-code |
welcomeHR.ts | Welcome new HR users | Custom template |
thankyouEmail.ts | Enrollment thank you | thank-you-email |
beneficiaryThankyouEmail.ts | Beneficiary thank you | Custom template |
resetPassword.ts | Password reset | reset-password |
principalResetPassword.ts | Principal password reset | Custom template |
successFulEndorsement.ts | Endorsement success | Custom template |
maxicare*.ts | Maxicare-specific notifications | Various |
Icare/*.ts | Icare-specific notifications | Various |
Intellicare/*.ts | Intellicare notifications | Various |
Total Templates: 45+ email templates across all insurance providers
Best Practices
- Always use merge variables for dynamic content
- Handle errors gracefully - wrap in try-catch
- Log email responses for debugging
- Use environment-specific templates when needed
- Test templates in Mandrill dashboard before deployment
Nodemailer with SMTP
Purpose: Secondary/fallback email service using SMTP (primarily Gmail for development/testing).
Package: nodemailer
Location: src/mail/mailer.ts
Configuration
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_AUTH_USER=your-email@gmail.com
SMTP_AUTH_PASS=your-app-password
SMTP_SECURE=falseSetup:
// src/mail/mailer.ts
import nodemailer from 'nodemailer';
export const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT),
secure: !!process.env.SMTP_SECURE,
auth: {
user: process.env.SMTP_AUTH_USER,
pass: process.env.SMTP_AUTH_PASS,
},
});Usage
import { transporter } from './mailer';
const sendEmail = async () => {
const info = await transporter.sendMail({
from: '"Howden" <noreply@howden.com>',
to: 'user@example.com',
subject: 'Subject',
text: 'Plain text body',
html: '<b>HTML body</b>',
});
console.log('Message sent:', info.messageId);
};When to Use
- Development: Quick testing without Mandrill setup
- Fallback: When Mandrill is unavailable
- Simple emails: Non-templated one-off emails
Cloud Storage
AWS S3
Purpose: File and image storage for uploads, documents, and assets.
Packages: @aws-sdk/client-s3, @aws-sdk/s3-request-presigner
Location: src/utils/uploadImage.ts, src/utils/uploadFile.ts
Configuration
AWS configuration is managed through the @howden/aws and @howden/common packages:
# AWS credentials configured via IAM roles or env vars
AWS_BUCKET_NAME=howden-bucket-nameS3 Client Setup:
// Using @howden/aws package
import { S3Client } from '@howden/aws';
import { AWS_BUCKET_NAME } from '@howden/common';Image Upload with Processing
// src/utils/uploadImage.ts
import { PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { S3Client } from '@howden/aws';
import sharp from 'sharp';
import cryptp from 'crypto';
export async function uploadImageToS3(
file: Express.Multer.File,
userId: string,
) {
// Generate unique filename
const imageName = `${userId}-${cryptp.randomBytes(32).toString('hex')}`;
// Resize image with Sharp
const buffer = await sharp(file.buffer)
.resize({ height: 1920, width: 1080, fit: 'contain' })
.toBuffer();
// Upload to S3
const params = {
Bucket: AWS_BUCKET_NAME,
Key: imageName,
Body: buffer,
ContentType: file.mimetype,
};
await S3Client.send(new PutObjectCommand(params));
// Generate signed URL (7 days expiration)
const url = await getSignedUrl(S3Client, new GetObjectCommand(params), {
expiresIn: 604800,
});
return url;
}File Upload (Raw)
// src/utils/uploadFile.ts
import { PutObjectCommand } from '@aws-sdk/client-s3';
import { S3Client } from '@howden/aws';
import cryptp from 'crypto';
export async function uploadFileToS3(
file: Express.Multer.File,
userId: string,
) {
const fileName = `${userId}-${cryptp.randomBytes(32).toString('hex')}`;
const params = {
Bucket: AWS_BUCKET_NAME,
Key: fileName,
Body: file.buffer,
ContentType: file.mimetype,
};
await S3Client.send(new PutObjectCommand(params));
return fileName;
}Features
- Image optimization with Sharp (resize, compression)
- Signed URLs for secure access
- Unique filenames with crypto random bytes
- User-scoped storage (filename prefix)
Authentication
Google OAuth
Purpose: Social authentication allowing users to sign in with Google accounts.
Packages: passport, passport-google-oauth, @types/passport-google-oauth
Location: src/routes/public/googleAuth.router.ts
Configuration
GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-your-client-secret
CALLBACK_URL=http://localhost:8000/api/v1/public/auth/google/callback
SESSION_SECRET=your-session-secretRouter Setup
// src/routes/public/googleAuth.router.ts
import passport from 'passport';
import { OAuth2Strategy as GoogleStrategy } from 'passport-google-oauth';
passport.use(
new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: process.env.CALLBACK_URL,
},
function (accessToken, refreshToken, profile, done) {
return done(null, profile);
},
),
);
// Routes
const googleAuthRouter = Router();
// Initiate OAuth flow
googleAuthRouter.get(
'/',
passport.authenticate('google', { scope: ['profile', 'email'] }),
);
// Callback handler
googleAuthRouter.get(
'/callback',
passport.authenticate('google', { failureRedirect: '/error' }),
(req, res) => {
res.redirect('/api/v1/public/auth/google/success');
},
);
// Success handler
googleAuthRouter.get('/success', async (req, res) => {
const data = await socialAuthRepository.registerWithGoogle(userProfile);
res
.cookie('howdenjwt', data.refreshToken, {
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000,
})
.header('Authorization', data.accessToken)
.json({
accessToken: data.accessToken,
refreshToken: data.refreshToken,
});
});Service Integration
// src/services/soicalAuth.service.ts
export class SocialAuthService {
public async registerWithGoogle(oauthUser: SocialUserProfile) {
// Check if user exists
const isUserExists = await socialRepository.findOne({
id: oauthUser.id,
provider: oauthUser.provider,
});
if (isUserExists) {
return this.processSelectedUser(oauthUser);
}
// Register new user with transaction
const queryRunner = AppDataSource.createQueryRunner();
await queryRunner.startTransaction();
try {
// Create user entity
const user = new User();
user.email = oauthUser.id;
user.firstName = oauthUser.name.familyName;
user.lastName = oauthUser.name?.givenName || '-';
user.profilePicture = oauthUser.photos[0].value || '-';
await queryRunner.manager.save(User, user);
// Create social login record
const socialLogin = new Social();
socialLogin.accountId = oauthUser.id;
socialLogin.provider = oauthUser.provider;
socialLogin.email = oauthUser.emails[0].value;
socialLogin.user = user;
await queryRunner.manager.save(Social, socialLogin);
await queryRunner.commitTransaction();
} catch (err) {
await queryRunner.rollbackTransaction();
return { success: false, error: 'Transaction failed.' };
}
return this.processSelectedUser(oauthUser);
}
private async processSelectedUser(oauthUser: SocialUserProfile) {
// Generate JWT tokens
const accessToken = jwt.sign(
{ id: user.userUuid, email: user.email },
process.env.JWT_SECRET_TOKEN,
{ expiresIn: '1h' },
);
const refreshToken = jwt.sign(
{ id: user.userUuid, email: user.email },
process.env.JWT_REFRESH_SECRET_TOKEN,
{ expiresIn: '1d' },
);
return { accessToken, refreshToken, statusCode: 201, success: true };
}
}Setup Instructions
-
Create Google Cloud Project:
- Go to Google Cloud Console
- Create new project or select existing
-
Enable Google+ API:
- Navigate to “APIs & Services” > “Library”
- Search and enable “Google+ API”
-
Create OAuth Credentials:
- Go to “Credentials” > “Create Credentials” > “OAuth client ID”
- Configure consent screen
- Select “Web application”
- Add authorized redirect URI:
http://localhost:8000/api/v1/public/auth/google/callback
-
Configure Environment:
GOOGLE_CLIENT_ID=your-client-id GOOGLE_CLIENT_SECRET=your-client-secret CALLBACK_URL=http://localhost:8000/api/v1/public/auth/google/callback
Database
PostgreSQL with TypeORM
Purpose: Primary relational database for all application data.
Packages: typeorm, pg, reflect-metadata
Location: src/data-source.ts
Configuration
# Development/Staging
POSTGRES_DB=howdenaces
POSTGRES_PORT=5432
POSTGRES_HOST=dpg-xxx.oregon-postgres.render.com
POSTGRES_USERNAME=howdenaces
POSTGRES_PASSWORD=your-password
POSTGRES_SYNC=false
POSTGRES_SSL=true
# Or local development via Docker
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USERNAME=thirdsugian
POSTGRES_PASSWORD=
POSTGRES_SYNC=true
POSTGRES_SSL=falseData Source Setup
// src/data-source.ts
import { DataSource } from 'typeorm';
export const AppDataSource = new DataSource({
type: 'postgres',
host: process.env.POSTGRES_HOST,
port: Number(process.env.POSTGRES_PORT),
username: process.env.POSTGRES_USERNAME,
password: process.env.POSTGRES_PASSWORD,
database: process.env.POSTGRES_DB,
synchronize: !!process.env.POSTGRES_SYNC, // Auto-migrate (dev only!)
logging: !!process.env.POSTGRES_LOGGING,
ssl: !!process.env.POSTGRES_SSL,
entities: ['build/entities/*.js', 'build/entities/**/*.js'],
migrations: ['build/migrations/*.js'],
subscribers: ['build/subscriber/**/*.js'],
});Initialization
// In your main server file (index.ts/app.ts)
import { AppDataSource } from './data-source';
AppDataSource.initialize()
.then(() => {
console.log('Database connected successfully');
})
.catch((error) => {
console.error('Database connection failed:', error);
});Usage in Services
import { AppDataSource } from '../data-source';
import { User } from '../entities';
// Repository pattern
const userRepository = AppDataSource.getRepository(User);
// Query with relations
const user = await userRepository.findOne({
where: { id: userId },
relations: ['company', 'insuranceProvider'],
});
// Raw query when needed
const results = await AppDataSource.query(`
SELECT * FROM users
WHERE created_at > $1
`, [date]);
// Transactions
const queryRunner = AppDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
await queryRunner.manager.save(User, newUser);
await queryRunner.manager.save(Profile, newProfile);
await queryRunner.commitTransaction();
} catch (err) {
await queryRunner.rollbackTransaction();
} finally {
await queryRunner.release();
}Entity Structure
Entities are located in src/entities/:
// Example entity
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
userUuid: string;
@Column({ unique: true })
email: string;
@Column()
firstName: string;
@Column()
lastName: string;
@ManyToOne(() => Company, company => company.users)
company: Company;
}Development Infrastructure
Docker
Purpose: Local development environment with PostgreSQL and pgAdmin.
File: docker-compose.yml
Services
| Service | Image | Ports | Purpose |
|---|---|---|---|
postgres | postgres:14.5 | 5550:5432 | PostgreSQL database |
pg-admin | dpage/pgadmin4:6.10 | 8082:80 | Database management UI |
Configuration
version: '3.8'
services:
postgres:
image: 'postgres:14.5'
ports:
- '5550:5432'
environment:
POSTGRES_USER: 'howden'
POSTGRES_PASSWORD: ''
POSTGRES_DB: 'behowden'
pg-admin:
image: dpage/pgadmin4:6.10
volumes:
- pgadmin_volume:/var/lib/pgadmin
ports:
- 8082:80
environment:
PGADMIN_DEFAULT_EMAIL: admin@admin.com
PGADMIN_DEFAULT_PASSWORD:
volumes:
pgadmin_volume:Usage
# Start services
docker-compose up -d
# Stop services
docker-compose down
# View logs
docker-compose logs -f postgresAccess
-
PostgreSQL:
localhost:5550- Database:
behowden - Username:
howden - Password:
howden@2023!
- Database:
-
pgAdmin: http://localhost:8082
- Email:
admin@admin.com - Password:
admin
- Email:
Environment Variables Summary
Required Variables
| Variable | Service | Required | Description |
|---|---|---|---|
MANDRILL_API_KEY | Mandrill | Yes (if using Mandrill) | Mailchimp Transactional API key |
SMTP_HOST | Nodemailer | No | SMTP server hostname |
SMTP_PORT | Nodemailer | No | SMTP server port |
SMTP_AUTH_USER | Nodemailer | No | SMTP username |
SMTP_AUTH_PASS | Nodemailer | No | SMTP password |
GOOGLE_CLIENT_ID | Google OAuth | No | Google OAuth client ID |
GOOGLE_CLIENT_SECRET | Google OAuth | No | Google OAuth client secret |
CALLBACK_URL | Google OAuth | No | OAuth callback URL |
POSTGRES_HOST | PostgreSQL | Yes | Database host |
POSTGRES_PORT | PostgreSQL | Yes | Database port |
POSTGRES_USERNAME | PostgreSQL | Yes | Database username |
POSTGRES_PASSWORD | PostgreSQL | Yes | Database password |
POSTGRES_DB | PostgreSQL | Yes | Database name |
JWT_SECRET_TOKEN | Authentication | Yes | JWT signing secret |
JWT_REFRESH_SECRET_TOKEN | Authentication | Yes | JWT refresh token secret |
AWS_BUCKET_NAME | AWS S3 | No | S3 bucket name |
Development .env Example
# Environment
NODE_ENV=development
# Email - Mandrill (Primary)
MANDRILL_API_KEY=md-YourApiKeyHere
# Email - SMTP (Fallback/Development)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_AUTH_USER=your-email@gmail.com
SMTP_AUTH_PASS=your-app-password
SMTP_SECURE=false
# Authentication
JWT_SECRET_TOKEN=your-jwt-secret
JWT_REFRESH_SECRET_TOKEN=your-refresh-secret
SESSION_SECRET=your-session-secret
# Google OAuth
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-your-secret
CALLBACK_URL=http://localhost:8000/api/v1/public/auth/google/callback
# Database - Local Docker
POSTGRES_HOST=localhost
POSTGRES_PORT=5550
POSTGRES_USERNAME=howden
POSTGRES_PASSWORD=
POSTGRES_DB=behowden
POSTGRES_SYNC=true
POSTGRES_SSL=false
# AWS S3
AWS_BUCKET_NAME=howden-dev-bucketTroubleshooting
Mandrill Email Not Sending
-
Check API key validity:
console.log('Mandrill key:', process.env.MANDRILL_API_KEY?.slice(0, 8)); -
Verify template exists in Mandrill dashboard
-
Check merge variable names match template exactly
-
Review Mandrill logs for delivery status
Google OAuth Errors
- Verify redirect URI matches exactly in Google Console
- Check client ID/secret are correct
- Enable Google+ API in Google Cloud Console
- Test in incognito to avoid cached credentials
Database Connection Issues
-
Check if container is running:
docker-compose ps -
Verify connection string:
psql postgresql://howden:howden@2023!@localhost:5550/behowden -
Check logs:
docker-compose logs postgres