Adding Api Scopes
Guidance for adding an API scope object to posthog/scopes.py and making it work for personal API…
by posthog · MIT
The skill
Guidance for adding an API scope object to posthog/scopes.py and making it work for personal API keys, OAuth tokens and MCP clients. Use when adding a scope object, exposing a viewset or MCP tool to tokens, moving a viewset off scope_object = "INTERNAL", deciding if a scope is internal, OAuth-hidden or privileged, choosing a scope group, reading the scope list in Python, the frontend or the MCP server, or when a scope test fails in scopes.test.ts, test_scopes.py or tool-filtering.test.ts. Trigger terms: new scope, APIScopeObject, ScopeObjectEnumApi, scope_object, required_scopes, API_SCOPES, API_SCOPE_GROUPS, mcp scopes, OAuth scopes, personal API key scope.
Adding an API scope
A scope has two parts: an object and an action, as in feature_flag:read.
Adding a scope means adding a scope object, such as feature_flag. It gives tokens feature_flag:read and feature_flag:write.
posthog/scopes.py is the source of the object list.
The frontend type and the MCP OAuth list are generated from it.
The picker rows and the groups are kept by hand in frontend/src/lib/scopes.tsx, and a test checks them.
They stay in the frontend on purpose: labels, plurals, groups and picker omissions are UI decisions, while posthog/scopes.py decides what exists and what it grants.
Decide if you need a new scope
Most endpoints fit an existing scope, for example insight:read.
Add a new scope only for a product area that a person wants to grant or withhold on its own, on a key or an OAuth app.
Name its object with a snake_case singular noun, such as feature_flag.
Decide what kind of scope it is
- Public: OAuth lists it, MCP can request it, and the key picker offers it. This is the default.
- Internal: only the server creates tokens with it. Use
INTERNAL_API_SCOPE_OBJECTS. - OAuth-hidden: a person can paste it into a personal API key, but OAuth clients do not see it. Use this for staff-only or unreleased surfaces. Use
OAUTH_HIDDEN_SCOPE_OBJECTS. - Privileged: only PostHog staff can give it to an OAuth app, through Django admin or a data migration. An app that registers itself cannot get it, and the CLI login and the key picker presets never include it. Use this for a scope that a partner app must not grant to itself, such as
llm_gateway. UsePRIVILEGED_SCOPES, and setunprivilegedExcluded: trueon the picker row.
Then decide two more things, separately from the kind:
- Project secret API keys: a project secret API key is a project credential with no user, for server-to-server calls. Allow the scope on it only when such a caller needs it. The allowed list is
PROJECT_SECRET_API_KEY_ALLOWED_API_SCOPE_ACTION, in bothposthog/scopes.pyandfrontend/src/lib/scopes.tsx. Read/adding-project-secret-api-key-authfirst. - Access control: if an organization must be able to restrict the resource per role or per object, add it to
ACCESS_CONTROL_RESOURCESinproducts/access_control/backend/facade/user_access_control.py. Access control resources use scope object names by design, so a viewset'sscope_objectnames both its token scope and its access control resource. Do not give access control a naming or a type of its own. Theresourcefields of the access control serializers takeGRANTABLE_API_SCOPE_OBJECTSas their choices, and the frontendAPIScopeObjecttype is generated from those fields (be careful: the personal API key modal, the OAuth consent screen, CLI login and the key presets also use that type).
To add the scope
- Declare it in Python. Add the object to the
APIScopeObjectliteral inposthog/scopes.py. If you chose internal, OAuth-hidden or privileged above, also add it to that set. - Connect it to its endpoints. Set
scope_object = "<object>"on each viewset the scope covers.- Standard actions need no extra work:
listandretrieveneed:read, andcreate,updateanddestroyneed:write. - A custom
@actionneedsrequired_scopes, for examplerequired_scopes=["<object>:write"]. Use:readif it only reads data, and:writeif it changes data. Without it, token requests get a 403. - An endpoint set to
scope_object = "INTERNAL"accepts only logged-in sessions. Change it to the new object to open it to tokens. - Then call each custom action with a personal API key that has only the new scope. No test checks this.
- Standard actions need no extra work:
- Regenerate. Run
hogli build:openapi, which carries the object to the frontend type, andhogli build:projections, which updates the OAuth lists of the MCP server and the web app. DO NOT EDIT GENERATED FILES BY HAND. - Show it in the pickers. Skip this step for an OAuth-hidden object: no picker shows it, and the tests do not ask for a row or a group. Otherwise, in
frontend/src/lib/scopes.tsx:- Add a row to
API_SCOPESwith a sentence-case label and plural (/writing-user-facing-copy). Disablewriteif no endpoint writes. If the key modal should not offer the object, add a reason toAPI_SCOPES_OMITTED_FROM_MODALinstead. - Add the object to one group in
API_SCOPE_GROUPS. See "Choose a group".
- Add a row to
- Let MCP tools request it. For each MCP tool that calls the endpoint, add the scope under
scopes:inproducts/<product>/mcp/tools.yaml.
Choose a group
The OAuth consent screen and the scope pickers show objects in groups, from API_SCOPE_GROUPS in frontend/src/lib/scopes.tsx.
The groups make a long list readable, so a person can find a product quickly.
- A group is a product area that a person recognizes, such as "Session replay" or "Feature flags, experiments & surveys". It is not a code module or a team.
- Put the object in the existing group that a person would look in first.
- Add a new group only when it gets two or more objects. A group with one object makes the list longer, not easier to read.
If a test fails
frontend/src/lib/scopes.test.ts, the coverage test: a grantable object has no picker row and no omission reason. Add a row toAPI_SCOPES, or a reason toAPI_SCOPES_OMITTED_FROM_MODAL. If the object is missing fromAPIScopeObject, runhogli build:openapifirst. If the object is OAuth-hidden, runhogli build:projectionsinstead, so the test learns to skip it.frontend/src/lib/scopes.test.ts, the group test: an object is in no group, or in two groups. Put it in exactly one group ofAPI_SCOPE_GROUPS. It also fails when an OAuth-hidden object has a row or a group. Remove them, because no picker shows a hidden object.posthog/test/test_scopes.py: an internal, OAuth-hidden or privileged scope leaks into a list it must stay out of, or the project secret API key list inposthog/scopes.pydiffers from the copy infrontend/src/lib/scopes.tsx. Fix the set, or make the two lists equal.services/mcp/tests/unit/tool-filtering.test.ts, the completeness test: an MCP tool requires a scope that OAuth does not advertise. If the scope is new, runhogli build:projections. If it is internal, add it to the test's server-only list. Otherwise fix the scope name intools.yaml.
What no test covers
- A custom
@actionwithoutrequired_scopes. Tokens get a 403. - A read scope on an action that changes data.
- An MCP tool scope that differs from its endpoint's
scope_object. - A group that makes no sense for the object, or a new group that should not exist. The group test only checks that each object has exactly one group. Check that it sits where a person would look for it, and that a new group has at least two objects. See "Choose a group".
- A wrong or unclear label.
Where to read scopes in code
Use these instead of writing your own list of scopes.
- Python: import from
posthog.scopes.API_SCOPE_OBJECTS: every object, internal ones too.GRANTABLE_API_SCOPE_OBJECTS: the objects a person can grant.ALL_SCOPES: the grantableobject:actionstrings.get_oauth_scopes_supported(): what the OAuth server advertises.
- Frontend type:
APIScopeObjectfrom~/types. - Frontend list at runtime:
Object.values(ScopeObjectEnumApi), fromproducts/access_control/frontend/generated/api.schemas. - Frontend labels and groups:
API_SCOPES,API_SCOPE_GROUPSandgetScopeDescriptionfromlib/scopes. - OAuth lists:
OAUTH_SCOPES_SUPPORTEDandOAUTH_SCOPES_HIDDEN, fromlib/oauthScopes.generatedin the web app andservices/mcp/src/lib/oauth-scopes.generated.tsin the MCP server. The two files are the same.
Use this skill
Connect your agent to the Skill Store MCP server and ask for it by ref: posthog/adding-api-scopes-12233446513.
Details
- Publisher
- posthog
- Category
- Software Development › APIs & backend
- License
- MIT
- Updated
- 2026-10-19
References
More from posthog
- Adding Warehouse Person Properties Sync columns from a synced data warehouse table onto PostHog person or group properties, so wareh…
- Analyzing Expensive Users Analyze the most expensive users in AI observability and explain why they cost so much.
- Analyzing Experiment Session Replays Analyze session replay patterns across experiment variants to understand user behavior differences.
- Analyzing Offline Evaluations Inspect and compare offline AI evaluation experiments, diagnose case-level regressions, and follo…