Philosophy
The principles and values that guide Hay's development
Development Philosophy#
Hay is built on a set of core principles that guide every technical decision. Understanding these principles will help you contribute effectively and build plugins that align with Hay's vision.
Core Principles#
1. Developer Experience First#
We believe that happy developers build better software.
In practice:
- Comprehensive TypeScript types throughout
- Clear, self-documenting code
- Detailed error messages with debugging hints
- Hot reload in development
- Extensive tooling and scripts
// Good: Clear, typed interface
interface CreateConversationParams {
customerId: string;
channel: 'email' | 'chat' | 'social';
subject?: string;
initialMessage: string;
}
// Bad: Unclear, any types
function create(params: any) { ... }
2. Convention Over Configuration#
Sensible defaults that work out of the box.
In practice:
- Zero config to get started
- Environment-based configuration
- Automatic discovery of plugins
- Intelligent defaults that can be overridden
// Convention: Plugins auto-discovered from /plugins directory
// Configuration: Override defaults only when needed
{
pluginDirectory: './custom-plugins', // optional
autoDiscovery: true // default
}
3. Fail Fast, Fail Loud#
Catch errors early and provide actionable feedback.
In practice:
- Strict TypeScript configuration
- Schema validation on all inputs
- Comprehensive error types
- Detailed error messages
// Validation errors are clear and actionable
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Invalid email format' });
4. Plugin-First Architecture#
Everything is a plugin, including core features.
Why:
- Forces modular design
- Ensures extensibility
- Dogfooding our own APIs
- Easy to add/remove features
In practice:
Plugins are discovered dynamically from the plugins/ directory. The core source code never hardcodes plugin IDs.
5. Data Ownership and Privacy#
Users own their data, always.
In practice:
- Export functionality for all data
- Clear data retention policies
- Encryption by default
- GDPR compliance built-in
- Self-hosting option available
6. Performance Matters#
Fast software is better software.
Optimization strategy:
- Measure first (no premature optimization)
- Optimize the critical path
- Cache aggressively
- Load lazily
// Example: Lazy loading plugins
const plugin = await import("./plugins/heavy-feature");
if (userNeedsFeature) {
await plugin.init();
}
Design Patterns#
Event-Driven Architecture#
Why: Decouples components and enables real-time features
Events are emitted via ConversationEventsService and WebSocket service, not a general event bus.
Dependency Injection#
Why: Testability and flexibility
Services use singleton exports and direct module imports (e.g., export const myService = new MyService()).
Repository Pattern#
Why: Abstracts data access, easy to swap databases
Repositories extend BaseRepository<T> (a generic TypeORM wrapper) — no interface abstraction layer.
Code Quality Standards#
TypeScript Usage#
- Strict mode enabled: No implicit any
- Explicit return types: For all public functions
- Discriminated unions: For state management
- Branded types: For IDs and sensitive data (aspirational pattern — not yet implemented in the codebase; IDs are currently plain strings)
Testing#
- Unit tests: For business logic
- Integration tests: For API endpoints
- E2E tests: For critical user flows
- Minimum 80% coverage: For new code (aspirational target — not currently enforced via CI)
describe("AutomationService", () => {
it("should trigger rule when conditions match", async () => {
const service = setupService();
const rule = createTestRule();
const result = await service.evaluate(rule, testConversation);
expect(result.triggered).toBe(true);
});
});
Documentation#
- TSDoc comments: On all public APIs
- README in each package: Setup and usage
- Architecture Decision Records: For major decisions
- Inline comments: Only for "why", not "what"
/**
* Evaluates automation rules against a conversation.
*
* @param rule - The automation rule to evaluate
* @param conversation - The conversation context
* @returns Evaluation result with actions to execute
*
* @throws {ValidationError} If rule or conversation is invalid
*
* @example
* ```ts
* const result = await evaluate(rule, conversation);
* if (result.matched) {
* await executeActions(result.actions);
* }
* ```
*/
async function evaluate(
rule: AutomationRule,
conversation: Conversation,
): Promise<EvaluationResult>;
Contribution Guidelines#
When contributing to Hay:
- Follow these principles: They're not just guidelines
- Write tests: Code without tests won't be merged
- Update docs: Code and docs should be in sync
- Start small: Small PRs are reviewed faster
- Ask questions: Better to ask than assume
Learning Resources#
- Architecture Guide - System design details
- Plugin Development - Build your first plugin
- Contributing Guide - How to contribute
Next Steps#
Ready to build? Start with: