How the PostHog Team Model and Organization Hierarchy Work: Structure, Permissions, and Best Practices
The PostHog Team model implements a strict parent-child hierarchy where Organizations own isolated analytics environments (Teams) with optional self-referential nesting for staging/production workflows, enforced through Django foreign keys and role-based access control.
The Team model and organization hierarchy in PostHog define how customer data is isolated and accessed across workspaces. Defined across posthog/models/team/team.py and posthog/models/organization.py, this architecture separates organizations (billing and user containers) from teams (isolated analytics environments with distinct API tokens), while supporting nested environments and aggressive Redis caching for high-throughput API access.
Core Database Relationships
PostHog’s hierarchy rests on three critical Django relationships defined in the core models.
Organization-to-Teams (One-to-Many):
In posthog/models/organization.py, the Organization model serves as the root container. The Team model in posthog/models/team/team.py defines:
organization = models.ForeignKey(
"posthog.Organization",
on_delete=models.CASCADE,
related_name="teams"
)
This creates a strict ownership model where every team belongs to exactly one organization, accessible via org.teams.all(), while organization deletion cascades to all child teams.
Self-Referential Team Nesting: Teams support optional environment hierarchies through a nullable self-reference:
parent_team = models.ForeignKey(
"posthog.Team",
on_delete=models.SET_NULL,
related_name="child_teams",
null=True
)
This enables patterns like Staging → Production chains without enforcing strict tree constraints.
User Association via OrganizationMembership:
Users link to organizations through OrganizationMembership (defined in posthog/models/organization.py), which stores role levels (member, admin, owner). Team-level access control extends this via Team.all_users_with_access(), aggregating users with direct organization membership, explicit team ACLs, or administrative roles.
Team Creation and Bootstrap Flow
PostHog provides two primary pathways for team instantiation.
Organization Bootstrap (Recommended):
The OrganizationManager.bootstrap() method in posthog/models/organization.py (lines 99-118) creates an organization, default project, and initial team atomically:
from posthog.models.organization import Organization
org, membership, team = Organization.objects.bootstrap(
user=request_user,
team_fields={"name": "Acme Analytics"},
name="Acme Corp"
)
This transaction ensures consistency between billing entities and analytics workspaces.
Direct Team Creation:
For additional teams, TeamManager.create() in posthog/models/team/team.py (lines 56-73) handles instantiation:
team = Team.objects.create(
name="New Workspace",
organization=existing_org
)
The manager automatically provisions a matching Project record and initializes default dashboards and session recording playlists.
Nested Team Hierarchies and Navigation
The parent_team field enables environment isolation without separate organization overhead.
Navigate hierarchies using the related_name accessors:
# Navigate upward
parent = team.parent_team # Returns None or Team instance
# Navigate downward
children = team.child_teams.all() # QuerySet of nested teams
# Organization-level aggregation
all_org_teams = organization.teams.select_related("parent_team").all()
This structure supports common patterns like separating production event streams from testing environments while maintaining unified organization-level billing and user management.
Caching and Token Management
PostHog implements aggressive Redis caching to minimize database load on high-traffic API endpoints.
Token-Based Cache Lookup:
The TeamManager.get_team_from_cache_or_token() method checks team_in_cache Redis keys before querying PostgreSQL. Cache population and invalidation occur through Django signals in posthog/models/team/team.py (lines 84-92):
put_team_in_cache_on_savetriggers onpost_savedelete_team_in_cache_on_deletetriggers onpost_delete
API Token Rotation:
For security incidents, teams support token regeneration via reset_token_and_save():
team = Team.objects.get(api_token="compromised-token")
team.reset_token_and_save(
user=request_user,
is_impersonated_session=False
)
This method invalidates the Redis cache entry and generates a cryptographically secure replacement.
Access Control Implementation
Permission enforcement operates at two levels.
Organization-Level Permissions:
Viewsets inherit from OrganizationMemberPermissions and OrganizationAdminWritePermissions to enforce role-based access across all organization teams.
Team-Level Permissions:
The TeamAndOrgViewSetMixin and AccessControlViewSetMixin (implemented in posthog/api/team.py) combine organization-wide membership checks with explicit team ACLs. The Team.all_users_with_access() method (lines 107-133 in team.py) dynamically aggregates authorized users by evaluating:
- Direct organization membership
- Explicit team access grants
- Administrative role elevation
Practical Implementation Examples
Bootstrap a New Organization with Default Team
from posthog.models.organization import Organization
org, membership, team = Organization.objects.bootstrap(
user=request_user,
team_fields={"name": "Production Environment"},
name="Acme Corp"
)
print(f"Org: {org.name}, Team Token: {team.api_token}")
Create a Child Team for Staging
from posthog.models.team import Team
production = Team.objects.get(id=42)
staging = Team.objects.create(
name="Staging",
organization=production.organization,
parent_team=production
)
Query All Accessible Teams for a User
accessible_teams = Team.objects.filter(
organization__memberships__user=request_user
).distinct().select_related("organization")
Rotate API Token After Security Event
team = Team.objects.get(api_token="phc_oldtoken123")
team.reset_token_and_save(
user=request_user,
is_impersonated_session=False
)
Summary
- Organizations are billing and user containers; Teams are isolated analytics environments with unique API tokens.
- The hierarchy uses a ForeignKey from
TeamtoOrganizationwithrelated_name="teams", and a self-referential ForeignKey (parent_team) for environment nesting. - Creation typically flows through
Organization.objects.bootstrap()for initial setup, orTeam.objects.create()for additional teams. - Redis caching via
get_team_from_cache_or_token()minimizes database load, with automatic invalidation through Django signals. - Access control combines
OrganizationMembershiproles withTeam.all_users_with_access()for granular permissions. - Token rotation uses
reset_token_and_save()to invalidate cached credentials and generate new tokens securely.
Frequently Asked Questions
How does team nesting work in PostHog?
Team nesting utilizes the parent_team ForeignKey on the Team model. A team can optionally specify another team as its parent, creating hierarchical relationships like staging and production environments. This is implemented via models.SET_NULL behavior, meaning deleting a parent team nullifies the reference rather than cascading deletion to children. Access child teams through the child_teams related manager.
What is the difference between an Organization and a Team in PostHog?
An Organization represents the billing entity and user management container, handling subscriptions and high-level permissions. A Team represents an isolated analytics workspace with its own distinct api_token, event ingestion pipeline, dashboards, and feature flags. While organizations manage who has access, teams determine where data is stored and how it is queried, with strict data isolation between teams.
How does PostHog cache Team objects for API performance?
PostHog caches active Team records in Redis using the get_team_in_cache and set_team_in_cache utility functions from posthog/models/team/team_caching.py. The TeamManager.get_team_from_cache_or_token() method attempts Redis lookup by api_token before falling back to PostgreSQL. Django signal handlers put_team_in_cache_on_save and delete_team_in_cache_on_delete ensure cache consistency automatically on model updates.
Can a user belong to multiple teams in different organizations?
Yes. Users associate with organizations through OrganizationMembership records, and can hold memberships in multiple organizations simultaneously. Within each organization, they inherit access to teams based on organization-level roles or explicit team grants. The Team.all_users_with_access() method aggregates these permission sources to determine effective access rights across the hierarchy.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →