Login Get started

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:

  1. Measure first (no premature optimization)
  2. Optimize the critical path
  3. Cache aggressively
  4. 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:

  1. Follow these principles: They're not just guidelines
  2. Write tests: Code without tests won't be merged
  3. Update docs: Code and docs should be in sync
  4. Start small: Small PRs are reviewed faster
  5. Ask questions: Better to ask than assume

Learning Resources#

Next Steps#

Ready to build? Start with:

Last updated July 13, 2026

View on GitHub