Managing access from Python
AccessClient is in the Python package (pip install featuremesh), not the demos repo. Everything the console does — creating orgs and projects, inviting people, rotating tokens, running service accounts — is also an AccessClient call. This guide is the working reference for doing it from a notebook or a script.
It assumes you already know the model: guest versus project, personal versus regular orgs, what the roles mean. If not, read Access management first — it is short.
Two clients, two tokens
Keep these straight and the rest of the page is mechanical.
| Client | Token | Job |
|---|---|---|
AccessClient | Identity token from the console | Orgs, projects, invitations, roles, service accounts |
BatchClient | Project access token | Running FeatureQL against one project |
from featuremesh import AccessClient
identity_token = "your_identity_token"
client = AccessClient(identity_token=identity_token) Every call below returns a success or failure status; the error codes at the end tell you which.
Working alone
Your personal org already exists, so you only need a project.
client.create_project(project_name="my_project") Project names are 2 to 16 characters — alphanumerics and _ — and unique within the org. The project reference becomes user_id/my_project.
Now mint a project access token and run FeatureQL through BatchClient in managed mode:
import duckdb
from featuremesh import BatchClient, Backend, RegistryDeployment, generate_access_token, set_default
project_reference = "user_id/my_project"
access_token = generate_access_token(identity_token, project_reference)
set_default("registry", RegistryDeployment.MANAGED)
fql_client = BatchClient(
access_token=access_token,
backend=Backend.DUCKDB,
sql_executor=lambda sql: duckdb.sql(sql).df(),
)
fql_client.query("CREATE FEATURE MY_FEATURE IN FM.MY_PROJECT AS 1;") Whether that CREATE succeeds is a namespace permissions question, not a role question. On a fresh project the seeded PUBLIC allows let it through.
Working with a team
Personal orgs cannot be shared, so a team starts with a regular org.
client.create_org(org_name="acme") # 2-16 chars, globally unique
client.create_project(org="acme", project_name="data") # needs ADMIN or OWNER in the org Invite people
client.invite_to_org(user_emails=["dana@acme.com"], org="acme", role="ADMIN")
client.invite_to_project(user_emails=["dana@acme.com"], project="acme/data", role="READ_WRITE") Org invitations take OWNER or ADMIN; project invitations also accept READ_WRITE and READ_ONLY. Each one creates a pending invitation and sends an email.
You cannot invite someone who already has access. To change what they have, use set_project_roles() — or revoke first, then invite again.
They accept
Invitees log in at cloud.featuremesh.com with the email you used. If they have no account yet, they register and pending invitations attach to it automatically.
client.accept_invitation(invitation_id=invitation_id)
client.decline_invitation(invitation_id=invitation_id) An invitation can be accepted once. Declining is final — you need a fresh invitation. Ignoring one expires it after 30 days.
Cancel invitations
client.cancel_invitations(user_emails=["dana@acme.com"], project="acme/data")
client.cancel_invitations(user_emails=["dana@acme.com"], org="acme") The sender can cancel their own invitations, and any OWNER of the org or project can cancel anyone's. An ADMIN is limited to invitations they sent themselves.
Seeing who has what
client.list_orgs() # orgs where you hold a role
client.list_org_members(org="acme") # OWNER and ADMIN only
client.list_projects(org="acme")
client.list_project_members(project="acme/data") # OWNER and ADMIN only
client.list_invitations(scope="sent") # or "received" / "all"; optional org= / project= filters
client.list_my_roles()
client.list_my_roles(org="acme")
client.list_my_roles(project="acme/data") Member listing is restricted on purpose: only OWNER and ADMIN can enumerate members, so nobody can map out an org they do not administer. Everyone else gets PERMISSION-DENIED.
Changing roles
client.set_org_roles(user_emails=["dana@acme.com"], org="acme", role="ADMIN")
client.revoke_org_roles(user_emails=["dana@acme.com"], org="acme")
client.set_project_roles(user_emails=["dana@acme.com"], project="acme/data", role="READ_ONLY")
client.revoke_project_roles(user_emails=["dana@acme.com"], project="acme/data") An ADMIN may only touch READ_WRITE and READ_ONLY roles. An OWNER may change anything, including other OWNERs — the sole guardrail is that the last OWNER cannot be revoked.
Handing things over
There is no single "transfer" call for ownership. You add the new owner, then remove yourself, which means the org or project is never ownerless:
client.set_project_roles(user_emails=["dana@acme.com"], project="acme/data", role="OWNER")
client.revoke_project_roles(user_emails=["me@acme.com"], project="acme/data") Org ownership works identically with set_org_roles() and revoke_org_roles().
Moving a project between orgs is one call:
client.transfer_project_to_org(project="acme/data", new_org="corp") You must be OWNER of the project and at least ADMIN in the destination org. The reference becomes corp/data; the features come along untouched.
Deleting
Deletion is two-step by design. The first call returns a validation code, which you type into the second.
client.delete_project(project="acme/data")
client.delete_project(project="acme/data", validation_code="the_code_you_got") client.delete_org(org="acme")
client.delete_org(org="acme", validation_code="the_code_you_got") Both are irreversible and OWNER-only, and codes expire after five minutes. Before deleting, you must remove every other user — and, for an org, every project. The point is to make you wind the thing down consciously rather than lose a shared workspace to one command. Personal orgs cannot be deleted at all.
Service accounts
A service account is an API-only identity scoped to one project: no password, no UI login, just a token. Use them for pipelines, training jobs, and anything else that runs without a human.
The rules worth knowing before you create one:
- They live in a project, and never in a personal org's project.
- Each has an owner — whoever created it — and only a project
OWNERorADMINcan create one. - Their role is
READ_ONLYorREAD_WRITE, neverOWNERorADMIN, and never above their owner's own permissions. - If the owner loses access to the project, the service account is deleted with them, and its tokens die immediately.
result = client.create_service_account(
project="acme/data",
name="ml-training-bot",
role="READ_WRITE",
)
service_account_id = result["service_account"]["id"]
for sa in client.list_service_accounts(project="acme/data"):
print(f"{sa['name']}: {sa['role']} (owner: {sa['owner_email']})")
client.delete_service_account(service_account_id=service_account_id) Tokens last 28 days and are minted by the owner:
token_result = client.generate_service_account_token(service_account_id=service_account_id)
sa_token = token_result["token"]
sa_client = BatchClient(
access_token=sa_token,
backend=Backend.DUCKDB,
sql_executor=lambda sql: duckdb.sql(sql).df(),
)
sa_client.query("RETURN feature1 := 1, feature2 := 2;") Refreshing its own token is the only access-management operation a service account may perform:
sa_access_client = featuremesh.create_access_client(sa_token)
refresh_result = sa_access_client.refresh_service_account_token()
new_sa_token = refresh_result["token"] It cannot create or delete service accounts, mint tokens for others, or touch orgs, projects, roles, or invitations. What it can do is query the project and mutate whatever namespace policy allows it.
In practice: one account per service, named for the service (ml-training-service, data-pipeline-bot), scoped down with namespace permissions rather than left on the project default, and refreshed before day 28. Its actions appear in the audit log under the owner's user id.
Tokens and audit
client.invalidate_token() # your current token, now
client.invalidate_all_identity_tokens(user_email="dana@acme.com") # org OWNER only
client.get_audit_log(org="acme", limit=100)
client.get_audit_log(project="acme/data", limit=100) Invalidation records a timestamp rather than a blocklist: every token issued before it stops working, and anything minted afterwards is fine. That is what makes rotation safe — invalidate, then generate.
Audit logs record who did what, when, and from which IP. They are available to OWNER and ADMIN of the org or project; READ_WRITE and READ_ONLY members cannot read them.
Error reference
| Code | Meaning |
|---|---|
PERMISSION-DENIED | You lack the required role |
NOT-FOUND | The org, project, or user does not exist |
ALREADY-EXISTS | Duplicate name |
LAST-OWNER | Cannot remove the last OWNER |
INVALID-EMAIL | Malformed email address |
INVITATION-EXPIRED | Older than 30 days |
INVALID-TOKEN | Expired or invalidated |
USERS-STILL-PRESENT | Other users still have access to the project or org |
PROJECTS-STILL-PRESENT | The org still contains projects |
VALIDATION-REQUIRED | Deletion needs the validation_code returned in the error |
A denial on a CREATE FEATURE is a different animal — that is UE/NAMESPACE-PERMISSION-DENIED from the FeatureQL compiler, not an access-management error.
Related
- Access management — the model behind these calls
- Namespace permissions — deciding who may write and delete features
- Tools overview — the rest of the tools surface