PostHog Handbook Library / Engineering

2,670 words. Estimated reading time: 12 min.

# SDK guidelines

Auto TL;DR

## At a Glance

This long page covers these main areas. The list is generated from the article headings, so it updates with every handbook rebuild.

1. Make the default experience excellent
2. Don't break the host application
3. Keep dependencies boring
4. Respect the platform
5. Be careful with resources
6. Keep identity and state boring
7. Make SDKs thread safe
8. Treat privacy as a product feature

These are living guidelines, and they're meant to help us make better tradeoffs, not to be a gatekeeper. If a guideline doesn't fit your SDK, don't treat it as a blocker. Talk about it, write down the decision, and move on with context for the next person.

The big idea: PostHog SDKs run inside customer applications. That means customers lend us trust every time they install one. Our job is to be useful, boring in production, and safe to run in places we don't control.

#### Make the default experience excellent

Most users never read every option. They install the package, copy the quickstart, and hope it works.

Good defaults matter more than lots of configuration. Capture the right context by default, batch sensibly, retry carefully, and avoid making users learn PostHog internals before they see value. Configuration is still important, but it should feel like customization rather than a requirement to get a safe baseline.

A clean install should get developers to first value quickly: install the package, initialize PostHog, and capture a test event or evaluate a feature flag in a few minutes. Quickstarts should work from a new project without hidden setup.

In general, features should be enabled by default unless there's a good reason not to, such as privacy risk, compatibility risk, performance risk, or platform limitations. Users should be able to opt in and out of features, and ideally high-level feature controls should also exist in PostHog project settings through remote config. When the reason is privacy-sensitive data, see Treat privacy as a product feature.

#### Don't break the host application

The SDK should never be the reason a customer's app crashes, slows down dramatically, fails to build, or starts behaving strangely.

Prefer graceful degradation over cleverness. If feature flag polling, replay capture, networking, storage, or background work fails, the app should keep running. In most cases, failing silently with useful debug logging is better than surprising the customer at runtime. The logger is our friend here: use it to explain what happened without making the host app pay for it.

Silent failure is not always the right default. Initialization errors, invalid configuration, unsupported hosts, project token problems, and explicit customer-called APIs should surface clear, idiomatic errors when the customer can act on them.

Retries and timeouts should be bounded, predictable, and documented. Avoid retry behavior that surprises customers, creates duplicate work, or hides persistent failures.

If an SDK can't support a platform, framework version, or runtime, make that clear at install time or startup. Don't make customers discover it through confusing production errors.

#### Keep dependencies boring

Every dependency adds size, security surface area, licensing questions, maintenance work, and compatibility risk. It can also introduce malware or compromised packages, naming clashes, dependency resolution surprises, runtime breakage from a transitive version change, and noticeably larger binaries or bundles. Use dependencies when they clearly improve the SDK, but be skeptical of adding them to the core path.

A good rule of thumb: basic event capture should work with as little extra machinery as the platform reasonably enables. Optional integrations can have optional dependencies, but the base SDK should stay lean.

If a specific feature needs a specific dependency, consider making that feature a separate module or package. For example, Session Replay may need image or video encoding dependencies, and iOS Error Tracking may need crash reporting dependencies. Users should have a clear way to opt out of that feature and dependency when they don't need it, can't ship it, or need a smaller binary.

That said, dependencies are sometimes the right choice. Some platforms don't provide safe basic primitives, such as an HTTP layer, storage, or concurrency tools. In those cases, use a boring, well-maintained dependency. If dependency risk is high but the code is small and stable, vendoring can also be a reasonable option. Document the tradeoff either way.

#### Respect the platform

Each SDK should feel natural in its language and ecosystem. Follow platform naming conventions, package manager expectations, async patterns, error handling style, logging conventions, and test tooling.

Consistency across PostHog SDKs is useful, but not at the cost of making a Ruby SDK feel like JavaScript, or a Swift SDK feel like Python. Prefer a small shared vocabulary – `capture`, `identify`, `alias`, `flush`, `shutdown` – and let the platform shape the details.

At the same time, don't make SDKs different for the sake of it. Users move between SDKs, and LLMs often help port examples from one language to another. Keep names, concepts, method behavior, and configuration shapes as close as the platform reasonably enables.

#### Be careful with resources

SDKs often run in hot paths, mobile apps, serverless functions, CLIs, browsers, background workers, and long-lived servers. Resource usage needs to be boring too.

Watch for memory growth, unbounded queues, aggressive timers, excessive network calls, large payloads, lock contention, startup cost, and battery usage. Add backpressure where possible. If the SDK holds data in memory, try to provide a clear maximum size for that data or queue, and make it configurable when customers may need to tune it.

