Documentation
ReferenceJavaScript SDKNamespacesauth.grants

auth.grants

Inviting people into a resource, and managing what they hold there.

A grant is one user holding one role on one resource. It is the only thing that exists at runtime — there is no "member" record behind it, which is why listing someone's grants is listing their organizations.

Server client only, and these act as the calling user — their own permission is enforced and they are recorded as the actor. Forward their headers with forwardUserHeaders. To act as the platform instead, use auth.admin.grants.

list

list(context?) → Grant[]

Every grant the calling user holds.

contextstringOptional

Narrows to grants whose role lives in this context — 'org', 'project'. Omit for everything.

This replaces a memberships table. To render "your organizations", list the grants in that context and look the resources up by resource:

const { data: grants } = await nulvless.auth.grants.list('org');
const orgs = await db.organizations.findBySlugs(grants.map((g) => g.resource));

getMembers

getMembers(resource) → GrantMembers

Everyone on one resource, already grouped by status.

const { data } = await nulvless.auth.grants.getMembers('acme');
resourcestringRequired

Whatever identifier you granted against — an org slug, a project id.

Returns four lists rather than one:

grantedGrant[]Optional
Active members.
invitationsGrant[]Optional
Invited, awaiting an answer.
requestedsGrant[]Optional
Asked to join, awaiting approval.
blockedsGrant[]Optional
Blocked — the record survives, the access does not.

A members screen renders these directly. No client-side bucketing, and no filtering a flat list by a status string you had to learn.


get

get(id) → Grant

One grant by id.

const { data: grant } = await nulvless.auth.grants.get(id);
idstringRequired

invite

invite(params, options) → Grant[]

Invites people to a resource. Batched — one call, many invitations.

invitesInvite[]Required

One entry per person. Each takes email plus a role_id, and optionally extra scopes on top of the role.

resourcestringRequired

The thing being shared. Not a context — the context came along with the role when it was defined.

messagestringOptional

Shown to the invitee. In practice the human-readable name of the resource, since resource itself is usually a slug.

noncestringOptional

Sudo confirmation.

const { data } = await nulvless.auth.grants.invite(
  {
    invites: [{ email: '[email protected]', role_id: adminRoleId }],
    resource: 'acme',
    message: 'Acme Inc',
  },
  {
    headers: forwardUserHeaders(req),
    redirect_uri: 'https://app.example.com/join',
  },
);

Three things that catch people

role_id is an id, not a name. 'admin' is rejected — read the ids once from auth.admin.roles.list(context).

redirect_uri is an option, not a param. It belongs in the second argument.

You invite an email, not a user. The person does not need an account. The grant comes back with status: 'invited' and no granted_to; that field fills in when they sign up.

Returns one Grant per invite. An invitation grants nothing until accepted — only granted passes a permission check.


accept

accept(id) → Grant

Takes a pending invitation to granted. Access starts here.

await nulvless.auth.grants.accept(grantId, {
  headers: forwardUserHeaders(req),
});

Needs no permission on the resource — the person does not have one yet, and the grant id is the authorization. Guard this route with authentication only. Requiring a role here is the most common way teams accidentally make invitations un-acceptable.


cancel

cancel(id) → Grant

Withdraws an invitation, or declines one.

await nulvless.auth.grants.cancel(grantId, {
  headers: forwardUserHeaders(req),
});

Same call from either side.


update

update(id, params) → Grant

Changes what one grant carries.

await nulvless.auth.grants.update(grantId, {
  scopes: ['projects:read'],
  nonce: code,
});
scopesstring[]Optional

Replaces the grant's scopes.

noncestringOptional

To change what everyone with a role can do, edit the role instead — every holder moves at once and no grant is touched. Reaching for update when you meant admin.roles.update is how a permission model drifts into per-person exceptions nobody can audit.


revoke

revoke({ id, nonce }) → Grant

Removes the grant. Tokens carrying it stop passing.

await nulvless.auth.grants.revoke({ id, nonce: code });
idstringRequired
noncestringOptional

block

block({ id, nonce }) → Grant

Keeps the record and denies the access.

await nulvless.auth.grants.block({ id, nonce: code });
idstringRequired
noncestringOptional

The difference from revoke matters when history does: a blocked grant still explains who took the actions attached to it, where a revoked one is gone.


leave

leave({ id, nonce }) → Grant

The user removing themselves.

await nulvless.auth.grants.leave({ id, nonce: code });
idstringRequired

The grant id — not the resource.

noncestringOptional

Self-service, so like accept it needs no role on the resource.


Who needs permission

The asymmetry is easy to get backwards:

invite, revoke, block, updateneeds a roleOptional

Acting on someone else's access. Typically owner or admin, and sudo-protected under manage_grants.

accept, cancel, leaveauthentication onlyOptional

Acting on your own. The grant id is the authorization.


The Grant object

idstringRequired
statusGrantStatusRequired
grantedActive. The only status that passes a check.
invitedSent, not answered.
requestedAsked for, not approved.
blockedDenied, record kept.
resourcestringRequired
granted_toUserOptional

Absent until the invitee has an account.

roleRoleOptional

Carries the key that verify({ roles }) matches against, and the context the role belongs to.

scopesstring[]Required
emailstringOptional

The invited address, before there is a user.

created_atstringOptional
updated_atstringOptional

Verifying against a grant that is still invited or requested answers 403 with forbidden_access_waiting_accept, not the generic forbidden_resource_access. That is the difference between "you have no access" and "your invitation is in your inbox".

Next

On this page