agentleFS
PostHogKnown publisherPostHog/posthog-foss/.agents/skills/adding-personhog-rpc/SKILL.md

adding-personhog-rpc

Skill727 starsChanged yesterdayMITView on GitHub

Guide for adding a new RPC to personhog-replica and personhog-router. Covers eligibility checks, proto definition, code generation for Python and Node.js clients, Rust implementation (storage trait, postgres queries, service handler, router wiring), and index compatibility validation. Use when adding a new gRPC endpoint to personhog, migrating a Django ORM query to personhog, or extending the personhog service API.

We split this skill into the rules it gives an agent

12 rules. About 0 are common, 6 are distinctive, and 6 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈6

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 6

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    If the data being accessed doesn't live in one of these tables, this RPC doesn't belong in personhog:

    line 19
  2. Distinctive:

    If the table is not listed above, stop — this data should not go through personhog.

    line 32
  3. Distinctive:

    Before writing any proto, figure out the SQL query you need.

    line 36
  4. Very specific:

    All proto files live in proto/personhog/.

    line 49
  5. Distinctive:

    Every leader RPC must therefore converge under redelivery — read-only lookups, merges that re-apply to the same state, tombstone-style …

    line 66
  6. Distinctive:

    If the operation you need fits neither shape, redesign it to carry an idempotency key before defining the proto.

    line 73
  7. Very specific:

    No generation step needed — tonic regenerates on cargo build.

    line 105
  8. Very specific:

    Use sqlx::query_as! or sqlx::query!

    line 116
  9. Very specific:

    Add timing instrumentation via DB_QUERY_DURATION and DB_ROWS_RETURNED metrics

    line 117
  10. Very specific:

    Use self.replica_pool for reads, self.primary_pool for writes

    line 118
  11. Distinctive:

    Call the storage trait method

    line 126
  12. Very specific:

    Use rstest parameterized tests where multiple variations of the same behavior are being tested.

    line 140

≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Adding a personhog RPC

This skill walks through adding a new RPC end-to-end: proto definition, code generation, Rust implementation, and client updates.

Before you start: eligibility check

Personhog serves person, distinct ID, group, group type mapping, cohort membership, and feature flag hash key override data. If the data being accessed doesn't live in one of these tables, this RPC doesn't belong in personhog:

TableData categoryRouting
posthog_personPersonDataReads: replica (eventual) or leader (strong). Writes: leader
posthog_persondistinctidPersonDataSame as person
posthog_groupNonPersonDataAll ops: replica
posthog_grouptypemappingNonPersonDataAll ops: replica
posthog_cohortpeopleNonPersonDataAll ops: replica
posthog_featureflaghashkeyoverrideNonPersonDataAll ops: replica
posthog_personoverridePersonDataReads/writes follow person routing
posthog_personlessdistinctidPersonDataSame as person

If the table is not listed above, stop — this data should not go through personhog.

Step 0: design the data access pattern

Before writing any proto, figure out the SQL query you need. Then validate it against the available indexes — see references/database-indexes.md.

Key questions:

  • What table(s) does this query hit?
  • Does the WHERE clause match an existing index? Every query must be an index scan, never a sequential scan.
  • Is this a read or write? This determines routing (see table above).
  • For reads: does the caller need strong consistency (primary pool) or is eventual (replica pool) acceptable?
  • For batch lookups: is the batch within a single team or cross-team?

Step 1: define proto messages and RPC

All proto files live in proto/personhog/. See references/proto-conventions.md for message conventions and a worked example.

Where to add what

  1. Message types → proto/personhog/types/v1/<domain>.proto (person.proto, group.proto, cohort.proto, feature_flag.proto, or common.proto)
  2. Service RPC → proto/personhog/service/v1/service.proto (the public API clients call)
  3. Replica RPC → proto/personhog/replica/v1/replica.proto (the internal API the router delegates to)
  4. Leader RPC → proto/personhog/leader/v1/leader.proto (only if this is a person-data write routed to leader)

The service and replica protos must both declare the RPC with identical signature. The router delegates from service → replica (or leader) transparently.

Leader RPCs must be safe under at-least-once delivery