If a customer reports one of these problems, consider adding a stress test or regression test with a safe threshold. The goal isn't to make performance tests flaky. It's to catch future PRs that clearly bring back the same class of problem.

#### Keep identity and state boring

Identity is one of the easiest places to confuse customers and corrupt data. `distinct_id`, anonymous IDs, `identify`, `alias`, reset/logout, group state, feature flag state, and persisted properties should behave predictably and, where possible, consistently across SDKs.

Be explicit about whether an SDK is stateful or stateless. Browser and mobile SDKs usually own local state because they persist anonymous IDs, queued data, flags, and replay/session context. Many server-side SDKs should be more stateless by default because one process can handle many users, tenants, requests, or jobs at the same time.

Don't accidentally make a stateless SDK stateful by storing per-user data globally. If state is needed, make the boundary obvious: request-scoped client, explicit context object, local storage, cookie, in-memory queue, or whatever is idiomatic for the platform.

#### Make SDKs thread-safe

Assume public SDK methods can be called from multiple threads, async tasks, workers, callbacks, request handlers, or lifecycle hooks. Queues, identity state, remote config, feature flag caches, loggers, and shutdown paths should be safe under concurrent access.

If a platform has a single-threaded runtime, still think about re-entrancy and async ordering. If something is not thread-safe, document it loudly and provide a safe path for normal usage.

#### Treat privacy as a product feature

PostHog helps customers understand users, but our SDKs should not collect sensitive data casually.

Be explicit about anything that can include personal data, request/response bodies, headers, screen contents, console logs, or exception context. Prefer opt-in for high-risk data, make masking and redaction easy, and document what leaves the device or server.

#### Think about security beyond privacy

Privacy is not the whole security story. SDKs should avoid exposing secrets, storing sensitive data unnecessarily, weakening TLS defaults, trusting unvalidated remote input, or making supply-chain risk worse.

Use platform-sandboxed storage where possible, such as app-scoped storage, Keychain/Keystore-style APIs for sensitive values, browser storage with the right assumptions, or restricted file permissions on servers. If data is only needed temporarily, prefer memory over durable storage.

Releases are part of the security model too. SDK publishing should be automated through CI and protected by an approval process, as described in the SDK release process. Avoid local machine publishing for official releases when CI can do it, because CI gives us clearer provenance, fewer long-lived credentials, and a better audit trail.

#### Design APIs for forward compatibility

SDK APIs live for a long time. Once a pattern is copied into thousands of apps, changing it gets expensive.

Keep the public API small, boring, and hard to misuse. Use options objects for things likely to grow. Avoid exposing internal concepts unless customers need them. Public APIs and configuration options should be unique: don't offer two or more ways to do the same thing unless there's a strong compatibility reason. Duplicate paths confuse humans, documentation, support, and LLMs.

Agents can help spec and drive SDK changes, especially repetitive cross-SDK work. Public APIs, configuration, defaults, and behavior that affects customers still need human review for ergonomics, platform fit, and long-term support cost.

Be careful not to expand the public API by accident. Exported helpers, leaked internal types, undocumented options, and test-only hooks can become APIs customers depend on. Keep internals private where the platform enables it. If something is experimental, say so clearly and consider keeping it behind an internal API until we're confident it should be public.

Prefer additive API changes over breaking ones. It's much easier to add a new method, option, or type than to remove one later. When you need a breaking change, respect the SDK's versioning scheme, make the migration obvious, document it clearly, and release it intentionally.

For larger migrations, write a migration doc and, where useful, an agent skill that can help apply the change across customer codebases. Try to batch breaking changes into a single major version instead of shipping a new breaking change every week.

#### Deprecate before removing

Use semver, or the ecosystem's closest equivalent, for public API changes. Removing a public method, field, configuration option, package, or behavior should usually wait for the next major version.

Before removing something, deprecate it first. Keep the deprecated method or option working until the major release, route it to the new implementation where possible, and log a clear runtime warning when it is used. The warning should say what changed, what to use instead, and where to find the migration guide.

Deprecation warnings should be useful, not noisy. Avoid logging the same warning thousands of times in a hot path if you can log it once per process, session, or call site.

#### Write less SDK code when the server can do it better

SDKs should collect useful context and send high-quality data. They should avoid owning complex business logic that can live safely on the server.

Server-side logic is easier to change, observe, roll back, and fix globally. SDK-side logic ships into customer apps and can take weeks, months, or years to update. Put logic in the SDK only when it needs local state, local performance, platform APIs, or offline behavior.

#### Make debugging humane

When something goes wrong, customers and support engineers need a path to answers.

