Skip to main content

vanilla js

Framework-agnostic AccessClient with subscribe, merge, fromServer, and helpers for any JS runtime. Foundation for Svelte, Solid, and custom integrations.

Install

import { AccessClient } from '@gentleduck/iam/client/vanilla'
import { AccessClient } from '@gentleduck/iam/client/vanilla'

Zero dependencies. Works in any JS runtime that supports fetch (browser, Node, Bun, Deno, edge runtimes).


Basic usage

Create an AccessClient from a permission map:

import { AccessClient } from '@gentleduck/iam/client/vanilla'
 
const access = new AccessClient(permissionsFromServer)
 
access.can('delete', 'post') // true
access.cannot('manage', 'billing') // true
access.can('manage', 'user', undefined, 'admin') // scoped check
import { AccessClient } from '@gentleduck/iam/client/vanilla'
 
const access = new AccessClient(permissionsFromServer)
 
access.can('delete', 'post') // true
access.cannot('manage', 'billing') // true
access.can('manage', 'user', undefined, 'admin') // scoped check

Fetch from server

fromServer sends a GET request, parses the JSON response as a PermissionMap, and returns a new AccessClient. It throws if the response is not OK (non-2xx status).

const access = await AccessClient.fromServer('/api/me/permissions', {
  headers: { Authorization: 'Bearer ' + token },
})
 
access.can('delete', 'post') // boolean
const access = await AccessClient.fromServer('/api/me/permissions', {
  headers: { Authorization: 'Bearer ' + token },
})
 
access.can('delete', 'post') // boolean

The second argument is a standard RequestInit. A Content-Type: application/json header is added automatically.


Allowed actions

allowedActions scans the permission map and returns a deduplicated array of action strings where the value is true. hasAnyOn short-circuits and returns as soon as it finds any allowed permission.

access.allowedActions('post') // ['read', 'create', 'update']
access.hasAnyOn('billing') // false
access.hasAnyOn('post') // true
access.allowedActions('post') // ['read', 'create', 'update']
access.hasAnyOn('billing') // false
access.hasAnyOn('post') // true

Both methods handle all four key formats. For a 3-part key like "org-1:read:post", the method correctly identifies "post" as the resource and "read" as the action.

These helpers suit UI summarization: navigation menus, toolbar groups, or coarse "does this user have anything on posts?" checks. They scan the existing PermissionMap and do not replace server-side permission evaluation.


Reactive updates

Subscribe to permission changes to trigger re-renders in any framework.

const unsubscribe = access.subscribe((newPermissions) => {
  // Re-render your UI with the new permissions
  rerender()
})
 
// Update permissions (e.g. after role change)
access.update(newPermissionsFromServer)
 
// Merge new permissions into existing ones
access.merge({ 'manage:team': true })
 
// Clean up
unsubscribe()
const unsubscribe = access.subscribe((newPermissions) => {
  // Re-render your UI with the new permissions
  rerender()
})
 
// Update permissions (e.g. after role change)
access.update(newPermissionsFromServer)
 
// Merge new permissions into existing ones
access.merge({ 'manage:team': true })
 
// Clean up
unsubscribe()

update replaces the entire map. merge shallow-merges new entries into the existing map. Both notify subscribers in the order they registered.


Framework integration patterns

Svelte

// stores/access.ts
import { writable, derived } from 'svelte/store'
import { AccessClient } from '@gentleduck/iam/client/vanilla'
 
const client = new AccessClient(initialPermissions)
const permissions = writable(client.permissions)
 
client.subscribe((perms) => permissions.set(perms))
 
export const can = (action: string, resource: string) =>
  derived(permissions, () => client.can(action, resource))
// stores/access.ts
import { writable, derived } from 'svelte/store'
import { AccessClient } from '@gentleduck/iam/client/vanilla'
 
const client = new AccessClient(initialPermissions)
const permissions = writable(client.permissions)
 
client.subscribe((perms) => permissions.set(perms))
 
export const can = (action: string, resource: string) =>
  derived(permissions, () => client.can(action, resource))

Solid

import { createSignal } from 'solid-js'
import { AccessClient } from '@gentleduck/iam/client/vanilla'
 
const client = new AccessClient(initialPermissions)
const [permissions, setPermissions] = createSignal(client.permissions)
 
client.subscribe(setPermissions)
 
export const can = (action: string, resource: string) => () => client.can(action, resource)
import { createSignal } from 'solid-js'
import { AccessClient } from '@gentleduck/iam/client/vanilla'
 
const client = new AccessClient(initialPermissions)
const [permissions, setPermissions] = createSignal(client.permissions)
 
client.subscribe(setPermissions)
 
export const can = (action: string, resource: string) => () => client.can(action, resource)

Web Components

class AccessGate extends HTMLElement {
  connectedCallback() {
    const action = this.getAttribute('action')
    const resource = this.getAttribute('resource')
 
    const render = () => {
      this.style.display = client.can(action, resource) ? '' : 'none'
    }
 
    render()
    this._unsub = client.subscribe(render)
  }
 
  disconnectedCallback() {
    this._unsub?.()
  }
}
 
customElements.define('access-gate', AccessGate)
class AccessGate extends HTMLElement {
  connectedCallback() {
    const action = this.getAttribute('action')
    const resource = this.getAttribute('resource')
 
    const render = () => {
      this.style.display = client.can(action, resource) ? '' : 'none'
    }
 
    render()
    this._unsub = client.subscribe(render)
  }
 
  disconnectedCallback() {
    this._unsub?.()
  }
}
 
customElements.define('access-gate', AccessGate)
<access-gate action="delete" resource="post">
  <button>Delete</button>
</access-gate>
<access-gate action="delete" resource="post">
  <button>Delete</button>
</access-gate>

API reference

Method / PropertyTypeDescription
constructor(perms?)new (PermissionMap?) -> AccessClientCreate from a permission map
AccessClient.fromServer(url, init?)static async -> AccessClientFetch permissions from an endpoint
can(action, resource, resourceId?, scope?)-> booleanCheck if allowed
cannot(action, resource, resourceId?, scope?)-> booleanCheck if denied
allowedActions(resource)-> string[]List all allowed actions for a resource
hasAnyOn(resource)-> booleanCheck if any permission exists on a resource
permissionsReadonly<PermissionMap>Current permission map (read-only)
update(perms)-> voidReplace permissions and notify listeners
merge(perms)-> voidMerge new permissions into existing and notify
subscribe(fn)-> () -> voidListen for changes, returns unsubscribe function

When to use

  • Non-React, non-Vue frontends (Svelte, Solid, Lit, custom)
  • Web Components / micro-frontends
  • Browser extensions, Electron, Tauri
  • Background scripts, service workers, web workers
  • Tests where you want a quick subscribable store