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

noboil

v0.0.2

Published

Schema-first, zero-boilerplate fullstack. Pick your database.

Readme

noboil

npm license

v0.0.1 · Schema-first, zero-boilerplate fullstack. Pick your database.

Peer deps: @auth/core, @convex-dev/auth, convex, convex-helpers, next, react, react-dom, spacetimedb, zod

One schema. Typed backend. Auto forms. Zero boilerplate.

Define a Zod schema once. Get authenticated CRUD, typesafe forms, file upload, real-time subscriptions, pagination, search, soft delete, org multi-tenancy with ACL, rate limiting, and conflict detection — all generated. Currently supports Convex and SpacetimeDB.

Quick Start

bunx noboil@latest init

Pick a database, name your project, done. The CLI scaffolds the full monorepo — all 5 vertical demos (blog, chat, movie, org, poll)

CLI

Run noboil with no args for an interactive dashboard with single-key hotkeys:

| key | command | what it does | | --- | ------------- | ------------------------------------------------------------ | | i | init | create a new project | | t | status | project snapshot (drift, sync age, health) | | d | doctor | health check; doctor --fix auto-remediates | | s | sync | pull upstream changes (cached at ~/.noboil/upstream.git) | | a | add | scaffold a table (auto-dispatches by DB in .noboilrc.json) | | e | eject | inline the noboil library into lib/noboil | | u | upgrade | bun add noboil@latest | | c | completions | print shell completion script |

All commands also work non-interactively. noboil init my-app --db=convex, noboil add post --type=owned --fields="title:string,content:string", etc. (Valid --type= values: cache, child, kv, log, org, owned, quota, singleton.) Run noboil <cmd> --help for options.

Shell completions:

noboil completions bash              # print
noboil completions install zsh       # append to ~/.zshrc

Crash logs land in ~/.noboil/last-error.log. Print the most recent with noboil doctor --last-error.

Extending noboil add

Drop a noboil.config.ts at the project root to hook lifecycle events:

import { defineConfig } from 'noboil/config'
export default defineConfig({
  hooks: {
    afterAdd: async ({ name, type }) => {
      // e.g. run a formatter, notify Slack, etc.
    }
  }
})

Before / After

Without noboil — each database has its own schema syntax, validator types, and CRUD boilerplate:

// schema.ts — Convex validators
import { defineSchema, defineTable } from 'convex/server'
import { v } from 'convex/values'

export default defineSchema({
  blog: defineTable({
    title: v.string(),
    content: v.string(),
    category: v.string(),
    published: v.boolean(),
    coverImage: v.optional(v.string()),
    tags: v.optional(v.array(v.string())),
    userId: v.id('users'),
    updatedAt: v.number()
  })
    .index('by_userId', ['userId'])
    .index('by_published', ['published'])
})
// blog.ts — manual CRUD
export const list = query({
  args: { paginationOpts: paginationOptsValidator },
  handler: async (ctx, { paginationOpts }) => {
    const userId = await getAuthUserId(ctx)
    if (!userId) throw new Error('Not authenticated')
    return ctx.db
      .query('blog')
      .withIndex('by_userId', q => q.eq('userId', userId))
      .order('desc')
      .paginate(paginationOpts)
  }
})

export const create = mutation({
  args: {
    title: v.string(),
    content: v.string(),
    category: v.string(),
    published: v.boolean()
  },
  handler: async (ctx, args) => {
    const userId = await getAuthUserId(ctx)
    if (!userId) throw new Error('Not authenticated')
    return ctx.db.insert('blog', { ...args, userId, updatedAt: Date.now() })
  }
})

export const update = mutation({
  args: {
    id: v.id('blog'),
    title: v.optional(v.string()),
    content: v.optional(v.string())
  },
  handler: async (ctx, { id, ...fields }) => {
    const userId = await getAuthUserId(ctx)
    if (!userId) throw new Error('Not authenticated')
    const doc = await ctx.db.get(id)
    if (!doc || doc.userId !== userId) throw new Error('Not found')
    await ctx.db.patch(id, { ...fields, updatedAt: Date.now() })
  }
})

export const rm = mutation({
  args: { id: v.id('blog') },
  handler: async (ctx, { id }) => {
    const userId = await getAuthUserId(ctx)
    if (!userId) throw new Error('Not authenticated')
    const doc = await ctx.db.get(id)
    if (!doc || doc.userId !== userId) throw new Error('Not found')
    await ctx.db.delete(id)
  }
})
// schema.ts — SpacetimeDB table + type builders
import { schema, table, t } from 'spacetimedb/server'

const blog = table(
  {
    name: 'blog',
    public: true,
    indexes: [
      { name: 'by_user', algorithm: 'btree', columns: ['userId'] },
      { name: 'by_published', algorithm: 'btree', columns: ['published'] }
    ]
  },
  {
    id: t.u32().primaryKey().autoInc(),
    title: t.string(),
    content: t.string(),
    category: t.string(),
    published: t.bool(),
    coverImage: t.option(t.string()),
    tags: t.option(t.array(t.string())),
    userId: t.identity(),
    createdAt: t.timestamp(),
    updatedAt: t.timestamp()
  }
)

