Database Architecture
Core Concepts
Team-Based Isolation
All data in PostHog is scoped to ateam_id:
Security critical: Never query without filtering by
team_id. This is enforced in code review and testing.Team vs Organization
- Organization: Billing entity, contains multiple teams
- Team: Data isolation boundary (formerly called “Project”)
- User: Can belong to multiple organizations
PostgreSQL Schema
Core Models
Team
Team
- Each team has a unique API token for event ingestion
- Teams are the primary data isolation boundary
- Team settings control product behavior
User & Organization
User & Organization
- Users belong to organizations via OrganizationMembership
- Access control is organization-level
- Billing is per organization
Product Models
Product-specific models live inproducts/<product>/backend/models.py:
Do not add domain-specific fields to the Team model. Use a Team Extension model instead. See
posthog/models/team/README.md for the pattern.ClickHouse Schema
Events Table
The core analytics table storing all events:- Partitioned by month for efficient pruning
- Replicated for high availability
- Replacing merge tree for deduplication
- Indexed on team_id, event, and timestamp
Person & Distinct IDs
PostHog maintains identity resolution across devices:- Event arrives with
distinct_id(e.g., device ID) - Look up
person_idinperson_distinct_idtable - Merge person properties if
$identifyevent - Associate all events with resolved
person_id
Session Recordings
Recording data uses a hybrid approach:- Small snapshots: Stored inline in ClickHouse
- Large recordings: Stored in S3, referenced by
session_id - Metadata: Always in ClickHouse for fast queries
Data Flow
1
Event Capture
Client SDK sends event to
/e/ endpoint:2
Ingestion & Enrichment
- Validate API key → resolve
team_id - Enrich with GeoIP data
- Add server-side properties
- Queue for processing
3
Identity Resolution
- Look up person by
distinct_id - Create person if first-time user
- Merge on
$identifyevents
4
Storage
- Write event to ClickHouse
sharded_events - Update person properties if changed
- Trigger webhooks/exports if configured
Querying Data
HogQL
PostHog’s SQL-like query language for ClickHouse:- Automatic
team_idinjection - SQL injection prevention via AST parsing
- Query timeout limits
- Result size limits
Query Runners
Python code that executes queries:Migrations
PostgreSQL Migrations
Standard Django migrations:ClickHouse Migrations
Managed migrations for ClickHouse schema changes:ClickHouse migrations require careful coordination in distributed environments. Always test on staging first.
Performance Considerations
PostgreSQL Optimization
- Use
select_related()andprefetch_related()to avoid N+1 queries - Add indexes on frequently queried fields
- Use
only()to fetch specific fields
ClickHouse Optimization
- Filter on
team_idandtimestampfirst (partition keys) - Use materialized columns for frequently accessed JSON properties
- Limit result sizes with
LIMIT - Use sampling for approximate results on large datasets
Next Steps
Development Setup
Set up your local environment
Testing
Learn about testing strategies