npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@douglance/stdb-rbac

v1.0.0

Published

Production-ready Role-Based Access Control (RBAC) for SpacetimeDB with React hooks

Downloads

12

Readme

@douglance/stdb-rbac

Production-ready Role-Based Access Control (RBAC) for SpacetimeDB with React hooks.

Eliminate 400+ lines of boilerplate per project with a standardized, type-safe RBAC system that works across your entire SpacetimeDB toolkit.

Features

  • 🔐 Admin-only operations - Secure grantRole/revokeRole reducers
  • 🚀 Zero-config bootstrap - First user automatically becomes admin
  • ⚛️ React hooks - useIsAdmin(), useHasRole(), useGrantRole()
  • 🔄 Real-time - Role changes propagate instantly to all clients
  • 🛡️ Type-safe - Full TypeScript support
  • 🧪 Idempotent - Duplicate role grants handled gracefully
  • 📦 Composable - Import tables into your SpacetimeDB module

Installation

bun add @douglance/stdb-rbac

Quick Start

1. Server-Side (SpacetimeDB Module)

// stdb-module/src/schema.ts
import { schema } from "spacetimedb/server";
import { rbacTables, registerRbacReducers } from "@douglance/stdb-rbac/module";
import { myGameTables } from "./tables";

// Compose RBAC tables with your game tables
const gameSchema = schema(...rbacTables, ...myGameTables);

// Register RBAC reducers (grantRole, revokeRole, clientConnected)
registerRbacReducers(gameSchema);

export default gameSchema;

That's it! Your module now has:

  • Role table (role_name, description)
  • UserRole table (user_identity, role_name)
  • grantRole reducer (admin-only)
  • revokeRole reducer (admin-only)
  • First-user-is-admin bootstrap

2. Client-Side (React App)

import { useIsAdmin, useGrantRole } from "@douglance/stdb-rbac";
import { useIdentity } from "@douglance/stdb-react-companion";

function AdminPanel() {
  const identity = useIdentity();
  const isAdmin = useIsAdmin(identity);
  const { call: grantRole, loading } = useGrantRole();

  if (!isAdmin) {
    return <div>Access denied. Admins only.</div>;
  }

  const handleGrantModerator = async (userId) => {
    await grantRole({
      user_identity: userId,
      role_name: "moderator",
    });
  };

  return (
    <div>
      <h1>Admin Panel</h1>
      <button onClick={() => handleGrantModerator(someUserId)} disabled={loading}>
        Grant Moderator Role
      </button>
    </div>
  );
}

API Reference

Server-Side Exports

rbacTables

Array of RBAC table definitions to spread into your schema.

import { rbacTables } from "@douglance/stdb-rbac/module";

const gameSchema = schema(...rbacTables, ...myGameTables);

registerRbacReducers(schema)

Registers all RBAC reducers on your schema:

  • clientConnected - Grants admin role to first user
  • grantRole(user_identity, role_name) - Admin-only role assignment
  • revokeRole(user_identity, role_name) - Admin-only role revocation
import { registerRbacReducers } from "@douglance/stdb-rbac/module";

registerRbacReducers(gameSchema);

Client-Side Hooks

useUserRoles(userId: Identity | null): string[]

Subscribe to all roles for a specific user.

const roles = useUserRoles(identity);
// roles = ["admin", "moderator"]

useHasRole(userId: Identity | null, roleName: string): boolean

Check if user has a specific role.

const hasModeratorRole = useHasRole(identity, "moderator");

if (hasModeratorRole) {
  return <ModeratorTools />;
}

useIsAdmin(userId: Identity | null): boolean

Convenience hook to check for admin role.

const isAdmin = useIsAdmin(identity);

if (!isAdmin) {
  return <div>Access denied</div>;
}

useAllRoles(): Role[]

Subscribe to all registered roles in the system.

const roles = useAllRoles();

return (
  <select>
    {roles.map(r => (
      <option key={r.role_name} value={r.role_name}>
        {r.role_name} - {r.description}
      </option>
    ))}
  </select>
);

useGrantRole()

Hook for calling the grantRole reducer.

const { call: grantRole, loading, error } = useGrantRole();

await grantRole({
  user_identity: someUserId,
  role_name: "moderator"
});

useRevokeRole()

Hook for calling the revokeRole reducer.

const { call: revokeRole, loading, error } = useRevokeRole();

await revokeRole({
  user_identity: someUserId,
  role_name: "moderator"
});

useAllUserRoles(): UserRole[]

Get all user-role assignments (admin use only).

const userRoles = useAllUserRoles();

