Frigg Core
The @friggframework/core package is the foundational layer of the Frigg Framework, implementing a hexagonal architecture pattern for building scalable, maintainable enterprise integrations. It provides the essential building blocks, domain logic, and infrastructure components that power the entire Frigg ecosystem.
Table of Contents
Architecture Overview
Frigg Core implements a hexagonal architecture (also known as ports and adapters) that separates business logic from external concerns:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Inbound Adapters β
β βββββββββββββββ βββββββββββββββ βββββββββββββββ β
β β Express β β Lambda β β WebSocket β β
β β Routes β β Handlers β β Handlers β β
β βββββββββββββββ βββββββββββββββ βββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Application Layer β
β βββββββββββββββ βββββββββββββββ βββββββββββββββ β
β β Use Cases β β Services β β Coordinatorsβ β
β β (Business β β β β β β
β β Logic) β β β β β β
β βββββββββββββββ βββββββββββββββ βββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Domain Layer β
β βββββββββββββββ βββββββββββββββ βββββββββββββββ β
β β Integration β β Entities β β Value β β
β β Aggregates β β β β Objects β β
β βββββββββββββββ βββββββββββββββ βββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Outbound Adapters β
β βββββββββββββββ βββββββββββββββ βββββββββββββββ β
β β Database β β API Modules β β Event β β
β β Repositoriesβ β β β Publishers β β
β βββββββββββββββ βββββββββββββββ βββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Installation
npm install @friggframework/core
yarn add @friggframework/core
Prisma Support (Optional)
@friggframework/core supports both MongoDB and PostgreSQL via Prisma ORM. Prisma is an optional peer dependency - you only need to install it if you're using database features that require migrations or schema generation.
When you need Prisma:
- Running database migrations (
prisma migrate, prisma db push)
- Generating Prisma clients for your application
- Using the migration Lambda function (
dbMigrate)
Installation:
npm install --save-dev prisma @prisma/client
yarn add -D prisma @prisma/client
Generate Prisma Clients:
npm run prisma:generate:mongo
npm run prisma:generate:postgres
npm run prisma:generate
Note: The published npm package includes pre-generated Prisma clients, so you don't need to install Prisma just to use @friggframework/core in production. Prisma is only required if you're actively developing migrations or running the migration Lambda function.
Prerequisites
- Node.js 16+
- MongoDB 4.4+ (for data persistence)
- AWS credentials (for SQS, KMS, Lambda deployment)
Environment Variables
MONGO_URI=mongodb://localhost:27017/frigg
FRIGG_ENCRYPTION_KEY=your-256-bit-encryption-key
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
DEBUG=frigg:*
LOG_LEVEL=info
Core Components
1. Integrations (/integrations)
The heart of the framework - manages integration lifecycle and business logic.
Key Classes:
IntegrationBase - Base class for all integrations
Integration - Domain aggregate using Proxy pattern
- Use cases:
CreateIntegration, UpdateIntegration, DeleteIntegration
Usage:
const { IntegrationBase } = require('@friggframework/core');
class SlackHubSpotSync extends IntegrationBase {
static Definition = {
name: 'slack-hubspot-sync',
version: '2.1.0',
modules: {
slack: 'slack',
hubspot: 'hubspot'
}
};
async onCreate({ integrationId }) {
await this.slack.createWebhook(process.env.WEBHOOK_URL);
await this.hubspot.setupContactSync();
await super.onCreate({ integrationId });
}
}
3. Database (/database)
MongoDB integration with Mongoose ODM.
Key Components:
- Connection management
- Pre-built models (User, Integration, Credential, etc.)
- Schema definitions
Usage:
const {
connectToDatabase,
IntegrationModel,
UserModel
} = require('@friggframework/core');
await connectToDatabase();
const userIntegrations = await IntegrationModel.find({
userId: 'user-123',
status: 'ENABLED'
});
const user = new UserModel({
email: 'user@example.com',
name: 'John Doe'
});
await user.save();
4. Encryption (/encrypt)
AES-256-GCM encryption for sensitive data.
Usage:
const { Encrypt, Cryptor } = require('@friggframework/core');
const encrypted = Encrypt.encrypt('sensitive-data');
const decrypted = Encrypt.decrypt(encrypted);
const cryptor = new Cryptor(process.env.CUSTOM_KEY);
const secureData = cryptor.encrypt(JSON.stringify({
accessToken: 'oauth-token',
refreshToken: 'refresh-token'
}));
5. Error Handling (/errors)
Standardized error types with proper HTTP status codes.
Usage:
const {
BaseError,
RequiredPropertyError,
FetchError
} = require('@friggframework/core');
throw new RequiredPropertyError('userId is required');
throw new FetchError('Failed to fetch data from external API', {
statusCode: 404,
response: errorResponse
});
throw new BaseError('Integration failed', {
integrationId: 'int-123',
errorCode: 'SYNC_FAILED'
});
6. Logging (/logs)
Structured logging with debug capabilities.
Usage:
const { debug, initDebugLog, flushDebugLog } = require('@friggframework/core');
initDebugLog('integration:slack');
debug('Processing webhook payload', {
eventType: 'contact.created',
payload: webhookData
});
await flushDebugLog();
7. User Management (/user)
Comprehensive user authentication and authorization system supporting both individual and organizational users.
Key Classes:
User - Domain aggregate for user entities
UserRepository - Data access for user operations
- Use cases:
LoginUser, CreateIndividualUser, CreateOrganizationUser, GetUserFromBearerToken
User Types:
- Individual Users: Personal accounts with email/username authentication
- Organization Users: Business accounts with organization-level access
- Hybrid Mode: Support for both user types simultaneously
Authentication Methods:
- Password-based: Traditional username/password authentication
- Token-based: Bearer token authentication with session management
- App-based: External app user ID authentication (passwordless)
Usage:
const {
LoginUser,
CreateIndividualUser,
GetUserFromBearerToken,
UserRepository
} = require('@friggframework/core');
const userConfig = {
usePassword: true,
primary: 'individual',
individualUserRequired: true,
organizationUserRequired: false
};
const userRepository = new UserRepository({ userConfig });
const createUser = new CreateIndividualUser({ userRepository, userConfig });
const user = await createUser.execute({
email: 'user@example.com',
username: 'john_doe',
password: 'secure_password',
appUserId: 'external_user_123'
});
const loginUser = new LoginUser({ userRepository, userConfig });
const authenticatedUser = await loginUser.execute({
username: 'john_doe',
password: 'secure_password'
});
const getUserFromToken = new GetUserFromBearerToken({ userRepository, userConfig });
const user = await getUserFromToken.execute('Bearer eyJhbGciOiJIUzI1NiIs...');
console.log('User ID:', user.getId());
console.log('Primary user:', user.getPrimaryUser());
console.log('Individual user:', user.getIndividualUser());
console.log('Organization user:', user.getOrganizationUser());
8. Lambda Utilities (/lambda)
AWS Lambda-specific utilities and helpers.
Usage:
const { TimeoutCatcher } = require('@friggframework/core');
exports.handler = async (event, context) => {
const timeoutCatcher = new TimeoutCatcher(context);
try {
const result = await processIntegrationSync(event);
return { statusCode: 200, body: JSON.stringify(result) };
} catch (error) {
if (timeoutCatcher.isNearTimeout()) {
await saveProgressState(event);
return { statusCode: 202, body: 'Processing continues...' };
}
throw error;
}
};
User Management & Behavior
Frigg Core provides a flexible user management system that supports various authentication patterns and user types. The system is designed around the concept of Individual Users (personal accounts) and Organization Users (business accounts), with configurable authentication methods.
User Configuration
User behavior is configured in the app definition, allowing you to customize authentication requirements:
const appDefinition = {
integrations: [HubSpotIntegration],
user: {
usePassword: true,
primary: 'individual',
organizationUserRequired: true,
individualUserRequired: true,
}
};
User Domain Model
The User class provides a rich domain model with behavior:
const { User } = require('@friggframework/core');
const user = new User(individualUser, organizationUser, usePassword, primary);
user.getId()
user.getPrimaryUser()
user.getIndividualUser()
user.getOrganizationUser()
user.isPasswordRequired()
user.isPasswordValid(password)
user.isIndividualUserRequired()
user.isOrganizationUserRequired()
user.setIndividualUser(individualUser)
user.setOrganizationUser(organizationUser)
Database Models
The user system uses MongoDB with Mongoose for data persistence:
{
email: String,
username: { type: String, unique: true },
hashword: String,
appUserId: String,
organizationUser: ObjectId
}
{
name: String,
appOrgId: String,
domain: String,
settings: Object
}
{
user: ObjectId,
token: String,
expires: Date,
created: Date
}
Security Features
- Password Hashing: Uses bcrypt with configurable salt rounds
- Token Management: Secure session tokens with expiration
- Unique Constraints: Enforced username and email uniqueness
- External References: Support for external app user/org IDs
- Flexible Authentication: Multiple authentication methods
Hexagonal Architecture
Use Case Pattern
Each business operation is encapsulated in a use case class:
class UpdateIntegrationStatus {
constructor({ integrationRepository }) {
this.integrationRepository = integrationRepository;
}
async execute(integrationId, newStatus) {
if (!['ENABLED', 'DISABLED', 'ERROR'].includes(newStatus)) {
throw new Error('Invalid status');
}
const integration = await this.integrationRepository.findById(integrationId);
if (!integration) {
throw new Error('Integration not found');
}
integration.status = newStatus;
integration.updatedAt = new Date();
return await this.integrationRepository.save(integration);
}
}
Repository Pattern
Data access is abstracted through repositories:
class IntegrationRepository {
async findById(id) {
return await IntegrationModel.findById(id);
}
async findByUserId(userId) {
return await IntegrationModel.find({ userId, deletedAt: null });
}
async save(integration) {
return await integration.save();
}
async createIntegration(entities, userId, config) {
const integration = new IntegrationModel({
entitiesIds: entities,
userId,
config,
status: 'NEW',
createdAt: new Date()
});
return await integration.save();
}
}
Domain Aggregates
Complex business objects with behavior:
const Integration = new Proxy(class {}, {
construct(target, args) {
const [params] = args;
const instance = new params.integrationClass(params);
Object.assign(instance, {
id: params.id,
userId: params.userId,
entities: params.entities,
config: params.config,
status: params.status,
modules: params.modules
});
return instance;
}
});
Usage Examples
Real-World HubSpot Integration Example
Here's a complete, production-ready HubSpot integration that demonstrates advanced Frigg features:
const {
get,
IntegrationBase,
WebsocketConnection,
} = require('@friggframework/core');
const FriggConstants = require('../utils/constants');
const hubspot = require('@friggframework/api-module-hubspot');
const testRouter = require('../testRouter');
const extensions = require('../extensions');
class HubSpotIntegration extends IntegrationBase {
static Definition = {
name: 'hubspot',
version: '1.0.0',
supportedVersions: ['1.0.0'],
hasUserConfig: true,
display: {
label: 'HubSpot',
description: hubspot.Config.description,
category: 'Sales & CRM, Marketing',
detailsUrl: 'https://hubspot.com',
icon: hubspot.Config.logoUrl,
},
modules: {
hubspot: {
definition: hubspot.Definition,
},
},
routes: [
{
path: '/hubspot/webhooks',
method: 'POST',
event: 'HUBSPOT_WEBHOOK',
},
testRouter,
],
};
constructor() {
super();
this.events = {
HUBSPOT_WEBHOOK: {
handler: async ({ data, context }) => {
console.log('Received HubSpot webhook:', data);
const activeConnections = await WebsocketConnection.getActiveConnections();
const message = JSON.stringify({
type: 'HUBSPOT_WEBHOOK',
data,
});
activeConnections.forEach((connection) => {
connection.send(message);
});
},
},
[FriggConstants.defaultEvents.GET_SAMPLE_DATA]: {
type: FriggConstants.eventTypes.USER_ACTION,
handler: this.getSampleData,
title: 'Get Sample Data',
description: 'Get sample data from HubSpot and display in a formatted table',
userActionType: 'QUICK_ACTION',
},
GET_OBJECT_LIST: {
type: FriggConstants.eventTypes.USER_ACTION,
handler: this.getObjectList,
title: 'Get Object List',
description: 'Get list of available HubSpot objects',
userActionType: 'DATA',
},
CREATE_RECORD: {
type: FriggConstants.eventTypes.USER_ACTION,
handler: this.createRecord,
title: 'Create Record',
description: 'Create a new record in HubSpot',
userActionType: 'DATA',
},
};
this.extensions = {
hubspotWebhooks: {
extension: extensions.hubspotWebhooks,
handlers: {
WEBHOOK_EVENT: this.handleWebhookEvent,
},
},
};
}
async getSampleData({ objectName }) {
let res;
switch (objectName) {
case 'deals':
res = await this.hubspot.api.searchDeals({
properties: ['dealname,amount,closedate'],
});
break;
case 'contacts':
res = await this.hubspot.api.listContacts({
after: 0,
properties: 'firstname,lastname,email',
});
break;
case 'companies':
res = await this.hubspot.api.searchCompanies({
properties: ['name,website,email'],
limit: 100,
});
break;
default:
throw new Error(`Unsupported object type: ${objectName}`);
}
const portalId = this.hubspot.entity.externalId;
const formatted = res.results.map((item) => {
const formattedItem = {
linkToRecord: `https://app.hubspot.com/contacts/${portalId}/${objectName}/${item.id}/`,
id: item.id,
};
for (const [key, value] of Object.entries(item.properties)) {
if (value !== null && value !== undefined && value !== '') {
formattedItem[key] = value;
}
}
delete formattedItem.hs_object_id;
return formattedItem;
});
return { label: objectName, data: formatted };
}
async getObjectList() {
return [
{ key: 'deals', label: 'Deals' },
{ key: 'contacts', label: 'Contacts' },
{ key: 'companies', label: 'Companies' },
];
}
async createRecord(args) {
let res;
const objectType = args.objectType;
delete args.objectType;
switch (objectType.toLowerCase()) {
case 'deal':
res = await this.hubspot.api.createDeal({ ...args });
break;
case 'company':
res = await this.hubspot.api.createCompany({ ...args });
break;
case 'contact':
res = await this.hubspot.api.createContact({ ...args });
break;
default:
throw new Error(`Unsupported object type: ${objectType}`);
}
return { data: res };
}
async getActionOptions({ actionId, data }) {
switch (actionId) {
case 'CREATE_RECORD':
let jsonSchema = {
type: 'object',
properties: {
objectType: {
type: 'string',
title: 'Object Type',
},
},
required: [],
};
let uiSchema = {
type: 'HorizontalLayout',
elements: [
{
type: 'Control',
scope: '#/properties/objectType',
rule: { effect: 'HIDE', condition: {} },
},
],
};
switch (data.name.toLowerCase()) {
case 'deal':
jsonSchema.properties = {
...jsonSchema.properties,
dealname: { type: 'string', title: 'Deal Name' },
amount: { type: 'number', title: 'Amount' },
};
jsonSchema.required = ['dealname', 'amount'];
uiSchema.elements.push(
{ type: 'Control', scope: '#/properties/dealname' },
{ type: 'Control', scope: '#/properties/amount' }
);
break;
case 'company':
jsonSchema.properties = {
...jsonSchema.properties,
name: { type: 'string', title: 'Company Name' },
website: { type: 'string', title: 'Website URL' },
};
jsonSchema.required = ['name', 'website'];
uiSchema.elements.push(
{ type: 'Control', scope: '#/properties/name' },
{ type: 'Control', scope: '#/properties/website' }
);
break;
case 'contact':
jsonSchema.properties = {
...jsonSchema.properties,
firstname: { type: 'string', title: 'First Name' },
lastname: { type: 'string', title: 'Last Name' },
email: { type: 'string', title: 'Email Address' },
};
jsonSchema.required = ['firstname', 'lastname', 'email'];
uiSchema.elements.push(
{ type: 'Control', scope: '#/properties/firstname' },
{ type: 'Control', scope: '#/properties/lastname' },
{ type: 'Control', scope: '#/properties/email' }
);
break;
default:
throw new Error(`Unsupported object type: ${data.name}`);
}
return {
jsonSchema,
uiSchema,
data: { objectType: data.name },
};
}
return null;
}
async getConfigOptions() {
return {};
}
}
module.exports = HubSpotIntegration;
index.js
const HubSpotIntegration = require('./src/integrations/HubSpotIntegration');
const appDefinition = {
integrations: [
HubSpotIntegration,
],
user: {
usePassword: true,
primary: 'individual',
organizationUserRequired: true,
individualUserRequired: true,
}
}
module.exports = {
Definition: appDefinition,
}
Key Features Demonstrated
This real-world example showcases:
π Webhook Integration: Real-time event processing with WebSocket broadcasting
π User Actions: Interactive data operations with dynamic form generation
π― API Module Integration: Direct use of @friggframework/api-module-hubspot
π Extension System: Modular functionality through extensions
π Dynamic Forms: JSON Schema-based form generation for different object types
π Deep Linking: Direct links to HubSpot records in formatted data
β‘ Real-time Updates: WebSocket connections for live data streaming
Testing
Running Tests
npm test
npm test -- --testPathPattern="integration.test.js"
Test Structure
The core package uses a comprehensive testing approach:
describe('CreateIntegration Use-Case', () => {
let integrationRepository;
let moduleFactory;
let useCase;
beforeEach(() => {
integrationRepository = new TestIntegrationRepository();
moduleFactory = new TestModuleFactory();
useCase = new CreateIntegration({
integrationRepository,
integrationClasses: [TestIntegration],
moduleFactory
});
});
describe('happy path', () => {
it('creates an integration and returns DTO', async () => {
const result = await useCase.execute(['entity-1'], 'user-1', { type: 'test' });
expect(result.id).toBeDefined();
expect(result.status).toBe('NEW');
});
});
describe('error cases', () => {
it('throws error for unknown integration type', async () => {
await expect(useCase.execute(['entity-1'], 'user-1', { type: 'unknown' }))
.rejects.toThrow('No integration class found for type: unknown');
});
});
});
Test Doubles
The framework provides test doubles for external dependencies:
const { TestIntegrationRepository, TestModuleFactory } = require('@friggframework/core/test');
const testRepo = new TestIntegrationRepository();
testRepo.addMockIntegration({ id: 'test-123', userId: 'user-1' });
const testFactory = new TestModuleFactory();
testFactory.addMockModule('hubspot', mockHubSpotModule);
Development
Project Structure
packages/core/
βββ integrations/ # Integration domain logic
β βββ use-cases/ # Business use cases
β βββ tests/ # Integration tests
β βββ integration-base.js # Base integration class
βββ modules/ # API module system
β βββ requester/ # HTTP clients
β βββ use-cases/ # Module management
βββ database/ # Data persistence
βββ encrypt/ # Encryption utilities
βββ errors/ # Error definitions
βββ logs/ # Logging system
βββ lambda/ # Serverless utilities
Adding New Components
- Create the component: Follow the established patterns
- Add tests: Comprehensive test coverage required
- Export from index.js: Make it available to consumers
- Update documentation: Keep README current
Code Style
npm run lint:fix
npm run lint
API Reference
Core Exports
const {
IntegrationBase,
IntegrationModel,
CreateIntegration,
UpdateIntegration,
DeleteIntegration,
OAuth2Requester,
ApiKeyRequester,
Credential,
Entity,
connectToDatabase,
mongoose,
UserModel,
Encrypt,
Cryptor,
BaseError,
debug,
TimeoutCatcher
} = require('@friggframework/core');
Environment Configuration
MONGO_URI | Yes | MongoDB connection string |
FRIGG_ENCRYPTION_KEY | Yes | 256-bit encryption key |
AWS_REGION | No | AWS region for services |
DEBUG | No | Debug logging pattern |
LOG_LEVEL | No | Logging level (debug, info, warn, error) |
License
This project is licensed under the MIT License - see the LICENSE.md file for details.
Support
Built with β€οΈ by the Frigg Framework team.