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

@everystack/admin

v0.4.13

Published

Declarative admin dashboard framework for PostgREST APIs

Readme

@everystack/admin

Declarative admin dashboard framework for PostgREST APIs. Configuration-driven CRUD screens, dashboard widgets, image field renderers, and data hooks — all wired to @everystack/api.

Install

pnpm add @everystack/admin @everystack/api @everystack/ui react react-native

Quick Start

Define your admin configuration and render with AdminRoot:

import { AdminRoot, EverystackAdapter } from '@everystack/admin';
import { createClient } from '@everystack/api/client';
import type { AdminConfig } from '@everystack/admin';

const client = createClient({ baseUrl: '/api', getToken: () => token });
const adapter = new EverystackAdapter({ client });

const config: AdminConfig = {
  resources: {
    users: {
      label: 'Users',
      fields: {
        id: { type: 'text', label: 'ID' },
        name: { type: 'text', label: 'Name' },
        email: { type: 'text', label: 'Email' },
        avatar: { type: 'image', label: 'Avatar' },
        createdAt: { type: 'datetime', label: 'Created' },
      },
      listFields: ['name', 'email', 'createdAt'],
      searchFields: ['name', 'email'],
    },
    posts: {
      label: 'Posts',
      fields: {
        id: { type: 'text', label: 'ID' },
        title: { type: 'text', label: 'Title' },
        content: { type: 'textarea', label: 'Content' },
        status: { type: 'select', label: 'Status', options: ['draft', 'published'] },
      },
    },
  },
  menu: [
    { label: 'Users', resource: 'users', icon: 'users' },
    { label: 'Posts', resource: 'posts', icon: 'file-text' },
  ],
};

function AdminApp() {
  return <AdminRoot config={config} adapter={adapter} />;
}

Entry Points

| Import | Description | |--------|-------------| | @everystack/admin | Config types, hooks, screens, adapter, components | | @everystack/admin/presets/twitter | Pre-built Twitter clone admin preset | | @everystack/admin/model-map | withModelMap, React-free (tests and server code can import it) |

Admin Configuration

Field Types

| Type | Description | |------|-------------| | text | Single-line text input | | textarea | Multi-line text | | number | Numeric input | | boolean | Toggle switch | | select | Dropdown with options array | | datetime | Date/time display | | image | Image upload with ImageFieldInput | | avatar | Avatar upload with AvatarFieldInput | | json | JSON display | | relation | Foreign key reference |

Resource Config

interface ResourceConfig {
  label: string;
  fields: Record<string, FieldDefinition>;
  listFields?: string[];      // Columns shown in list view
  searchFields?: string[];    // Fields searchable via text input
  defaultSort?: string;       // Default ordering (e.g., 'createdAt.desc')
  dashboard?: ResourceDashboardConfig; // Per-resource analytics
}

List search

With EverystackAdapter (supportsSearch: true), the search box searches listed string, email and URL fields by substring. Listed number and enum fields match exactly by default. Typing 42 matches numeric ID 42, not 142. Typing Active matches the configured enum value active, alongside text fields containing "Active".

Set a field's searchable: false to leave it out. An explicit searchable: true keeps substring matching, including for numbers and enums. Virtual fields are never searched. Enum fields need enumValues; automatic defaults only include fields in listFields, or all fields when listFields is unset. Adapters without supportsSearch: true keep searching the first field explicitly marked searchable: true.

For direct adapter calls, pass searchFields for substring matches and searchExact for typed equality in the same OR group. Lists and aggregates accept both:

await adapter.fetchList('users', {
  search: '42',
  searchFields: ['name'],
  searchExact: [
    { field: 'id', kind: 'number' },
    { field: 'status', kind: 'enum', values: ['active', 'archived'] },
  ],
});

Automatic numeric terms accept only whole numbers with at most ten digits and an absolute value at most 2147483647. Decimals and larger values still search text fields. Enum terms use the configured spelling after a case-insensitive match; values containing commas, parentheses, quotes, backslashes or asterisks are skipped. If no configured field can match non-empty text, the adapter returns an empty result without a request. Search text without either field list is an error. RPC-backed lists retain their first-field search; non-empty search text with no first search field returns an empty page without calling the RPC.

Relations from the model map

everystack db:generate writes db/model-map.generated.json from your models (see docs/model-map.md). Pass your resources through withModelMap and the admin reads relations from it instead of a hand-written copy:

import { withModelMap, type ModelMap } from '@everystack/admin/model-map';
import modelMap from '../db/model-map.generated.json';

const resources = withModelMap([
  { name: 'profiles', displayField: 'username', fields: [/* ... */] },
  { name: 'posts', fields: [{ name: 'authorId', type: 'relation' }, /* ... */] },
], modelMap as ModelMap);
  • Relation fields. A field typed 'relation' with no relation block gets one: the resource that reads the target table, the referenced key, and that resource's displayField. The list then shows alice instead of an id, and Edit offers a picker.
  • displayField is set once on the target resource. Without it, relations show the id.
  • Delete preview. A resource with no associations gets one per foreign key that points at its primary key. On a hard delete the preview states the database's rule for each ("the database refuses the delete while any exist", "deletes them too", "clears their link"). The admin only shows the rule; the database enforces it. Counts show the rows you can see. A resource whose model is lifecycle: true gets lifecycle: true from the map: its delete is a soft delete, no rule fires, and the preview says so. lifecycle also makes Show load a deleted row and offer Restore, which calls the adapter's restore (POST /<table>/restore). The table's delete rule decides whether the caller may restore. A child table no resource reads is counted by its bare name only when it is public.
  • Hand-written config wins. A relation block or an associations list (even []) is kept.
  • Left unfilled, showing the id: a target with no un-narrowed resource (a resource with a where would hide rows the key can point at), a target in another schema than the resource reads, a composite or private key, or a field whose name matches no foreign-key column.
  • Each unfilled relation field says why, once: withModelMap(resources, map, { onUnfilled }) receives { resource, field, reason, target? } (no-model, no-foreign-key, no-target-resource, no-key). Without onUnfilled it is a console.warn, except in production.
  • snake_case naming: a field spelled as the SQL column (author_id) is matched and gets the SQL key. A one-word column (owner) looks the same in both namings, so it gets the target key as a field key.
  • A map whose mapVersion this admin does not know is refused by number.