const spacetimedb = schema(blog)
// reducers — manual CRUD
spacetimedb.reducer(
  'create_blog',
  {
    title: t.string(),
    content: t.string(),
    category: t.string(),
    published: t.bool()
  },
  (ctx, args) => {
    ctx.db.blog.insert({
      id: 0,
      ...args,
      updatedAt: ctx.timestamp,
      userId: ctx.sender
    })
  }
)

spacetimedb.reducer(
  'update_blog',
  { id: t.u32(), title: t.string().optional(), content: t.string().optional() },
  (ctx, { id, ...fields }) => {
    const row = ctx.db.blog.id.find(id)
    if (!row || !identityEquals(row.userId, ctx.sender))
      throw new SenderError('NOT_FOUND')
    ctx.db.blog.id.update({ ...row, ...fields, updatedAt: ctx.timestamp })
  }
)

spacetimedb.reducer('delete_blog', { id: t.u32() }, (ctx, { id }) => {
  const row = ctx.db.blog.id.find(id)
  if (!row || !identityEquals(row.userId, ctx.sender))
    throw new SenderError('NOT_FOUND')
  ctx.db.blog.id.delete(id)
})

With noboil — one Zod schema, same code for both databases:

const s = schema({
  owned: {
    blog: object({
      title: string().min(1, 'Required'),
      content: string().min(3),
      category: zenum(['tech', 'life', 'tutorial']),
      published: boolean(),
      coverImage: file().nullable().optional(),
      tags: array(string()).max(5).optional()
    })
  }
})
const api = noboil({
  ...config,
  tables: ({ table }) => ({
    blog: table(s.blog, {
      rateLimit: { max: 10, window: 60_000 },
      search: 'content'
    })
  })
})

Auth, ownership, Zod validation, file upload, cursor pagination, rate limiting, conflict detection — all included. Same API across databases. create, update, and rm each accept single or bulk input (up to 100 items).

Factories

Each table declares its shape via Zod and picks a factory that matches its access pattern. noboil generates the matching CRUD/ops per factory.

| Factory | Shape | Generates | Use for | | ----------- | ------------------------- | ------------------------------------------------------------------------ | ------------------------------------------ | | cache | keyed external API cache | get/load/refresh/invalidate/purge | TMDB movies, Gravatar avatars | | child | nested under a parent | create/list/rm/update by parentId | comments under posts, items under orders | | kv | string-keyed state | get (public) / set/rm (role-gated) | feature flags, status banners, site config | | log | append-only event stream | append/listAfter/purgeByParent with per-parent seq + idempotency | messages, audit trails, event sourcing | | org | org-scoped with editors | addEditor/removeEditor + full CRUD | multi-tenant, team-shared resources | | owned | user-scoped | create/list/read/update/rm | user-owned data (posts, chats, tasks) | | quota | sliding-window rate limit | check/record/consume | anti-spam, vote throttling, API limits | | singleton | one per user | get/upsert | user preferences, profiles |

One s.ts file, one Zod schema per table, one factory per access pattern:

const s = schema({
  owned:     { chat: object({...}) },                                          // user-scoped CRUD
  log:       { message: { parent: 'chat', schema: object({...}) } },           // append-only log
  kv:        { banner: { keys: ['active'] as const, schema: object({...}), writeRole: 'admin' } },
  quota:     { sendMessage: { limit: 30, durationMs: 60_000 } },               // 30 msgs/minute
  singleton: { prefs: object({...}) }
})

Each factory’s React hook mirrors the server API: useCrud / useSingleton / useCache / useLog / useKv / useQuota. See doc/content/docs/ for per-factory guides.

Monorepo Structure

noboil/
  web/
<!-- AUTO-GENERATED:DEMO-TREE -->
    cvx/              5 Convex demo web apps (blog, chat, movie, org, poll)
    stdb/             5 SpacetimeDB demo web apps (blog, chat, movie, org, poll)
<!-- /AUTO-GENERATED:DEMO-TREE -->
  doc/                Documentation site (fumadocs)
  lib/
    fe/               shared frontend utilities (Next.js + auth shells)
    e2e/              shared Playwright utilities
  backend/
    convex/           Convex backend (schema + functions)
    spacetimedb/      SpacetimeDB backend (module + bindings)
  readonly/ui/        shared shadcn components (synced from cnsync, read-only)
  lib/noboil/           noboil — single published package (CLI + convex + spacetimedb + shared)

Package

A single published package noboil ships the CLI, Convex bindings, SpacetimeDB bindings, and shared utilities. Subpath exports (noboil/convex/*, noboil/spacetimedb/*) or conditional exports (noboil/components, noboil/server, etc. resolved per customConditions) give you both ergonomic and explicit import styles.

Requirements

Bun ≥ 1.3. TypeScript ≥ 5.0 with moduleResolution: "bundler" or "nodenext" (required for customConditions). Next.js, Vite, or any ESM bundler.

The noboil/components, noboil/react, and noboil/next subpaths import shadcn primitives via the @a/ui path alias. Consumers using these subpaths must add the alias to their tsconfig.json (e.g. "@a/ui/*": ["./components/ui/*"]) and have the matching shadcn components installed. Backend-only subpaths (noboil/server, noboil/schema, noboil/zod, etc.) have no such requirement.

Docs

noboil.dev/docs

License

MIT. Author: 1qh.