return (
  <table>
    {userRoles.map(ur => (
      <tr key={`${ur.user_identity}-${ur.role_name}`}>
        <td>{ur.user_identity.toHexString()}</td>
        <td>{ur.role_name}</td>
      </tr>
    ))}
  </table>
);

Database Schema

Role Table

| Column | Type | Description | |--------|------|-------------| | role_name | string (PK) | Unique role name (e.g., "admin", "moderator") | | description | string | Human-readable description |

UserRole Table

| Column | Type | Description | |--------|------|-------------| | id | u64 (PK, auto-inc) | Auto-generated ID | | user_identity | Identity | User's SpacetimeDB Identity | | role_name | string | Role name (foreign key to Role.role_name) |

How It Works

Bootstrap Process

  1. First Connection: When the first user connects to your SpacetimeDB module:

    • clientConnected hook checks if any admins exist
    • If none exist, creates "admin" role and grants it to the connecting user
  2. Subsequent Connections: All other users connect without special privileges

Permission Model

  • Admin-only operations: grantRole and revokeRole check if ctx.sender has "admin" role
  • Last admin protection: Cannot revoke admin role if only one admin exists
  • Idempotent grants: Calling grantRole multiple times for the same user+role is safe

Custom Roles

Applications can create custom roles by calling grantRole with any role name:

await grantRole({ user_identity: userId, role_name: "vip_member" });
await grantRole({ user_identity: userId, role_name: "content_creator" });

Roles are created automatically on first grant.

Example: Game Admin System

// Server module
import { rbacTables, registerRbacReducers } from "@douglance/stdb-rbac/module";

const gameSchema = schema(...rbacTables, GameEntity, PlayerStats);
registerRbacReducers(gameSchema);

// Admin-only reducer
gameSchema.reducer("spawnBoss", { bossType: t.string() }, (ctx, args) => {
  // Check if caller is admin
  let isAdmin = false;
  for (const ur of ctx.db.UserRole.iter()) {
    if (ur.user_identity.isEqual(ctx.sender) && ur.role_name === "admin") {
      isAdmin = true;
      break;
    }
  }

  if (!isAdmin) {
    throw new Error("Only admins can spawn bosses");
  }

  // Spawn boss...
});
// Client app
function GameAdminPanel() {
  const identity = useIdentity();
  const isAdmin = useIsAdmin(identity);
  const { call: spawnBoss } = useReducer("spawnBoss");

  if (!isAdmin) return null;

  return (
    <div>
      <h2>Admin Controls</h2>
      <button onClick={() => spawnBoss({ bossType: "dragon" })}>
        Spawn Dragon Boss
      </button>
    </div>
  );
}

Best Practices

1. Gate UI, Not Just Reducers

Always hide admin UI from non-admins:

const isAdmin = useIsAdmin(identity);

if (!isAdmin) {
  return null; // Don't render admin UI at all
}

2. Check Roles in Reducers

Even if UI is gated, always verify permissions in reducers:

gameSchema.reducer("deleteUser", { userId: t.identity() }, (ctx, args) => {
  if (!hasRole(ctx, ctx.sender, "admin")) {
    throw new Error("Permission denied");
  }
  // ... delete logic
});

3. Use Specific Roles

Create roles for specific permissions instead of checking for "admin" everywhere:

// ✅ Good
const canModerate = useHasRole(identity, "moderator");

// ❌ Less flexible
const isAdmin = useIsAdmin(identity);

4. Document Custom Roles

If your app uses custom roles, document them:

/**
 * Custom roles:
 * - admin: Full access
 * - moderator: Can mute/kick users
 * - vip: Can access premium features
 * - content_creator: Can upload content
 */

Troubleshooting

"Permission denied" when calling grantRole

Cause: You're not an admin.

Solution: The first user to connect becomes admin automatically. Reconnect with a fresh database, or have an existing admin grant you the role.

Roles not updating in real-time

Cause: Missing useTable subscription.

Solution: The hooks automatically subscribe to table changes. Ensure your component is mounted and SpacetimeDBProvider is properly configured.

Cannot revoke last admin

Cause: Safety check prevents removing the last admin.

Solution: Grant admin role to another user first, then revoke from the original admin.

Migration from DIY RBAC

If you have an existing RBAC implementation:

  1. Export existing roles: Query your current UserRole table
  2. Deploy stdb-rbac: Add to your module schema
  3. Import roles: Use grantRole reducer to recreate role assignments
  4. Remove old code: Delete your custom RBAC tables and reducers
  5. Update client: Replace custom hooks with stdb-rbac hooks

License

MIT

Contributing

Issues and PRs welcome at [GitHub repository URL]