Skip to main content
PostHog uses a product-oriented architecture where each major feature is a self-contained vertical slice with its own backend, frontend, and optional shared code.

What is a Product?

A product is a user-facing feature area like Feature Flags, Experiments, or Session Replay. Each product:
  • Owns its models, logic, API, and UI
  • Is isolated from other products
  • Has clear boundaries enforced by tooling
  • Can be tested independently with Turborepo

Product Structure

Each product follows this standardized layout:

Creating a New Product

Use the hogli CLI to scaffold a new product:
1

Bootstrap the product

This creates the full structure with apps.py, package.json, and all directories.
2

Register the Django app

Add to posthog/settings/web.py:
3

Update tach.toml

Add import boundary configuration:
4

Verify structure

Backend Conventions

Django App Registration

Each product backend is a real Django app:
The label must be the product name only, not the full path. This keeps migrations and app labels stable.

Import Patterns

Always use the real Python path:
Use string labels for relations:

Isolation with Facades

Products communicate through facades - a thin, stable public API.

Contracts (Frozen Dataclasses)

Define the product’s interface using frozen dataclasses:
Rules for contracts:
  • No Django imports
  • Immutable (frozen=True)
  • Small and hashable
  • No business logic

Facade API

Expose product functionality through a facade:

Using Facades

Other products import only the facade:
Why explicit mappers?Mapper functions (_to_artifact) provide an explicit boundary where internal models become external contracts. This allows:
  • Controlled exposure of fields
  • Field transformations and computed values
  • Protection from internal model changes

Business Logic Layer

Business logic lives in backend/logic.py:
Business logic includes:
  • Validation rules
  • Business invariants
  • Cross-field validations
  • Idempotency checks
  • Side effects

Presentation Layer

The presentation layer handles HTTP concerns:
Presentation responsibilities:
  • Validate incoming JSON
  • Convert JSON → frozen dataclasses
  • Call facade methods
  • Convert frozen dataclasses → JSON
  • No business logic

Frontend Organization

Product frontends use React with Kea for state management:

Selective Testing

Products use Turborepo for selective test execution:
How it works:
  • Turbo tracks dependencies via package.json
  • Only runs tests for changed products
  • Contracts (frozen dataclasses) define stability
  • When contracts don’t change, downstream tests are skipped
See the Products Architecture document for the full technical rationale behind DTOs, facades, and isolated testing.

Isolation Rules

Forbidden

  • ❌ Importing another product’s models.py
  • ❌ Importing another product’s logic.py
  • ❌ Importing views/serializers from another product
  • ❌ Returning ORM objects from facades

Allowed

  • ✅ Importing another product’s backend.facade
  • ✅ Using frozen dataclasses from contracts
  • ✅ Calling business logic within the same product
  • ✅ Presentation calling its own facade

Next Steps

Data Model

Explore PostHog’s data structures

Contributing

Learn how to contribute code