Provide debug logging that can be enabled without rebuilding the world. Include enough information to understand initialization, dropped events, retries, network failures, feature flag decisions, and queue state. Avoid logging secrets. Project tokens are public identifiers, but other credentials are not.

Remember that SDKs often run on customer devices or infrastructure where we don't have access to logs. When it helps support and debugging, include minimal, high-value SDK state in captured data, recordings, or diagnostics. Session Replay is a good example: a small amount of SDK health context can make production issues much easier to investigate. Keep this data minimal, documented, and privacy-aware.

#### Test the boring paths and the weird paths

The happy path matters, but SDK bugs often hide in shutdown, retries, offline mode, old runtimes, ad blockers, proxies, clock skew, app backgrounding, forked processes, serverless cold starts, and partial initialization.

Prefer tests that match how customers use the SDK. Add small example apps where they help. For mobile and browser SDKs, remember that customers can't always roll out fixes quickly, so a little extra caution before release is worth it.

#### Treat docs and examples as part of the SDK

An SDK without good docs is only half shipped. Keep the quickstart current, show idiomatic examples, and explain common production setup: flushing on shutdown, identifying users, using custom hosts, handling feature flags, and enabling debug logs.

Each SDK should have a troubleshooting page for common install, build, configuration, network, and runtime errors.

Examples should be boring, copy-pasteable, and close to how customers write real production code in that ecosystem.

Public methods, configuration options, and types should have documentation comments in the platform's standard style, such as JSDoc, docstrings, KDoc, or Swift documentation comments. Write them for humans, but remember that LLMs and IDEs parse them too. A good comment explains what the method or option does, when to use it, defaults, side effects, and any privacy or performance caveats.

The public API reference should be complete and current. It should cover public methods, types, configuration options, defaults, side effects, return values, errors, and examples where they help.

#### Release like people depend on it

Because they do. Use semver or the ecosystem's closest equivalent, keep changelogs readable, call out breaking changes loudly, and follow the SDK release process. Official releases should be automated through CI and sit behind an approval process to reduce supply-chain risk.

Release cadence is a balance. Giant releases are hard to review, hard to debug, and hard to roll back, but releasing every tiny change can also create noise and upgrade fatigue. Prefer coherent releases: small enough to understand, grouped enough to be useful, and clearly documented so customers know whether they should care.

The right cadence also depends on the platform and ecosystem. Web and server SDK users can often upgrade quickly through a package manager, but mobile, desktop, game engine, and enterprise customers may deal with app store review, slow adoption, long release trains, or internal approval processes.

#### Keep changelogs and version bumps accurate

A changelog is customer-facing release documentation, not just release metadata. For SDKs using changesets, the changelog entry is part of the code change. As the author of a change, you're responsible for writing a meaningful summary and choosing the correct semver bump before the PR lands. Take care to ensure that the entry accurately reflects what changed, who it may affect, and whether customers need to take action. If they do, describe that action clearly.

Use semver, or the ecosystem's closest equivalent:

- Patch versions are for backward-compatible bug fixes, security fixes, documentation/package metadata corrections, and internal changes that don't add customer-facing API or behavior. A user should be able to upgrade a patch release without changing their code or expectations.
- Minor versions are for backward-compatible additions or behavior improvements: new public methods, options, integrations, supported platforms, or feature support. Existing code should continue to compile, run, and behave as before.
- Major versions are for breaking changes: removing or renaming public APIs, changing method signatures or return types, changing default behavior in a way customers may rely on, dropping supported platforms/runtime versions, changing persisted data formats without safe migration, or making previously valid configuration invalid. Major version bumps are ideally planned in advance, and should typically be avoided unless absolutely necessary.

If a release does include a breaking change, call it out clearly in the changelog and provide migration guidance. Before introducing a breaking change, consider alternative solutions by asking yourself:

- Could this change have been written in a backward-compatible manner?
- If this supersedes an existing public API or behavior, should the old one be deprecated first?
- Can the new behavior be introduced as an opt-in option first, or can the old behavior remain available through a defaulted option or alias?
- Do we understand who is affected and how hard migration will be?
- Should this be batched with other breaking changes in the next major release?

#### Document decisions and sharp edges

If an SDK supports only certain platform versions, has unusual threading behavior, drops events under pressure, stores data locally, or handles privacy-sensitive data, write it down.

Most of the time, a code comment near the decision is enough. For bigger decisions, write an RFC or add the guidance here if it applies across SDKs. This isn't bureaucracy. It's how we avoid the next contributor rediscovering the same tradeoff six months later.

Canonical URL: https://posthog.com/handbook/engineering/sdks/guidelines

GitHub source: contents/handbook/engineering/sdks/guidelines.md

Content hash: `49b0e23a533ba564`