Add or rename a role
Roles are a single key:value map in config.js. Higher numbered ranks express greater access.
ROLE_RANKS: { viewer: 10, editor: 20, admin: 30, superadmin: 40 },
Above, superadmin was added and now outranks everything below it. requireAuth('superadmin') can be used immediately. The gaps (10, 20, 30) leave room to slot roles in between without renumbering. For example auditor: 25. The auth sheet’s role dropdown is a snapshot of the ROLES list from the moment SETUP_createAuthSheet() ran; re-running it creates a new sheet rather than updating the existing one. To offer a new role on an existing sheet, add the value to the Role column’s data-validation rule by hand.
Any additional role can be defined. However, renaming the built-in admin or editor will break functionality as both names are load-bearing. The core administrative guards call requireAuth('admin') literally (auth-setup.js, auth-scopes.js), and domain mapping returns literal 'admin' and 'editor' (resolveDomainRole_ in auth-adapter.js). If you must rename them, only do so with matching changes in all three files.
Renaming any role after deployment also orphans existing rows in the auth sheet: a stored role name that no longer appears in ROLE_RANKS fails the membership check and the user is denied. It fails closed and is safe, but is unlikely to reflect desired functionality. A rename in sheet mode therefore also means editing the sheet, in order: update the Role column’s data-validation rule first – it was created with the old names and rejects anything else – then change each stored role to the new name.
The client UX guard in client/auth-api.html (hasUserRole()) reads the role order from the server, which getUserInfo() returns on the user object, so there is no second copy to keep in sync. That guard is UX only: getting it wrong shows or hides a panel, it does not weaken the server guard.
Add a guarded server endpoint
Open the function with a guard, then do the work. The guard returns the verified actor.
function archiveProject(projectId) {
const auth = requireAuth('editor'); // first line, always
// ... do the work, attribute it to auth.email
return { ok: true, by: auth.email };
}
Never trust a role or email sent up from the client. Resolve it on the server.
Call it from the client
Add the endpoint to the api object, then call it like any other.
Object.assign(api, {
archiveProject: (id) => invoke('archiveProject', [id]),
});
api.archiveProject('p_123')
.then(result => console.log('archived by', result.by))
.catch(err => console.error(err.message)); // 'AUTH_ERROR: editor access required'
invoke surfaces AUTH_ERROR: messages through the wrapper’s handler, so an under-privileged call fails with a clear message rather than a silent no-op.
Gate UI by role
Reveal a control only for roles that qualify. Remember this is tidiness, the server still guards the data.
requireRole('admin', () => show('admin-panel'), () => {});
Use scopes for multi-tenant separation
Roles answer “what may this user do”. Scopes answer “which slice of data”. A scope is an opaque tag, a tenant or team or region, on both users and resources. The module is in auth-scopes.js and is optional; delete the file if you do not need it.
Attach scopes to a user (admin only):
setUserScopes('alice@example.com', ['tenant_a', 'tenant_b']);
Guard a scoped route. The caller must meet the role and hold the scope:
function getTenantReport(tenantId) {
const auth = requireScope(tenantId, 'editor');
// ... return only data for tenantId
}
Admins are not exempt from scope checks by default. If you want an admin to see every scope, check the role yourself and branch:
function getTenantReport(tenantId) {
const auth = requireAuth('viewer');
if (auth.role !== 'admin' && !userHasScope(tenantId)) {
throw new Error(`AUTH_ERROR: no access to scope '${tenantId}'`);
}
// ...
}
(userHasScope with no email checks the current user. Passing an explicit email reads someone else’s scopes and requires admin.)
Switch auth modes
Set USE_DOMAIN_AUTH: true and fill the allowlists in config.js. Application code does not change, because both auth modes route through the same adapter and the same hierarchy. See Architecture.