A leader-path request can be delivered more than once: clients retry UNAVAILABLE after ambiguous failures, and the router internally replays fence- and transport-bounced requests. Every leader RPC must therefore converge under redelivery — read-only lookups, merges that re-apply to the same state, tombstone-style deletes, max-merge version floors — or carry explicit operation identity so duplicates can be detected. Note the precise contract: a convergent merge still loses to interleaving (a replay can clobber a same-field write another caller made in between); that residual is accepted today and closes with operation identity (see personhog-leader's README). An RPC that neither converges nor carries identity (an unguarded increment, an append) must not be added. If the operation you need fits neither shape, redesign it to carry an idempotency key before defining the proto.

Step 2: generate client stubs

Python

bin/generate_personhog_proto.sh

Then update three files:

  • posthog/personhog_client/proto/__init__.py — add re-exports for new request/response message types
  • posthog/personhog_client/client.py — add a wrapper method matching the pattern of existing methods
  • posthog/personhog_client/fake_client.py — implement the method for test use

Node.js

cd nodejs && pnpm run generate:personhog-proto

Then update:

  • nodejs/src/common/personhog/groups.ts or persons.ts — add a wrapper method to the matching operations class (PersonHogGroupOperations / PersonHogPersonOperations), following the pattern of existing methods. client.ts only constructs and exposes these operation objects; it holds no RPC wrappers itself.
  • nodejs/src/common/personhog/client.test.ts — add a default stub to SERVICE_DEFAULTS for the new RPC

Rust

No generation step needed — tonic regenerates on cargo build. But you must implement the RPC (next step), or the build will fail.

Step 3: implement in Rust

The compiler guides you — once the proto is defined, cargo build errors tell you exactly which trait methods are missing.

3a. Storage layer (personhog-replica)

  1. Add a trait method in rust/personhog-replica/src/storage/traits/<domain>.rs
  2. Implement the query in rust/personhog-replica/src/storage/postgres/<domain>.rs
    • Use sqlx::query_as! or sqlx::query! macros
    • Add timing instrumentation via DB_QUERY_DURATION and DB_ROWS_RETURNED metrics
    • Use self.replica_pool for reads, self.primary_pool for writes
    • Return early for empty batch inputs
  3. Add storage tests in rust/personhog-replica/tests/storage_tests.rs

3b. Service layer (personhog-replica)

  1. Add the RPC handler in rust/personhog-replica/src/service/mod.rs
    • Extract fields from the proto request
    • Call the storage trait method
    • Convert storage results to proto responses
    • Map storage errors to tonic Status codes
  2. Add service tests in rust/personhog-replica/tests/service_tests.rs

3c. Router wiring (personhog-router)

The router forwards request bytes without decoding them, so a new RPC needs no handler there.

  1. Add the method name to KNOWN_METHODS in rust/personhog-router/src/proxy.rs, keeping the list sorted
    • Methods that must reach the leader are matched by name in proxy.rs; everything else forwards to a replica
    • known_methods_is_sorted and known_methods_matches_service_proto in the same file fail until the list matches service.proto
  2. Add the method to every PersonHogService mock that implements the generated trait, including the ones outside personhog (rust/personhog-router/tests/common/mod.rs, rust/property-defs-rs/tests/); tonic traits have no default methods

Use rstest parameterized tests where multiple variations of the same behavior are being tested.

Step 4: verify

cargo build -p personhog-proto
cargo build -p personhog-replica
cargo build -p personhog-router
cargo test -p personhog-replica
cargo test -p personhog-router

Checklist

  • Query uses an existing index (no seq scans)
  • Proto messages added to types/v1/<domain>.proto
  • RPC added to both service.proto and replica.proto (and leader.proto if needed)
  • Leader RPCs only: operation is safe under at-least-once delivery (convergent under redelivery — read-only, re-appliable merge, tombstone, max-merge — or carries explicit operation identity)
  • Python stubs generated, proto/__init__.py updated, client.py method added, fake_client.py updated
  • Node.js stubs generated, client.test.ts SERVICE_DEFAULTS updated
  • Rust storage trait + postgres impl + service handler + router wiring all implemented
  • Tests added at storage, service, and router layers
  • cargo build and cargo test pass for all three crates

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

Reviews can't be read right now.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.