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

@cfast/admin

v0.2.0

Published

Auto-generated admin UI from your Drizzle schema with role management and impersonation

Downloads

299

Readme

@cfast/admin

A complete admin panel generated from your Drizzle schema. With role management and user impersonation.

@cfast/admin gives you a production-ready admin UI derived from your database schema. It's not a generic CRUD generator. It understands your permission system, your auth setup, and your data relationships. Every table gets a list view, detail view, create form, and edit form. Users get a role management panel. Admins get impersonation.

You add one route to your React Router app, and you have an admin panel.

Why This Exists

Building an admin panel is the same work every time: list pages, detail pages, create/edit forms, user management, role assignment. The structure is always the same — only the schema changes.

@cfast/admin automates the schema → configuration step. It reads your Drizzle tables, infers column types, and generates the configuration for @cfast/ui components. The actual rendering is delegated entirely to @cfast/ui — admin doesn't have its own component library.

This means:

  • Apps that don't use the admin panel still get <ListView>, <DetailView>, and <DataTable> from @cfast/ui
  • Custom admin overrides use the same components as the rest of the app
  • Admin stays thin and focused on auto-generation

Design Goals

  • One route, full admin. Mount the admin at /admin and you're done. Every table, every relationship, every action.
  • Permission-aware by default. The admin panel uses @cfast/db under the hood. Admins see everything. Moderators see what moderators see. The admin UI doesn't bypass your permission system — it uses it.
  • User management built in. View users, assign roles, revoke roles, impersonate users. Integrated with @cfast/auth.
  • Customizable, not locked in. Override any view, any field, any action. But the default is good enough to ship.
  • UI delegated to @cfast/ui. Admin generates configuration. @cfast/joy renders it.

API

Minimal Setup

// app/routes/admin.tsx
import { createAdmin } from "@cfast/admin";
import type { AdminAuthConfig } from "@cfast/admin";
import * as schema from "~/schema";

// Auth adapter — bridges your app's auth to admin's interface
const auth: AdminAuthConfig = {
  async requireUser(request) {
    // Return { user: AdminUser, grants: unknown[] }
    const session = await getSession(request);
    return { user: session.user, grants: session.grants };
  },
  hasRole: (user, role) => user.roles.includes(role),
  getRoles: (userId) => authInstance.getRoles(userId),
  setRole: (userId, role) => authInstance.setRole(userId, role),
  removeRole: (userId, role) => authInstance.removeRole(userId, role),
  setRoles: (userId, roles) => authInstance.setRoles(userId, roles),
  impersonate: (adminId, targetId, request) => { /* ... */ },
  stopImpersonation: (request) => { /* ... */ },
};

// DB factory — called per-request with grants and user context
function db(grants: unknown[], user: { id: string } | null) {
  return createDb({ d1: env.DB, schema, grants, user });
}

const admin = createAdmin({
  db,
  auth,
  schema,
  requiredRole: "admin", // Role required to access admin (default: "admin")
});

// React Router route:
export const loader = admin.loader;
export const action = admin.action;
export default admin.Component;

Server/Client Splitting

For React Router apps where server code must not leak into client bundles, use the individual factories:

// app/admin.server.ts — server only
import { createAdminLoader, createAdminAction, introspectSchema } from "@cfast/admin";

const tableMetas = introspectSchema(schema);
export const adminLoader = createAdminLoader(config, tableMetas);
export const adminAction = createAdminAction(config, tableMetas);
// app/routes/admin.tsx — safe for client bundle
import { createAdminComponent, introspectSchema } from "@cfast/admin";
import { adminLoader, adminAction } from "~/admin.server";

const tableMetas = introspectSchema(schema);
const AdminComponent = createAdminComponent(tableMetas);

export const loader = adminLoader;
export const action = adminAction;
export default AdminComponent;

Table Configuration

Customize how tables appear in the admin:

createAdmin({
  db,
  auth,
  schema,
  tables: {
    posts: {
      label: "Blog Posts",
      listColumns: ["title", "author", "published", "createdAt"],
      searchable: ["title", "content"],
      defaultSort: { column: "createdAt", direction: "desc" },
      exclude: false, // Set true to hide a table from admin
      fields: {
        content: { component: RichTextEditor },
      },
    },
    // Tables not listed here use sensible defaults
    // Auth-internal tables (session, account, verification, passkey) are auto-excluded
  },
});

User Management

Built-in views for managing users and roles:

createAdmin({
  // ...
  users: {
    // Which roles can be assigned through the admin UI
    // (respects auth.roleGrants for who can assign what)
    assignableRoles: ["user", "editor", "moderator", "admin"],
  },
});

The admin automatically provides:

  • User list with search and filters (via @cfast/ui's <ListView>)
  • User detail page with profile info and activity (via @cfast/ui's <DetailView>)
  • Role assignment panel (respects roleGrants from @cfast/auth)
  • Impersonation button (for authorized roles) - starts an impersonation session via @cfast/auth

Impersonation UX

When an admin impersonates a user:

  1. The admin panel shows an impersonation banner with a "Stop Impersonation" button
  2. The rest of the app behaves as that user (same session, same permissions)
  3. Impersonation start/stop is handled by the auth.impersonate and auth.stopImpersonation callbacks you provide
  4. Audit logging is the responsibility of your auth adapter (see the example in Minimal Setup)

Custom Actions

Add table-level or row-level actions:

createAdmin({
  // ...
  tables: {
    posts: {
      actions: {
        row: [
          {
            label: "Publish",
            action: async (id: string, formData: FormData) => {
              // Custom logic — called with the record ID and form data
            },
            confirm: "Are you sure you want to publish?", // Optional confirmation dialog
            variant: "default", // "default" | "danger" — controls button styling
          },
        ],
        table: [
          {
            label: "Export CSV",
            handler: async (selectedIds: string[]) => {
              // Called with an array of selected record IDs
            },
          },
        ],
      },
    },
  },
});

Dashboard

The admin index page shows an overview dashboard:

createAdmin({
  // ...
  dashboard: {
    widgets: [
      { type: "count", table: "users", label: "Total Users" },
      { type: "count", table: "posts", label: "Published Posts", where: { published: true } },
      { type: "recent", table: "posts", limit: 5, label: "Recent Posts" },
    ],
  },
});

How It Works

@cfast/admin is a thin layer that does two things:

  1. Schema introspection — reads your Drizzle schema to generate configuration: which columns to show, which fields to use in forms, which relations to resolve, which actions to offer
  2. Configuration → UI components — passes that configuration to @cfast/ui components for rendering

The rendering stack:

| Admin feature | Rendered with | |---|---| | Create/edit forms | @cfast/forms' <AutoForm> | | User role display | @cfast/ui's <RoleBadge> | | User avatars | @cfast/ui's <AvatarWithInitials> | | Confirm dialogs | @cfast/ui's useConfirm | | List, detail, dashboard views | Built-in admin components (MUI Joy) | | Navigation sidebar | Built-in admin components (MUI Joy) |

Integration

  • @cfast/ui — All rendering. Admin generates configuration, UI renders pixels.
  • @cfast/db — All data access. Every CRUD operation goes through permission-checked Operations via .run().
  • @cfast/auth — User management, role assignment, and impersonation.
  • @cfast/forms — Create/edit forms via <AutoForm>.
  • @cfast/actions — Custom row and table actions, permission-aware.
  • @cfast/permissions — The admin respects the permission system. An editor role in the admin sees what editors see.
  • @cfast/pagination — List views paginate via @cfast/pagination hooks.

The admin is not a separate app. It's a React Router route that uses the same database, same permissions, same auth as the rest of your application.