Data Adapter

The EverystackAdapter bridges admin hooks to the @everystack/api client:

import { EverystackAdapter } from '@everystack/admin';
import { createClient } from '@everystack/api/client';

const client = createClient({ baseUrl: '/api', getToken: () => token });

const adapter = new EverystackAdapter({
  client,
  // Optional: custom fetch for RPC calls
  rpcFetch: async (name, body) => { /* ... */ },
});

The adapter implements the DataAdapter interface:

interface DataAdapter {
  list(resource: string, params: ListParams): Promise<ListResult>;
  getOne(resource: string, id: string): Promise<DataRecord>;
  create(resource: string, data: DataRecord): Promise<DataRecord>;
  update(resource: string, id: string, data: DataRecord): Promise<DataRecord>;
  delete(resource: string, id: string): Promise<void>;
  aggregate(resource: string, params: AggregateParams): Promise<AggregateResult>;
  rpc(name: string, body: unknown): Promise<unknown>;
  subscribe?(resource: string, callback: (event: SubscriptionEvent) => void): () => void;
}

Hooks

useAdminData

CRUD data hook for any resource:

const { data, total, isLoading, error, refetch } = useAdminData('posts', {
  page: 1,
  perPage: 20,
  sort: 'createdAt.desc',
  filters: { status: 'eq.published' },
});

useAggregates

Server-side aggregation hooks:

const { data: count } = useCount('posts');
const { data: grouped } = useGroupedCount('posts', 'status');
const { data: stats } = useStats('posts', 'views', ['sum', 'avg', 'max']);

useRpc

Call server-side RPC functions:

const { data, isLoading, call } = useRpc('dashboard-stats', {
  autoFetch: true,
  body: { period: '7d' },
});

useStorageConfig

Access storage configuration for image uploads:

const { uploadUrl, cdnUrl } = useStorageConfig();

Screens

Built-in screens that render from config:

| Component | Description | |-----------|-------------| | AdminLayout | Navigation sidebar + content area | | AdminDashboard | Overview with stat cards | | ResourceList | Paginated table with filters | | ResourceShow | Read-only record detail | | ResourceCreate | Create form | | ResourceEdit | Edit form | | ResourceDashboard | Per-resource analytics |

Manual Screen Usage

import { AdminLayout, ResourceList, ResourceEdit } from '@everystack/admin';

// In your router:
<AdminLayout config={config}>
  <ResourceList resource="posts" config={config.resources.posts} />
</AdminLayout>

Image Fields

Image upload field renderers for use in create/edit forms:

import { ImageFieldInput, AvatarFieldInput, ImageDisplay } from '@everystack/admin';

// In a form
<ImageFieldInput
  value={formData.coverImage}
  onChange={(key) => setFormData({ ...formData, coverImage: key })}
  storageConfig={{ uploadUrl: '/api/storage', cdnUrl: 'https://cdn.example.com' }}
/>

<AvatarFieldInput
  value={formData.avatar}
  onChange={(key) => setFormData({ ...formData, avatar: key })}
  storageConfig={storageConfig}
/>

// Read-only display
<ImageDisplay value={record.coverImage} cdnUrl="https://cdn.example.com" />

Dashboard Widgets

SVG-based chart components for analytics:

import { StatDials, DimensionBars, TimeHistogram, EventTable } from '@everystack/admin';

<StatDials data={[{ label: 'Users', value: 1234, trend: 12 }]} />
<DimensionBars data={[{ label: 'iOS', value: 60 }, { label: 'Android', value: 40 }]} />
<TimeHistogram data={timeSeriesData} />
<EventTable columns={columns} data={events} />

Presets

Pre-built admin configurations for common schemas:

import { twitterPreset } from '@everystack/admin/presets/twitter';

const config = twitterPreset({
  // Override any resource config
  resources: {
    posts: { label: 'Tweets' },
  },
});

Utilities

import { toCamelCase, toSnakeCase, resolveImageUrl, uploadFile } from '@everystack/admin';

// Naming conversion
toCamelCase('created_at'); // 'createdAt'
toSnakeCase('createdAt');  // 'created_at'

// Image URL resolution
const url = resolveImageUrl('uploads/abc.jpg', { cdnUrl: 'https://cdn.example.com' });

// File upload
const result = await uploadFile(file, {
  uploadUrl: '/api/storage/presign',
  getToken: () => token,
});

Peer Dependencies

| Package | Version | |---------|---------| | @everystack/api | >=0.1.0 | | @everystack/ui | >=0.1.0 | | react | >=18.0.0 | | react-native | >=0.72.0 |


Part of everystack — a self-hosted application stack for Expo apps on AWS.

License

AGPL-3.0-only © Scalable Technology, Inc.

A commercial license is available for organizations that cannot or do not wish to comply with the AGPL-3.0 terms. For commercial licensing, contact [email protected].