Membership in TeleQuick has two independent layers. An org role is account-wide. A grant says which product a member may open and at what level. The Members screen (Admin → Members) edits both, plus the pending invite list.

Org roles vs per-product grants

The org role is account-wide: it covers things like invites, SSO, billing and branding, and it decides who may administer other members. It does not enumerate products. The member-administration procedures behind this screen (admin.setGrant, admin.updateMemberRole, admin.removeMember) accept owner and admin only. Owners and admins also pass every grant gate implicitly, so their rows reach every product regardless of what the grant matrix shows for them. For everyone else, product access comes entirely from grants. A member with no grant rows is in the organization and can reach no product console. admin.listMembers returns userId, email, name, role and createdAt. Identities are resolved through the public.user_identities RPC rather than read directly from the auth schema. name is an empty string when the account has no name recorded — the invite flow and password signup do not set one — so the screen falls back to the email address.

The grant levels and which products accept them

admin.setGrant takes one modality and one role. Passing role: null deletes the grant row, which the screen shows as No access. These are the grantable products, with the console each one opens: Expand the Product access cell on a member row to see and edit the full matrix. The count on the button is the number of products with a grant row. Writes are idempotent: if the submitted level equals the stored level, setGrant returns without touching the row or writing an audit entry. Otherwise it upserts on (org_id, user_id, modality) and records granted_by.

Access presets on the invite form

The invite form offers presets instead of an eleven-row grid, because the common cases are few. Each preset sets both the org role and the grant map; you can still adjust either afterwards, which flips the selector to Custom. The screen always sends the grants object explicitly, including when it is empty. An omitted grants field means “older client, synthesise a grant set from the role” on the server, which is not the same thing as the deliberate empty set behind No access yet.

Guard rails: last owner, owner-only changes, no self-edits

The server enforces these, not the UI. They return errors even if a client calls the procedure directly.
  • No self-edits. setGrant rejects a userId equal to the caller with BAD_REQUEST — “Cannot change your own access — ask another admin”. This stops an admin quietly widening their own product access without a second pair of eyes. updateMemberRole applies the same rule to the org role, which also prevents accidental self-lockout.
  • Owner changes are owner-only. Only an owner may grant the owner role or change a member who already holds it.
  • The last owner cannot be demoted. updateMemberRole refuses the change that would leave the org with no owner. removeMember mirrors the same checks.
  • The target must be a member. setGrant looks the user up in org_member first and returns NOT_FOUND — “Member not in this org” — if there is no row. You cannot pre-grant products to someone who has not accepted an invite.
Removing a member takes effect immediately: they lose access to the organization at once. Any pending invite for that address is unaffected, and you can invite them again later.

The invite lifecycle: send, resend, accept, expire, revoke

admin.inviteMember takes orgId, email, role and grants. The invite row in org_invite carries id, email, role, grants, created_at, expires_at and accepted_at. admin.listInvites returns only rows where accepted_at is null, newest first. So an invite leaves the Pending invites table as soon as it is accepted — the member then appears in the Members table instead. The Expires column renders the row’s expires_at; once that moment passes the link no longer works and you send a fresh invite. Inviting an address that is already a member does not create a second invitation. The mutation returns already_member: true together with a message, and no acceptance link. The screen surfaces the message as a notice rather than falling into the “email not delivered, copy the link” path, which would otherwise promise a link that never renders. admin.revokeInvite takes orgId and inviteId. Revoking stops the invite link from working. You can send a new invite to the same address at any time.

When an invite email is not delivered

The invite response includes a mail_delivered flag:
  • mail_delivered: true — the invitation was emailed and nothing else is needed.
  • mail_delivered: false — the invite exists but the message did not go out. The response then carries accept_url (or magic_link), and the screen renders that URL so you can hand it to the invitee out of band.
Treat the link as a credential: whoever opens it joins the org with the role and grants recorded on the invite. If you would rather not pass it around, revoke the invite and send a new one.

Entitlements bound every grant

Grants only ever narrow access. Your plan’s entitlements and the product list your org is provisioned for still decide what the organization can reach at all. Granting streams to a member of an org without the Stream entitlement gets them nothing — the grant row exists, but the gate above it still fails. Two consequences worth remembering when you debug a “why can’t they see it” report:
  1. If nobody in the org can open a product, the problem is the entitlement, not the grant.
  2. If one person cannot open a product that colleagues can, the problem is the grant — unless that colleague is an owner or admin, who reach everything regardless of the matrix.

Auditing membership changes

Grant changes write an audit entry through recordAudit: Because a no-op write returns early, repeated saves of the same level do not produce duplicate audit rows. The granted_by column on org_member_grant independently records who last set the surviving grant.

Procedures behind this screen

  • Authentication — API keys and relay tokens, which are scoped separately from membership